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 enumflags2::{BitFlags, bitflags};
14use futures_util::TryStreamExt;
15use reqwest::Method;
16use serde::{Deserialize, Serialize};
17use tokio_util::io::StreamReader;
18
19use crate::{
20    DocumentCustomField, Error, Result, client::PaperlessClient, correspondent::CorrespondentId,
21    custom_field::CustomFieldId, document_type::DocumentTypeId, tag::TagId,
22};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
25#[repr(transparent)]
26pub struct DocumentId(pub i32);
27
28/// Represents a document.
29///
30/// Changes made through mutating methods such as
31/// [`set_title`](Document::set_title),
32/// [`set_content`](Document::set_content),
33/// [`add_tag`](Document::add_tag), and
34/// [`set_custom_field`](Document::set_custom_field)
35/// are only tracked locally at first.
36///
37/// They are not sent to the Paperless server until
38/// [`patch`](Document::patch) is called.
39#[derive(Debug, Clone)]
40pub struct Document {
41    data: DocumentData,
42    client: Arc<PaperlessClient>,
43    content_is_truncated: bool,
44    changed_values: BitFlags<ChangedAttributes>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub(crate) struct DocumentData {
49    id: DocumentId,
50    original_file_name: String,
51    page_count: u32,
52    title: String,
53    content: String,
54    tags: Vec<TagId>,
55    owner: i32,
56    correspondent: Option<CorrespondentId>,
57    custom_fields: Vec<DocumentCustomField>,
58    document_type: Option<DocumentTypeId>,
59}
60
61#[bitflags]
62#[repr(u8)]
63#[derive(Copy, Clone, Debug, PartialEq)]
64enum ChangedAttributes {
65    Title,
66    Content,
67    Tags,
68    CustomFields,
69    Correspondent,
70    DocumentType,
71}
72
73/// The content (OCR) of a document, either full or truncated.
74#[derive(Debug, Clone)]
75pub enum Content<'a> {
76    /// Full content of the document.
77    Full(&'a str),
78
79    /// Truncated content of the document.
80    Truncated(&'a str),
81}
82
83#[derive(Debug, Serialize)]
84struct PatchRequest {
85    #[serde(skip_serializing_if = "Option::is_none")]
86    title: Option<String>,
87
88    #[serde(skip_serializing_if = "Option::is_none")]
89    content: Option<String>,
90
91    #[serde(skip_serializing_if = "Option::is_none")]
92    tags: Option<Vec<TagId>>,
93
94    #[serde(skip_serializing_if = "Option::is_none")]
95    custom_fields: Option<Vec<DocumentCustomField>>,
96
97    #[serde(skip_serializing_if = "Option::is_none")]
98    correspondent: Option<CorrespondentId>,
99
100    #[serde(skip_serializing_if = "Option::is_none")]
101    document_type: Option<DocumentTypeId>,
102}
103
104impl std::fmt::Display for DocumentId {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        write!(f, "{}", self.0)
107    }
108}
109
110impl Document {
111    pub(crate) fn new(
112        data: DocumentData,
113        client: Arc<PaperlessClient>,
114        content_is_truncated: bool,
115    ) -> Self {
116        Self {
117            data,
118            client,
119            content_is_truncated,
120            changed_values: BitFlags::default(),
121        }
122    }
123
124    /// Get the id of the document
125    #[inline]
126    #[must_use]
127    pub fn id(&self) -> DocumentId {
128        self.data.id
129    }
130
131    /// Get the title of the document.
132    #[inline]
133    #[must_use]
134    pub fn title(&self) -> &str {
135        &self.data.title
136    }
137
138    /// Get the original file name of the document.
139    #[inline]
140    #[must_use]
141    pub fn original_file_name(&self) -> &str {
142        &self.data.original_file_name
143    }
144
145    /// Get the correspondent id of the document.
146    #[inline]
147    #[must_use]
148    pub fn correspondent(&self) -> Option<CorrespondentId> {
149        self.data.correspondent
150    }
151
152    /// Get the document type id of the document.
153    #[inline]
154    #[must_use]
155    pub fn document_type(&self) -> Option<DocumentTypeId> {
156        self.data.document_type
157    }
158
159    /// Get the number of pages in the document.
160    #[inline]
161    #[must_use]
162    pub fn page_count(&self) -> u32 {
163        self.data.page_count
164    }
165
166    /// Get all tag-ids for the document.
167    #[inline]
168    #[must_use]
169    pub fn tags(&self) -> &[TagId] {
170        &self.data.tags
171    }
172
173    /// Get all custom fields for the document.
174    #[inline]
175    #[must_use]
176    pub fn custom_fields(&self) -> &[DocumentCustomField] {
177        &self.data.custom_fields
178    }
179
180    /// Get the content of the document.
181    #[inline]
182    #[must_use]
183    pub fn content(&self) -> Content<'_> {
184        if self.content_is_truncated {
185            Content::Truncated(&self.data.content)
186        } else {
187            Content::Full(&self.data.content)
188        }
189    }
190
191    /// Add a tag to the document.
192    pub fn add_tag(&mut self, tag_id: TagId) {
193        if !self.data.tags.contains(&tag_id) {
194            self.data.tags.push(tag_id);
195            self.changed_values |= ChangedAttributes::Tags;
196        }
197    }
198
199    pub fn remove_tag(&mut self, tag_id: TagId) {
200        if let Some(index) = self.data.tags.iter().position(|id| *id == tag_id) {
201            self.data.tags.remove(index);
202            self.changed_values |= ChangedAttributes::Tags;
203        }
204    }
205
206    /// Set the title of the document.
207    pub fn set_title(&mut self, title: &str) {
208        self.data.title = title.to_string();
209        self.changed_values |= ChangedAttributes::Title;
210    }
211
212    /// Set the content of the document.
213    pub fn set_content(&mut self, content: &str) {
214        self.data.content = content.to_string();
215        self.content_is_truncated = false;
216        self.changed_values |= ChangedAttributes::Content;
217    }
218
219    /// Set a custom field for the document.
220    pub fn set_custom_field(&mut self, field: CustomFieldId, value: &str) {
221        for custom_field in &mut self.data.custom_fields {
222            if custom_field.field == field {
223                custom_field.value = value.to_string();
224                self.changed_values |= ChangedAttributes::CustomFields;
225                return;
226            }
227        }
228
229        self.data.custom_fields.push(DocumentCustomField {
230            field,
231            value: value.to_string(),
232        });
233        self.changed_values |= ChangedAttributes::CustomFields;
234    }
235
236    /// Remove a custom field from the document.
237    pub fn remove_custom_field(&mut self, field: CustomFieldId) {
238        if let Some(index) = self
239            .data
240            .custom_fields
241            .iter()
242            .position(|custom_field| custom_field.field == field)
243        {
244            self.data.custom_fields.remove(index);
245            self.changed_values |= ChangedAttributes::CustomFields;
246        }
247    }
248
249    /// Returns `true` if the document has unsaved changes.
250    #[inline]
251    #[must_use]
252    pub fn is_dirty(&self) -> bool {
253        !self.changed_values.is_empty()
254    }
255
256    /// Refresh the document from the server.
257    ///
258    /// This will discard any local changes and replace them with the server's state.
259    pub async fn reload(&mut self) -> Result<()> {
260        let document_data = self
261            .client
262            .as_ref()
263            .get_document_data_by_id(self.data.id)
264            .await?;
265
266        self.data = document_data;
267
268        self.changed_values = BitFlags::empty();
269        self.content_is_truncated = false;
270        Ok(())
271    }
272
273    /// Update the document on the server.
274    ///
275    /// This applies the currently tracked local changes to the remote Paperless document.
276    pub async fn patch(&mut self) -> Result<()> {
277        if !self.is_dirty() {
278            return Ok(());
279        }
280
281        let patch = PatchRequest {
282            title: self
283                .changed_values
284                .contains(ChangedAttributes::Title)
285                .then_some(self.data.title.clone()),
286
287            content: self
288                .changed_values
289                .contains(ChangedAttributes::Content)
290                .then_some(self.data.content.clone()),
291
292            tags: self
293                .changed_values
294                .contains(ChangedAttributes::Tags)
295                .then_some(self.data.tags.clone()),
296
297            custom_fields: self
298                .changed_values
299                .contains(ChangedAttributes::CustomFields)
300                .then_some(
301                    self.data
302                        .custom_fields
303                        .iter()
304                        .map(|field| DocumentCustomField {
305                            field: field.field,
306                            value: field.value.clone(),
307                        })
308                        .collect(),
309                ),
310            correspondent: self
311                .changed_values
312                .contains(ChangedAttributes::Correspondent)
313                .then_some(self.data.correspondent)
314                .flatten(),
315
316            document_type: self
317                .changed_values
318                .contains(ChangedAttributes::DocumentType)
319                .then_some(self.data.document_type)
320                .flatten(),
321        };
322
323        self.client
324            .request(
325                Method::PATCH,
326                &format!("/api/documents/{}/", self.data.id),
327                Some(&serde_json::to_value(patch).expect("Patch request")),
328            )
329            .await?;
330
331        self.changed_values = BitFlags::empty();
332        Ok(())
333    }
334
335    /// Get the full content of the document, replacing any truncated content.
336    pub async fn get_full_content(&mut self) -> Result<()> {
337        if !self.content_is_truncated {
338            return Ok(());
339        }
340
341        let doc = self.client.get_document_data_by_id(self.data.id).await?;
342        self.data.content = doc.content;
343        self.content_is_truncated = false;
344        Ok(())
345    }
346
347    /// Download the document to a file.
348    pub async fn download_to_file(&self, path: &Path) -> Result<()> {
349        let resp = self
350            .client
351            .request(
352                Method::GET,
353                &format!("/api/documents/{}/download/", self.data.id),
354                None,
355            )
356            .await?;
357
358        if !resp.status().is_success() {
359            return Err(Error::Other(format!(
360                "Failed to download document: {}",
361                resp.status()
362            )));
363        }
364
365        let mut stream = StreamReader::new(
366            resp.bytes_stream()
367                .map_err(|e| io::Error::other(format!("Failed to read response body: {e}"))),
368        );
369
370        let mut file = tokio::fs::File::create(path)
371            .await
372            .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
373
374        tokio::io::copy(&mut stream, &mut file)
375            .await
376            .map_err(|e| Error::Other(format!("Failed to write file: {e}")))?;
377
378        Ok(())
379    }
380
381    /// Download the document to a buffer.
382    pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
383        let resp = self
384            .client
385            .request(
386                Method::GET,
387                &format!("/api/documents/{}/download/", self.data.id),
388                None,
389            )
390            .await?;
391
392        if resp.status().is_success() {
393            let bytes = resp
394                .bytes()
395                .await
396                .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
397            Ok(bytes.to_vec())
398        } else {
399            Err(Error::Other(format!(
400                "Failed to download document: {}",
401                resp.status()
402            )))
403        }
404    }
405}
406
407impl Display for Content<'_> {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        match self {
410            Content::Full(text) => write!(f, "{text}"),
411            Content::Truncated(text) => write!(f, "{text}..."),
412        }
413    }
414}
415
416impl AsRef<str> for Content<'_> {
417    fn as_ref(&self) -> &str {
418        match self {
419            Content::Full(text) | Content::Truncated(text) => text,
420        }
421    }
422}