1use std::{fmt::Display, io, path::Path, sync::Arc};
12
13use chrono::{DateTime, NaiveDate, Utc};
14use enumflags2::{BitFlags, bitflags};
15use futures_util::TryStreamExt;
16use reqwest::Method;
17use serde::{Deserialize, Serialize};
18use tokio_util::io::StreamReader;
19
20use crate::{
21 DocumentCustomField, Error, Result, client::PaperlessClient, correspondent::CorrespondentId,
22 custom_field::CustomFieldId, document_type::DocumentTypeId, tag::TagId, user::UserId,
23};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
26#[repr(transparent)]
27pub struct DocumentId(pub i32);
28
29#[derive(Debug, Clone)]
41pub struct Document {
42 data: DocumentData,
43 client: Arc<PaperlessClient>,
44 content_is_truncated: bool,
45 changed_values: BitFlags<ChangedAttributes>,
46}
47
48#[derive(Debug, Clone, Deserialize)]
49pub(crate) struct DocumentData {
50 id: DocumentId,
51 archive_serial_number: Option<ArchiveSerialNumber>,
52 original_file_name: String,
53 added: DateTime<Utc>,
54 created: Option<NaiveDate>,
55 modified: DateTime<Utc>,
56 page_count: u32,
57 title: String,
58 content: String,
59 tags: Vec<TagId>,
60 owner: Option<UserId>,
61 correspondent: Option<CorrespondentId>,
62 custom_fields: Vec<DocumentCustomField>,
63 document_type: Option<DocumentTypeId>,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
67#[repr(transparent)]
68pub struct ArchiveSerialNumber(pub u32);
69
70#[bitflags]
71#[repr(u16)]
72#[derive(Copy, Clone, Debug, PartialEq)]
73enum ChangedAttributes {
74 Title,
75 Content,
76 Tags,
77 CustomFields,
78 Correspondent,
79 DocumentType,
80 Created,
81 Owner,
82}
83
84#[derive(Debug, Clone)]
86pub enum Content<'a> {
87 Full(&'a str),
89
90 Truncated(&'a str),
92}
93
94#[derive(Debug, Serialize)]
95struct PatchRequest {
96 #[serde(skip_serializing_if = "Option::is_none")]
97 title: Option<String>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
100 content: Option<String>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
103 tags: Option<Vec<TagId>>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
106 custom_fields: Option<Vec<DocumentCustomField>>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
109 correspondent: Option<CorrespondentId>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
112 document_type: Option<DocumentTypeId>,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
115 created: Option<NaiveDate>,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
118 owner: Option<UserId>,
119}
120
121impl std::fmt::Display for DocumentId {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 write!(f, "{}", self.0)
124 }
125}
126
127impl std::fmt::Display for ArchiveSerialNumber {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 write!(f, "{}", self.0)
130 }
131}
132
133impl Document {
134 pub(crate) fn new(
135 data: DocumentData,
136 client: Arc<PaperlessClient>,
137 content_is_truncated: bool,
138 ) -> Self {
139 Self {
140 data,
141 client,
142 content_is_truncated,
143 changed_values: BitFlags::default(),
144 }
145 }
146
147 #[inline]
149 #[must_use]
150 pub fn id(&self) -> DocumentId {
151 self.data.id
152 }
153
154 #[inline]
156 #[must_use]
157 pub fn archive_serial_number(&self) -> Option<ArchiveSerialNumber> {
158 self.data.archive_serial_number
159 }
160
161 #[inline]
163 #[must_use]
164 pub fn added(&self) -> &DateTime<Utc> {
165 &self.data.added
166 }
167
168 #[inline]
170 #[must_use]
171 pub fn created(&self) -> Option<&NaiveDate> {
172 self.data.created.as_ref()
173 }
174
175 #[inline]
177 #[must_use]
178 pub fn modified(&self) -> &DateTime<Utc> {
179 &self.data.modified
180 }
181
182 #[inline]
184 #[must_use]
185 pub fn title(&self) -> &str {
186 &self.data.title
187 }
188
189 #[inline]
191 #[must_use]
192 pub fn original_file_name(&self) -> &str {
193 &self.data.original_file_name
194 }
195
196 #[inline]
198 #[must_use]
199 pub fn correspondent(&self) -> Option<CorrespondentId> {
200 self.data.correspondent
201 }
202
203 #[inline]
205 #[must_use]
206 pub fn owner(&self) -> Option<UserId> {
207 self.data.owner
208 }
209
210 #[inline]
212 #[must_use]
213 pub fn document_type(&self) -> Option<DocumentTypeId> {
214 self.data.document_type
215 }
216
217 #[inline]
219 #[must_use]
220 pub fn page_count(&self) -> u32 {
221 self.data.page_count
222 }
223
224 #[inline]
226 #[must_use]
227 pub fn tags(&self) -> &[TagId] {
228 &self.data.tags
229 }
230
231 #[inline]
233 #[must_use]
234 pub fn custom_fields(&self) -> &[DocumentCustomField] {
235 &self.data.custom_fields
236 }
237
238 #[inline]
240 #[must_use]
241 pub fn content(&self) -> Content<'_> {
242 if self.content_is_truncated {
243 Content::Truncated(&self.data.content)
244 } else {
245 Content::Full(&self.data.content)
246 }
247 }
248
249 pub fn add_tag(&mut self, tag_id: TagId) {
251 if !self.data.tags.contains(&tag_id) {
252 self.data.tags.push(tag_id);
253 self.changed_values |= ChangedAttributes::Tags;
254 }
255 }
256
257 pub fn remove_tag(&mut self, tag_id: TagId) {
258 if let Some(index) = self.data.tags.iter().position(|id| *id == tag_id) {
259 self.data.tags.remove(index);
260 self.changed_values |= ChangedAttributes::Tags;
261 }
262 }
263
264 pub fn set_title(&mut self, title: &str) {
266 self.data.title = title.to_string();
267 self.changed_values |= ChangedAttributes::Title;
268 }
269
270 pub fn set_content(&mut self, content: &str) {
272 self.data.content = content.to_string();
273 self.content_is_truncated = false;
274 self.changed_values |= ChangedAttributes::Content;
275 }
276
277 pub fn set_custom_field(&mut self, field: CustomFieldId, value: &str) {
279 for custom_field in &mut self.data.custom_fields {
280 if custom_field.field == field {
281 custom_field.value = value.to_string();
282 self.changed_values |= ChangedAttributes::CustomFields;
283 return;
284 }
285 }
286
287 self.data.custom_fields.push(DocumentCustomField {
288 field,
289 value: value.to_string(),
290 });
291 self.changed_values |= ChangedAttributes::CustomFields;
292 }
293
294 pub fn remove_custom_field(&mut self, field: CustomFieldId) {
296 if let Some(index) = self
297 .data
298 .custom_fields
299 .iter()
300 .position(|custom_field| custom_field.field == field)
301 {
302 self.data.custom_fields.remove(index);
303 self.changed_values |= ChangedAttributes::CustomFields;
304 }
305 }
306
307 pub fn set_created(&mut self, created: NaiveDate) {
309 self.data.created = Some(created);
310 self.changed_values |= ChangedAttributes::Created;
311 }
312
313 pub fn set_owner(&mut self, owner: UserId) {
315 self.data.owner = Some(owner);
316 self.changed_values |= ChangedAttributes::Owner;
317 }
318
319 pub fn set_correspondent(&mut self, correspondent: CorrespondentId) {
321 self.data.correspondent = Some(correspondent);
322 self.changed_values |= ChangedAttributes::Correspondent;
323 }
324
325 pub fn set_document_type(&mut self, document_type: DocumentTypeId) {
327 self.data.document_type = Some(document_type);
328 self.changed_values |= ChangedAttributes::DocumentType;
329 }
330
331 #[inline]
333 #[must_use]
334 pub fn is_dirty(&self) -> bool {
335 !self.changed_values.is_empty()
336 }
337
338 pub async fn reload(&mut self) -> Result<()> {
342 let document_data = self
343 .client
344 .as_ref()
345 .get_document_data_by_id(self.data.id)
346 .await?;
347
348 self.data = document_data;
349
350 self.changed_values = BitFlags::empty();
351 self.content_is_truncated = false;
352 Ok(())
353 }
354
355 pub async fn patch(&mut self) -> Result<()> {
359 if !self.is_dirty() {
360 return Ok(());
361 }
362
363 let patch = PatchRequest {
364 title: self
365 .changed_values
366 .contains(ChangedAttributes::Title)
367 .then_some(self.data.title.clone()),
368
369 content: self
370 .changed_values
371 .contains(ChangedAttributes::Content)
372 .then_some(self.data.content.clone()),
373
374 tags: self
375 .changed_values
376 .contains(ChangedAttributes::Tags)
377 .then_some(self.data.tags.clone()),
378
379 custom_fields: self
380 .changed_values
381 .contains(ChangedAttributes::CustomFields)
382 .then_some(
383 self.data
384 .custom_fields
385 .iter()
386 .map(|field| DocumentCustomField {
387 field: field.field,
388 value: field.value.clone(),
389 })
390 .collect(),
391 ),
392 correspondent: self
393 .changed_values
394 .contains(ChangedAttributes::Correspondent)
395 .then_some(self.data.correspondent)
396 .flatten(),
397
398 document_type: self
399 .changed_values
400 .contains(ChangedAttributes::DocumentType)
401 .then_some(self.data.document_type)
402 .flatten(),
403
404 created: self
405 .changed_values
406 .contains(ChangedAttributes::Created)
407 .then_some(self.data.created)
408 .flatten(),
409
410 owner: self
411 .changed_values
412 .contains(ChangedAttributes::Owner)
413 .then_some(self.data.owner)
414 .flatten(),
415 };
416
417 self.client
418 .request(
419 Method::PATCH,
420 &format!("/api/documents/{}/", self.data.id),
421 Some(&serde_json::to_value(patch).expect("Patch request")),
422 )
423 .await?;
424
425 self.changed_values = BitFlags::empty();
426 Ok(())
427 }
428
429 pub async fn get_full_content(&mut self) -> Result<()> {
431 if !self.content_is_truncated {
432 return Ok(());
433 }
434
435 let doc = self.client.get_document_data_by_id(self.data.id).await?;
436 self.data.content = doc.content;
437 self.content_is_truncated = false;
438 Ok(())
439 }
440
441 pub async fn download_to_file(&self, path: &Path) -> Result<()> {
443 let resp = self
444 .client
445 .request(
446 Method::GET,
447 &format!("/api/documents/{}/download/", self.data.id),
448 None,
449 )
450 .await?;
451
452 if !resp.status().is_success() {
453 return Err(Error::Other(format!(
454 "Failed to download document: {}",
455 resp.status()
456 )));
457 }
458
459 let mut stream = StreamReader::new(
460 resp.bytes_stream()
461 .map_err(|e| io::Error::other(format!("Failed to read response body: {e}"))),
462 );
463
464 let mut file = tokio::fs::File::create(path)
465 .await
466 .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
467
468 tokio::io::copy(&mut stream, &mut file)
469 .await
470 .map_err(|e| Error::Other(format!("Failed to write file: {e}")))?;
471
472 Ok(())
473 }
474
475 pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
477 let resp = self
478 .client
479 .request(
480 Method::GET,
481 &format!("/api/documents/{}/download/", self.data.id),
482 None,
483 )
484 .await?;
485
486 if resp.status().is_success() {
487 let bytes = resp
488 .bytes()
489 .await
490 .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
491 Ok(bytes.to_vec())
492 } else {
493 Err(Error::Other(format!(
494 "Failed to download document: {}",
495 resp.status()
496 )))
497 }
498 }
499}
500
501impl Display for Content<'_> {
502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503 match self {
504 Content::Full(text) => write!(f, "{text}"),
505 Content::Truncated(text) => write!(f, "{text}..."),
506 }
507 }
508}
509
510impl AsRef<str> for Content<'_> {
511 fn as_ref(&self) -> &str {
512 match self {
513 Content::Full(text) | Content::Truncated(text) => text,
514 }
515 }
516}