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