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