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