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