Skip to main content

paperless_api/
document.rs

1//! Types for working with Paperless documents.
2//!
3//! Document mutations are applied locally first.
4//! Methods such as [`set_title`](Document::set_title),
5//! [`set_content`](Document::set_content),
6//! [`add_tag`](Document::add_tag), etc..
7//! only update the in-memory [`Document`] value and mark it as changed.
8//! The changes are only sent to the Paperless server when
9//! [`patch`](Document::patch) is called.
10
11use std::{fmt::Display, sync::Arc, time::Duration};
12
13use chrono::{DateTime, NaiveDate, Utc};
14use derive_more::Display;
15use enumflags2::{BitFlags, bitflags};
16
17#[cfg(feature = "tokio-fs")]
18use futures_util::TryStreamExt;
19
20use reqwest::Method;
21use serde::{Deserialize, Serialize};
22
23#[cfg(feature = "tokio-fs")]
24use tokio::io::AsyncWriteExt;
25
26use crate::{
27    Error, Result,
28    client::PaperlessClient,
29    id::{
30        CorrespondentId, CustomFieldId, DocumentId, DocumentTypeId, StoragePathId, TagId, UserId,
31    },
32    metadata::custom_field::DocumentCustomField,
33    metadata::permission::ItemPermissions,
34    note::Note,
35    share_link::{ShareLink, ShareLinkFileVersion},
36};
37
38/// Represents a document.
39///
40/// Changes made through mutating methods such as
41/// [`set_title`](Document::set_title),
42/// [`set_content`](Document::set_content),
43/// [`add_tag`](Document::add_tag), and
44/// [`set_custom_field`](Document::set_custom_field)
45/// are only tracked locally at first.
46///
47/// They are not sent to the Paperless server until
48/// [`patch`](Document::patch) is called.
49#[derive(Debug, Clone)]
50pub struct Document {
51    data: DocumentData,
52
53    client: Arc<PaperlessClient>,
54    content_is_truncated: bool,
55    changed_values: BitFlags<ChangedAttributes>,
56}
57
58#[derive(Debug, Clone, Deserialize)]
59pub(crate) struct DocumentData {
60    id: DocumentId,
61    archive_serial_number: Option<ArchiveSerialNumber>,
62    original_file_name: String,
63    added: DateTime<Utc>,
64    created: Option<NaiveDate>,
65    modified: DateTime<Utc>,
66    page_count: u32,
67    title: String,
68    content: String,
69    tags: Vec<TagId>,
70    owner: Option<UserId>,
71    correspondent: Option<CorrespondentId>,
72    custom_fields: Vec<DocumentCustomField>,
73    document_type: Option<DocumentTypeId>,
74    storage_path: Option<StoragePathId>,
75    notes: Vec<Note>,
76
77    #[serde(flatten)]
78    permissions: ItemPermissions,
79}
80
81#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
82#[repr(transparent)]
83pub struct ArchiveSerialNumber(pub u32);
84
85#[bitflags]
86#[repr(u16)]
87#[derive(Copy, Clone, Debug, PartialEq)]
88enum ChangedAttributes {
89    Title,
90    Content,
91    Tags,
92    CustomFields,
93    Correspondent,
94    DocumentType,
95    Created,
96    Owner,
97    StoragePath,
98
99    Deleted,
100}
101
102/// The content (OCR) of a document, either full or truncated.
103#[derive(Debug, Clone)]
104pub enum Content<'a> {
105    /// Full content of the document.
106    Full(&'a str),
107
108    /// Truncated content of the document.
109    Truncated(&'a str),
110}
111
112#[derive(Debug, Serialize)]
113struct PatchRequest {
114    #[serde(skip_serializing_if = "Option::is_none")]
115    title: Option<String>,
116
117    #[serde(skip_serializing_if = "Option::is_none")]
118    content: Option<String>,
119
120    #[serde(skip_serializing_if = "Option::is_none")]
121    tags: Option<Vec<TagId>>,
122
123    #[serde(skip_serializing_if = "Option::is_none")]
124    custom_fields: Option<Vec<DocumentCustomField>>,
125
126    #[serde(skip_serializing_if = "Option::is_none")]
127    correspondent: Option<CorrespondentId>,
128
129    #[serde(skip_serializing_if = "Option::is_none")]
130    document_type: Option<DocumentTypeId>,
131
132    #[serde(skip_serializing_if = "Option::is_none")]
133    created: Option<NaiveDate>,
134
135    #[serde(skip_serializing_if = "Option::is_none")]
136    owner: Option<UserId>,
137
138    #[serde(skip_serializing_if = "Option::is_none")]
139    storage_path: Option<StoragePathId>,
140}
141
142#[derive(Debug, Serialize)]
143struct ShareLinkRequest {
144    document: DocumentId,
145    file_version: ShareLinkFileVersion,
146    expiration: DateTime<Utc>,
147}
148
149impl Document {
150    pub(crate) fn new(
151        data: DocumentData,
152        client: Arc<PaperlessClient>,
153        content_is_truncated: bool,
154    ) -> Self {
155        Self {
156            data,
157            client,
158            content_is_truncated,
159            changed_values: BitFlags::default(),
160        }
161    }
162
163    /// Get the unique identifier of the document.
164    #[inline]
165    #[must_use]
166    pub fn id(&self) -> DocumentId {
167        self.data.id
168    }
169
170    /// Get the archive serial number of the document.
171    #[inline]
172    #[must_use]
173    pub fn archive_serial_number(&self) -> Option<ArchiveSerialNumber> {
174        self.data.archive_serial_number
175    }
176
177    /// Get the timestamp when the document was added.
178    #[inline]
179    #[must_use]
180    pub fn added(&self) -> &DateTime<Utc> {
181        &self.data.added
182    }
183
184    /// Get the created timestamp of the document.
185    #[inline]
186    #[must_use]
187    pub fn created(&self) -> Option<&NaiveDate> {
188        self.data.created.as_ref()
189    }
190
191    /// Get the modified timestamp of the document.
192    #[inline]
193    #[must_use]
194    pub fn modified(&self) -> &DateTime<Utc> {
195        &self.data.modified
196    }
197
198    /// Get the title of the document.
199    #[inline]
200    #[must_use]
201    pub fn title(&self) -> &str {
202        &self.data.title
203    }
204
205    /// Get the original file name of the document.
206    #[inline]
207    #[must_use]
208    pub fn original_file_name(&self) -> &str {
209        &self.data.original_file_name
210    }
211
212    /// Get the correspondent id of the document.
213    #[inline]
214    #[must_use]
215    pub fn correspondent(&self) -> Option<CorrespondentId> {
216        self.data.correspondent
217    }
218
219    /// Get the owner id of the document.
220    #[inline]
221    #[must_use]
222    pub fn owner(&self) -> Option<UserId> {
223        self.data.owner
224    }
225
226    /// Get the document type id of the document.
227    #[inline]
228    #[must_use]
229    pub fn document_type(&self) -> Option<DocumentTypeId> {
230        self.data.document_type
231    }
232
233    /// Get the number of pages in the document.
234    #[inline]
235    #[must_use]
236    pub fn page_count(&self) -> u32 {
237        self.data.page_count
238    }
239
240    /// Get all tag-ids for the document.
241    #[inline]
242    #[must_use]
243    pub fn tags(&self) -> &[TagId] {
244        &self.data.tags
245    }
246
247    /// Get all custom fields for the document.
248    #[inline]
249    #[must_use]
250    pub fn custom_fields(&self) -> &[DocumentCustomField] {
251        &self.data.custom_fields
252    }
253
254    /// Get the content of the document.
255    #[inline]
256    #[must_use]
257    pub fn content(&self) -> Content<'_> {
258        if self.content_is_truncated {
259            Content::Truncated(&self.data.content)
260        } else {
261            Content::Full(&self.data.content)
262        }
263    }
264
265    /// Get the storage path of the document.
266    #[inline]
267    #[must_use]
268    pub fn storage_path(&self) -> Option<StoragePathId> {
269        self.data.storage_path
270    }
271
272    /// Get the notes for the document.
273    #[inline]
274    #[must_use]
275    pub fn notes(&self) -> &[Note] {
276        &self.data.notes
277    }
278
279    /// Get the permissions for the document.
280    #[inline]
281    #[must_use]
282    pub fn permissions(&self) -> &ItemPermissions {
283        &self.data.permissions
284    }
285
286    /// Add a tag to the document.
287    pub fn add_tag(&mut self, tag_id: TagId) {
288        if !self.data.tags.contains(&tag_id) {
289            self.data.tags.push(tag_id);
290            self.changed_values |= ChangedAttributes::Tags;
291        }
292    }
293
294    /// Remove a tag from the document.
295    pub fn remove_tag(&mut self, tag_id: TagId) {
296        if let Some(index) = self.data.tags.iter().position(|id| *id == tag_id) {
297            self.data.tags.remove(index);
298            self.changed_values |= ChangedAttributes::Tags;
299        }
300    }
301
302    /// Set the title of the document.
303    pub fn set_title(&mut self, title: &str) {
304        self.data.title = title.to_string();
305        self.changed_values |= ChangedAttributes::Title;
306    }
307
308    /// Set the content of the document.
309    pub fn set_content(&mut self, content: &str) {
310        self.data.content = content.to_string();
311        self.content_is_truncated = false;
312        self.changed_values |= ChangedAttributes::Content;
313    }
314
315    /// Set a custom field for the document.
316    pub fn set_custom_field(&mut self, field: CustomFieldId, value: &str) {
317        for custom_field in &mut self.data.custom_fields {
318            if custom_field.field == field {
319                custom_field.value = value.to_string();
320                self.changed_values |= ChangedAttributes::CustomFields;
321                return;
322            }
323        }
324
325        self.data.custom_fields.push(DocumentCustomField {
326            field,
327            value: value.to_string(),
328        });
329        self.changed_values |= ChangedAttributes::CustomFields;
330    }
331
332    /// Remove a custom field from the document.
333    pub fn remove_custom_field(&mut self, field: CustomFieldId) {
334        if let Some(index) = self
335            .data
336            .custom_fields
337            .iter()
338            .position(|custom_field| custom_field.field == field)
339        {
340            self.data.custom_fields.remove(index);
341            self.changed_values |= ChangedAttributes::CustomFields;
342        }
343    }
344
345    /// Set the created date of the document.
346    pub fn set_created(&mut self, created: NaiveDate) {
347        self.data.created = Some(created);
348        self.changed_values |= ChangedAttributes::Created;
349    }
350
351    /// Set the owner of the document.
352    pub fn set_owner(&mut self, owner: UserId) {
353        self.data.owner = Some(owner);
354        self.changed_values |= ChangedAttributes::Owner;
355    }
356
357    /// Set the correspondent of the document.
358    pub fn set_correspondent(&mut self, correspondent: CorrespondentId) {
359        self.data.correspondent = Some(correspondent);
360        self.changed_values |= ChangedAttributes::Correspondent;
361    }
362
363    /// Set the document type of the document.
364    pub fn set_document_type(&mut self, document_type: DocumentTypeId) {
365        self.data.document_type = Some(document_type);
366        self.changed_values |= ChangedAttributes::DocumentType;
367    }
368
369    /// Set the storage path of the document.
370    pub fn set_storage_path(&mut self, storage_path: StoragePathId) {
371        self.data.storage_path = Some(storage_path);
372        self.changed_values |= ChangedAttributes::StoragePath;
373    }
374
375    /// Returns `true` if the document has unsaved changes.
376    #[inline]
377    #[must_use]
378    pub fn is_dirty(&self) -> bool {
379        !self.changed_values.is_empty() && !self.changed_values.contains(ChangedAttributes::Deleted)
380    }
381
382    /// Returns `true` if the document was deleted.
383    #[inline]
384    #[must_use]
385    pub fn is_deleted(&self) -> bool {
386        self.changed_values.contains(ChangedAttributes::Deleted)
387    }
388
389    fn fail_if_deleted(&self) -> Result<()> {
390        if self.is_deleted() {
391            Err(Error::AlreadyDeleted)
392        } else {
393            Ok(())
394        }
395    }
396
397    /// Refresh the document from the server.
398    ///
399    /// This will discard any local changes and replace them with the server's state.
400    pub async fn refresh(&mut self) -> Result<()> {
401        let document_data = self
402            .client
403            .as_ref()
404            .get_document_data_by_id(self.data.id)
405            .await?;
406
407        self.data = document_data;
408
409        self.changed_values = BitFlags::empty();
410        self.content_is_truncated = false;
411        Ok(())
412    }
413
414    /// Update the document on the server.
415    ///
416    /// This applies the currently tracked local changes to the remote Paperless document.
417    pub async fn patch(&mut self) -> Result<()> {
418        if !self.is_dirty() {
419            return Ok(());
420        }
421
422        self.fail_if_deleted()?;
423
424        let patch = PatchRequest {
425            title: self
426                .changed_values
427                .contains(ChangedAttributes::Title)
428                .then_some(self.data.title.clone()),
429
430            content: self
431                .changed_values
432                .contains(ChangedAttributes::Content)
433                .then_some(self.data.content.clone()),
434
435            tags: self
436                .changed_values
437                .contains(ChangedAttributes::Tags)
438                .then_some(self.data.tags.clone()),
439
440            custom_fields: self
441                .changed_values
442                .contains(ChangedAttributes::CustomFields)
443                .then_some(
444                    self.data
445                        .custom_fields
446                        .iter()
447                        .map(|field| DocumentCustomField {
448                            field: field.field,
449                            value: field.value.clone(),
450                        })
451                        .collect(),
452                ),
453            correspondent: self
454                .changed_values
455                .contains(ChangedAttributes::Correspondent)
456                .then_some(self.data.correspondent)
457                .flatten(),
458
459            document_type: self
460                .changed_values
461                .contains(ChangedAttributes::DocumentType)
462                .then_some(self.data.document_type)
463                .flatten(),
464
465            created: self
466                .changed_values
467                .contains(ChangedAttributes::Created)
468                .then_some(self.data.created)
469                .flatten(),
470
471            owner: self
472                .changed_values
473                .contains(ChangedAttributes::Owner)
474                .then_some(self.data.owner)
475                .flatten(),
476
477            storage_path: self
478                .changed_values
479                .contains(ChangedAttributes::StoragePath)
480                .then_some(self.data.storage_path)
481                .flatten(),
482        };
483
484        self.client
485            .request_with_body(
486                Method::PATCH,
487                &format!("/api/documents/{}/", self.data.id),
488                &patch,
489            )
490            .await?;
491
492        self.changed_values = BitFlags::empty();
493        Ok(())
494    }
495
496    pub async fn delete(&mut self) -> Result<()> {
497        self.client
498            .request(
499                Method::DELETE,
500                &format!("/api/documents/{}/", self.data.id),
501                None,
502            )
503            .await?;
504
505        self.changed_values = BitFlags::from(ChangedAttributes::Deleted);
506        Ok(())
507    }
508
509    /// Get the full content of the document, replacing any truncated content.
510    pub async fn get_full_content(&mut self) -> Result<()> {
511        self.fail_if_deleted()?;
512
513        if !self.content_is_truncated {
514            return Ok(());
515        }
516
517        let doc = self.client.get_document_data_by_id(self.data.id).await?;
518        self.data.content = doc.content;
519        self.content_is_truncated = false;
520        Ok(())
521    }
522
523    /// Download the document to a buffer.
524    pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
525        self.fail_if_deleted()?;
526
527        let resp = self
528            .client
529            .request(
530                Method::GET,
531                &format!("/api/documents/{}/download/", self.data.id),
532                None,
533            )
534            .await?;
535
536        if resp.status().is_success() {
537            let bytes = resp
538                .bytes()
539                .await
540                .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
541            Ok(bytes.to_vec())
542        } else {
543            Err(Error::Other(format!(
544                "Failed to download document: {}",
545                resp.status()
546            )))
547        }
548    }
549
550    /// Download the document to a file, requires the `tokio-fs` feature.
551    #[cfg(feature = "tokio-fs")]
552    pub async fn download_to_file(&self, path: &std::path::Path) -> Result<()> {
553        self.fail_if_deleted()?;
554
555        let resp = self
556            .client
557            .request(
558                Method::GET,
559                &format!("/api/documents/{}/download/", self.data.id),
560                None,
561            )
562            .await?;
563
564        if !resp.status().is_success() {
565            return Err(Error::Other(format!(
566                "Failed to download document: {}",
567                resp.status()
568            )));
569        }
570
571        let mut file = tokio::fs::File::create(path)
572            .await
573            .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
574
575        let mut stream = resp.bytes_stream();
576
577        while let Some(chunk) = stream
578            .try_next()
579            .await
580            .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?
581        {
582            file.write_all(&chunk)
583                .await
584                .map_err(|e| Error::Other(format!("Failed to write file: {e}")))?;
585        }
586
587        Ok(())
588    }
589
590    /// Generates a share link for the document that expires after the specified duration.
591    pub fn generate_share_link_duration(
592        &self,
593        valid_for: Duration,
594        version: ShareLinkFileVersion,
595    ) -> impl Future<Output = Result<ShareLink>> {
596        let expires = Utc::now() + valid_for;
597        self.generate_share_link_expires(expires, version)
598    }
599
600    /// Generates a share link for the document that expires at the specified time.
601    pub async fn generate_share_link_expires(
602        &self,
603        expires: DateTime<Utc>,
604        version: ShareLinkFileVersion,
605    ) -> Result<ShareLink> {
606        self.fail_if_deleted()?;
607
608        let resp = self
609            .client
610            .request(
611                Method::POST,
612                "/api/share_links/",
613                Some(
614                    &serde_json::to_value(ShareLinkRequest {
615                        document: self.data.id,
616                        file_version: version,
617                        expiration: expires,
618                    })
619                    .expect("Share link request"),
620                ),
621            )
622            .await?;
623
624        let mut share_link: ShareLink = resp
625            .json()
626            .await
627            .map_err(|e| Error::Other(format!("Failed to generate share link: {e}")))?;
628
629        share_link.base_url = self.client.base_url.clone();
630        Ok(share_link)
631    }
632}
633
634impl Display for Content<'_> {
635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636        match self {
637            Content::Full(text) => write!(f, "{text}"),
638            Content::Truncated(text) => write!(f, "{text}..."),
639        }
640    }
641}
642
643impl AsRef<str> for Content<'_> {
644    fn as_ref(&self) -> &str {
645        match self {
646            Content::Full(text) | Content::Truncated(text) => text,
647        }
648    }
649}