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};
12
13use chrono::{DateTime, NaiveDate, Utc};
14use enumflags2::{BitFlags, bitflags};
15use futures_util::TryStreamExt;
16use reqwest::Method;
17use serde::{Deserialize, Serialize};
18use tokio_util::io::StreamReader;
19
20use crate::{
21    DocumentCustomField, Error, Result, client::PaperlessClient, correspondent::CorrespondentId,
22    custom_field::CustomFieldId, document_type::DocumentTypeId, tag::TagId, user::UserId,
23};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
26#[repr(transparent)]
27pub struct DocumentId(pub i32);
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}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
67#[repr(transparent)]
68pub struct ArchiveSerialNumber(pub u32);
69
70#[bitflags]
71#[repr(u16)]
72#[derive(Copy, Clone, Debug, PartialEq)]
73enum ChangedAttributes {
74    Title,
75    Content,
76    Tags,
77    CustomFields,
78    Correspondent,
79    DocumentType,
80    Created,
81    Owner,
82}
83
84/// The content (OCR) of a document, either full or truncated.
85#[derive(Debug, Clone)]
86pub enum Content<'a> {
87    /// Full content of the document.
88    Full(&'a str),
89
90    /// Truncated content of the document.
91    Truncated(&'a str),
92}
93
94#[derive(Debug, Serialize)]
95struct PatchRequest {
96    #[serde(skip_serializing_if = "Option::is_none")]
97    title: Option<String>,
98
99    #[serde(skip_serializing_if = "Option::is_none")]
100    content: Option<String>,
101
102    #[serde(skip_serializing_if = "Option::is_none")]
103    tags: Option<Vec<TagId>>,
104
105    #[serde(skip_serializing_if = "Option::is_none")]
106    custom_fields: Option<Vec<DocumentCustomField>>,
107
108    #[serde(skip_serializing_if = "Option::is_none")]
109    correspondent: Option<CorrespondentId>,
110
111    #[serde(skip_serializing_if = "Option::is_none")]
112    document_type: Option<DocumentTypeId>,
113
114    #[serde(skip_serializing_if = "Option::is_none")]
115    created: Option<NaiveDate>,
116
117    #[serde(skip_serializing_if = "Option::is_none")]
118    owner: Option<UserId>,
119}
120
121impl std::fmt::Display for DocumentId {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        write!(f, "{}", self.0)
124    }
125}
126
127impl std::fmt::Display for ArchiveSerialNumber {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        write!(f, "{}", self.0)
130    }
131}
132
133impl Document {
134    pub(crate) fn new(
135        data: DocumentData,
136        client: Arc<PaperlessClient>,
137        content_is_truncated: bool,
138    ) -> Self {
139        Self {
140            data,
141            client,
142            content_is_truncated,
143            changed_values: BitFlags::default(),
144        }
145    }
146
147    /// Get the id of the document
148    #[inline]
149    #[must_use]
150    pub fn id(&self) -> DocumentId {
151        self.data.id
152    }
153
154    /// Get the archive serial number of the document.
155    #[inline]
156    #[must_use]
157    pub fn archive_serial_number(&self) -> Option<ArchiveSerialNumber> {
158        self.data.archive_serial_number
159    }
160
161    /// Get the timestamp when the document was added.
162    #[inline]
163    #[must_use]
164    pub fn added(&self) -> &DateTime<Utc> {
165        &self.data.added
166    }
167
168    /// Get the created timestamp of the document.
169    #[inline]
170    #[must_use]
171    pub fn created(&self) -> Option<&NaiveDate> {
172        self.data.created.as_ref()
173    }
174
175    /// Get the modified timestamp of the document.
176    #[inline]
177    #[must_use]
178    pub fn modified(&self) -> &DateTime<Utc> {
179        &self.data.modified
180    }
181
182    /// Get the title of the document.
183    #[inline]
184    #[must_use]
185    pub fn title(&self) -> &str {
186        &self.data.title
187    }
188
189    /// Get the original file name of the document.
190    #[inline]
191    #[must_use]
192    pub fn original_file_name(&self) -> &str {
193        &self.data.original_file_name
194    }
195
196    /// Get the correspondent id of the document.
197    #[inline]
198    #[must_use]
199    pub fn correspondent(&self) -> Option<CorrespondentId> {
200        self.data.correspondent
201    }
202
203    /// Get the owner id of the document.
204    #[inline]
205    #[must_use]
206    pub fn owner(&self) -> Option<UserId> {
207        self.data.owner
208    }
209
210    /// Get the document type id of the document.
211    #[inline]
212    #[must_use]
213    pub fn document_type(&self) -> Option<DocumentTypeId> {
214        self.data.document_type
215    }
216
217    /// Get the number of pages in the document.
218    #[inline]
219    #[must_use]
220    pub fn page_count(&self) -> u32 {
221        self.data.page_count
222    }
223
224    /// Get all tag-ids for the document.
225    #[inline]
226    #[must_use]
227    pub fn tags(&self) -> &[TagId] {
228        &self.data.tags
229    }
230
231    /// Get all custom fields for the document.
232    #[inline]
233    #[must_use]
234    pub fn custom_fields(&self) -> &[DocumentCustomField] {
235        &self.data.custom_fields
236    }
237
238    /// Get the content of the document.
239    #[inline]
240    #[must_use]
241    pub fn content(&self) -> Content<'_> {
242        if self.content_is_truncated {
243            Content::Truncated(&self.data.content)
244        } else {
245            Content::Full(&self.data.content)
246        }
247    }
248
249    /// Add a tag to the document.
250    pub fn add_tag(&mut self, tag_id: TagId) {
251        if !self.data.tags.contains(&tag_id) {
252            self.data.tags.push(tag_id);
253            self.changed_values |= ChangedAttributes::Tags;
254        }
255    }
256
257    pub fn remove_tag(&mut self, tag_id: TagId) {
258        if let Some(index) = self.data.tags.iter().position(|id| *id == tag_id) {
259            self.data.tags.remove(index);
260            self.changed_values |= ChangedAttributes::Tags;
261        }
262    }
263
264    /// Set the title of the document.
265    pub fn set_title(&mut self, title: &str) {
266        self.data.title = title.to_string();
267        self.changed_values |= ChangedAttributes::Title;
268    }
269
270    /// Set the content of the document.
271    pub fn set_content(&mut self, content: &str) {
272        self.data.content = content.to_string();
273        self.content_is_truncated = false;
274        self.changed_values |= ChangedAttributes::Content;
275    }
276
277    /// Set a custom field for the document.
278    pub fn set_custom_field(&mut self, field: CustomFieldId, value: &str) {
279        for custom_field in &mut self.data.custom_fields {
280            if custom_field.field == field {
281                custom_field.value = value.to_string();
282                self.changed_values |= ChangedAttributes::CustomFields;
283                return;
284            }
285        }
286
287        self.data.custom_fields.push(DocumentCustomField {
288            field,
289            value: value.to_string(),
290        });
291        self.changed_values |= ChangedAttributes::CustomFields;
292    }
293
294    /// Remove a custom field from the document.
295    pub fn remove_custom_field(&mut self, field: CustomFieldId) {
296        if let Some(index) = self
297            .data
298            .custom_fields
299            .iter()
300            .position(|custom_field| custom_field.field == field)
301        {
302            self.data.custom_fields.remove(index);
303            self.changed_values |= ChangedAttributes::CustomFields;
304        }
305    }
306
307    /// Set the created date of the document.
308    pub fn set_created(&mut self, created: NaiveDate) {
309        self.data.created = Some(created);
310        self.changed_values |= ChangedAttributes::Created;
311    }
312
313    /// Set the owner of the document.
314    pub fn set_owner(&mut self, owner: UserId) {
315        self.data.owner = Some(owner);
316        self.changed_values |= ChangedAttributes::Owner;
317    }
318
319    /// Set the correspondent of the document.
320    pub fn set_correspondent(&mut self, correspondent: CorrespondentId) {
321        self.data.correspondent = Some(correspondent);
322        self.changed_values |= ChangedAttributes::Correspondent;
323    }
324
325    /// Set the document type of the document.
326    pub fn set_document_type(&mut self, document_type: DocumentTypeId) {
327        self.data.document_type = Some(document_type);
328        self.changed_values |= ChangedAttributes::DocumentType;
329    }
330
331    /// Returns `true` if the document has unsaved changes.
332    #[inline]
333    #[must_use]
334    pub fn is_dirty(&self) -> bool {
335        !self.changed_values.is_empty()
336    }
337
338    /// Refresh the document from the server.
339    ///
340    /// This will discard any local changes and replace them with the server's state.
341    pub async fn reload(&mut self) -> Result<()> {
342        let document_data = self
343            .client
344            .as_ref()
345            .get_document_data_by_id(self.data.id)
346            .await?;
347
348        self.data = document_data;
349
350        self.changed_values = BitFlags::empty();
351        self.content_is_truncated = false;
352        Ok(())
353    }
354
355    /// Update the document on the server.
356    ///
357    /// This applies the currently tracked local changes to the remote Paperless document.
358    pub async fn patch(&mut self) -> Result<()> {
359        if !self.is_dirty() {
360            return Ok(());
361        }
362
363        let patch = PatchRequest {
364            title: self
365                .changed_values
366                .contains(ChangedAttributes::Title)
367                .then_some(self.data.title.clone()),
368
369            content: self
370                .changed_values
371                .contains(ChangedAttributes::Content)
372                .then_some(self.data.content.clone()),
373
374            tags: self
375                .changed_values
376                .contains(ChangedAttributes::Tags)
377                .then_some(self.data.tags.clone()),
378
379            custom_fields: self
380                .changed_values
381                .contains(ChangedAttributes::CustomFields)
382                .then_some(
383                    self.data
384                        .custom_fields
385                        .iter()
386                        .map(|field| DocumentCustomField {
387                            field: field.field,
388                            value: field.value.clone(),
389                        })
390                        .collect(),
391                ),
392            correspondent: self
393                .changed_values
394                .contains(ChangedAttributes::Correspondent)
395                .then_some(self.data.correspondent)
396                .flatten(),
397
398            document_type: self
399                .changed_values
400                .contains(ChangedAttributes::DocumentType)
401                .then_some(self.data.document_type)
402                .flatten(),
403
404            created: self
405                .changed_values
406                .contains(ChangedAttributes::Created)
407                .then_some(self.data.created)
408                .flatten(),
409
410            owner: self
411                .changed_values
412                .contains(ChangedAttributes::Owner)
413                .then_some(self.data.owner)
414                .flatten(),
415        };
416
417        self.client
418            .request(
419                Method::PATCH,
420                &format!("/api/documents/{}/", self.data.id),
421                Some(&serde_json::to_value(patch).expect("Patch request")),
422            )
423            .await?;
424
425        self.changed_values = BitFlags::empty();
426        Ok(())
427    }
428
429    /// Get the full content of the document, replacing any truncated content.
430    pub async fn get_full_content(&mut self) -> Result<()> {
431        if !self.content_is_truncated {
432            return Ok(());
433        }
434
435        let doc = self.client.get_document_data_by_id(self.data.id).await?;
436        self.data.content = doc.content;
437        self.content_is_truncated = false;
438        Ok(())
439    }
440
441    /// Download the document to a file.
442    pub async fn download_to_file(&self, path: &Path) -> Result<()> {
443        let resp = self
444            .client
445            .request(
446                Method::GET,
447                &format!("/api/documents/{}/download/", self.data.id),
448                None,
449            )
450            .await?;
451
452        if !resp.status().is_success() {
453            return Err(Error::Other(format!(
454                "Failed to download document: {}",
455                resp.status()
456            )));
457        }
458
459        let mut stream = StreamReader::new(
460            resp.bytes_stream()
461                .map_err(|e| io::Error::other(format!("Failed to read response body: {e}"))),
462        );
463
464        let mut file = tokio::fs::File::create(path)
465            .await
466            .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
467
468        tokio::io::copy(&mut stream, &mut file)
469            .await
470            .map_err(|e| Error::Other(format!("Failed to write file: {e}")))?;
471
472        Ok(())
473    }
474
475    /// Download the document to a buffer.
476    pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
477        let resp = self
478            .client
479            .request(
480                Method::GET,
481                &format!("/api/documents/{}/download/", self.data.id),
482                None,
483            )
484            .await?;
485
486        if resp.status().is_success() {
487            let bytes = resp
488                .bytes()
489                .await
490                .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
491            Ok(bytes.to_vec())
492        } else {
493            Err(Error::Other(format!(
494                "Failed to download document: {}",
495                resp.status()
496            )))
497        }
498    }
499}
500
501impl Display for Content<'_> {
502    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503        match self {
504            Content::Full(text) => write!(f, "{text}"),
505            Content::Truncated(text) => write!(f, "{text}..."),
506        }
507    }
508}
509
510impl AsRef<str> for Content<'_> {
511    fn as_ref(&self) -> &str {
512        match self {
513            Content::Full(text) | Content::Truncated(text) => text,
514        }
515    }
516}