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    ///
411    /// Returns the raw thumbnail image data.
412    pub async fn thumbnail(&self) -> Result<Vec<u8>> {
413        let resp = self
414            .client
415            .request(
416                Method::GET,
417                &format!("/api/documents/{}/thumb/", self.data.id),
418                None,
419                None,
420            )
421            .await?;
422
423        Ok(resp
424            .bytes()
425            .await
426            .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?
427            .to_vec())
428    }
429
430    /// Update the document on the server.
431    ///
432    /// This applies the currently tracked local changes to the remote Paperless document.
433    pub async fn patch(&mut self) -> Result<()> {
434        if !self.is_dirty() {
435            return Ok(());
436        }
437
438        self.fail_if_deleted()?;
439
440        let patch = UpdateDocumentData {
441            title: self
442                .changed_values
443                .contains(ChangedAttributes::Title)
444                .then_some(self.data.title.clone()),
445
446            archive_serial_number: self
447                .changed_values
448                .contains(ChangedAttributes::ArchiveSerialNumber)
449                .then_some(self.data.archive_serial_number),
450
451            content: self
452                .changed_values
453                .contains(ChangedAttributes::Content)
454                .then_some(self.data.content.clone()),
455
456            tags: self
457                .changed_values
458                .contains(ChangedAttributes::Tags)
459                .then_some(self.data.tags.clone()),
460
461            custom_fields: self
462                .changed_values
463                .contains(ChangedAttributes::CustomFields)
464                .then_some(self.data.custom_fields.clone()),
465
466            correspondent: self
467                .changed_values
468                .contains(ChangedAttributes::Correspondent)
469                .then_some(self.data.correspondent),
470
471            document_type: self
472                .changed_values
473                .contains(ChangedAttributes::DocumentType)
474                .then_some(self.data.document_type),
475
476            created: self
477                .changed_values
478                .contains(ChangedAttributes::Created)
479                .then_some(self.data.created),
480
481            owner: self
482                .changed_values
483                .contains(ChangedAttributes::Owner)
484                .then_some(self.data.owner),
485
486            storage_path: self
487                .changed_values
488                .contains(ChangedAttributes::StoragePath)
489                .then_some(self.data.storage_path),
490        };
491
492        self.client
493            .request(
494                Method::PATCH,
495                &format!("/api/documents/{}/", self.data.id),
496                Some(&serde_json::to_value(&patch).map_err(|e| Error::Other(e.to_string()))?),
497                None,
498            )
499            .await?;
500
501        self.changed_values = BitFlags::empty();
502        Ok(())
503    }
504
505    /// Delete the document
506    pub async fn delete(&mut self) -> Result<()> {
507        self.client
508            .request(
509                Method::DELETE,
510                &format!("/api/documents/{}/", self.data.id),
511                None,
512                None,
513            )
514            .await?;
515
516        self.changed_values = BitFlags::from(ChangedAttributes::Deleted);
517        Ok(())
518    }
519
520    /// Get the full content of the document, replacing any truncated content.
521    pub async fn get_full_content(&mut self) -> Result<()> {
522        self.fail_if_deleted()?;
523
524        if !self.content_is_truncated {
525            return Ok(());
526        }
527
528        let doc = self.client.get_document_data_by_id(self.data.id).await?;
529        self.data.content = doc.content;
530        self.content_is_truncated = false;
531        Ok(())
532    }
533
534    /// Download the document to a buffer.
535    pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
536        self.fail_if_deleted()?;
537
538        let resp = self
539            .client
540            .request(
541                Method::GET,
542                &format!("/api/documents/{}/download/", self.data.id),
543                None,
544                None,
545            )
546            .await?;
547
548        if resp.status().is_success() {
549            let bytes = resp
550                .bytes()
551                .await
552                .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
553            Ok(bytes.to_vec())
554        } else {
555            Err(Error::Other(format!(
556                "Failed to download document: {}",
557                resp.status()
558            )))
559        }
560    }
561
562    /// Download the document to a file, requires the `tokio-fs` feature.
563    pub async fn download_to_file(&self, path: &std::path::Path) -> Result<()> {
564        self.fail_if_deleted()?;
565
566        let resp = self
567            .client
568            .request(
569                Method::GET,
570                &format!("/api/documents/{}/download/", self.data.id),
571                None,
572                None,
573            )
574            .await?;
575
576        if !resp.status().is_success() {
577            return Err(Error::Other(format!(
578                "Failed to download document: {}",
579                resp.status()
580            )));
581        }
582
583        let mut file = tokio::fs::File::create(path)
584            .await
585            .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
586
587        resp.bytes_stream()
588            .map_err(|e| Error::Other(format!("Failed to read document chunk: {e}")))
589            .try_fold(&mut file, |file, chunk| async move {
590                file.write_all(&chunk).await.map_err(|e| {
591                    Error::Other(format!("Failed to save document chunk to file: {e}"))
592                })?;
593                Ok(file)
594            })
595            .await?;
596
597        Ok(())
598    }
599
600    /// Generates a share link for the document that expires after the specified duration.
601    pub fn generate_share_link_duration(
602        &self,
603        valid_for: Duration,
604        version: ShareLinkFileVersion,
605    ) -> impl Future<Output = Result<ShareLink>> {
606        let expires = Utc::now() + valid_for;
607        self.generate_share_link_expires(expires, version)
608    }
609
610    /// Generates a share link for the document that expires at the specified time.
611    pub async fn generate_share_link_expires(
612        &self,
613        expires: DateTime<Utc>,
614        version: ShareLinkFileVersion,
615    ) -> Result<ShareLink> {
616        self.fail_if_deleted()?;
617
618        let mut share_link = self
619            .client
620            .request_json::<ShareLink>(
621                Method::POST,
622                "/api/share_links/",
623                Some(
624                    &serde_json::to_value(&CreateShareLink {
625                        document: self.id(),
626                        expiration: expires,
627                        file_version: version,
628                    })
629                    .map_err(|e| Error::Other(e.to_string()))?,
630                ),
631                None,
632            )
633            .await?;
634
635        share_link.base_url = self.client.base_url.clone();
636        Ok(share_link)
637    }
638}
639
640impl Display for Content<'_> {
641    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642        match self {
643            Content::Full(text) => write!(f, "{text}"),
644            Content::Truncated(text) => write!(f, "{text}..."),
645        }
646    }
647}
648
649impl AsRef<str> for Content<'_> {
650    fn as_ref(&self) -> &str {
651        match self {
652            Content::Full(text) | Content::Truncated(text) => text,
653        }
654    }
655}