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, user::UserId,
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: Option<UserId>,
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 owner id of the document.
153    #[inline]
154    #[must_use]
155    pub fn owner(&self) -> Option<UserId> {
156        self.data.owner
157    }
158
159    /// Get the document type id of the document.
160    #[inline]
161    #[must_use]
162    pub fn document_type(&self) -> Option<DocumentTypeId> {
163        self.data.document_type
164    }
165
166    /// Get the number of pages in the document.
167    #[inline]
168    #[must_use]
169    pub fn page_count(&self) -> u32 {
170        self.data.page_count
171    }
172
173    /// Get all tag-ids for the document.
174    #[inline]
175    #[must_use]
176    pub fn tags(&self) -> &[TagId] {
177        &self.data.tags
178    }
179
180    /// Get all custom fields for the document.
181    #[inline]
182    #[must_use]
183    pub fn custom_fields(&self) -> &[DocumentCustomField] {
184        &self.data.custom_fields
185    }
186
187    /// Get the content of the document.
188    #[inline]
189    #[must_use]
190    pub fn content(&self) -> Content<'_> {
191        if self.content_is_truncated {
192            Content::Truncated(&self.data.content)
193        } else {
194            Content::Full(&self.data.content)
195        }
196    }
197
198    /// Add a tag to the document.
199    pub fn add_tag(&mut self, tag_id: TagId) {
200        if !self.data.tags.contains(&tag_id) {
201            self.data.tags.push(tag_id);
202            self.changed_values |= ChangedAttributes::Tags;
203        }
204    }
205
206    pub fn remove_tag(&mut self, tag_id: TagId) {
207        if let Some(index) = self.data.tags.iter().position(|id| *id == tag_id) {
208            self.data.tags.remove(index);
209            self.changed_values |= ChangedAttributes::Tags;
210        }
211    }
212
213    /// Set the title of the document.
214    pub fn set_title(&mut self, title: &str) {
215        self.data.title = title.to_string();
216        self.changed_values |= ChangedAttributes::Title;
217    }
218
219    /// Set the content of the document.
220    pub fn set_content(&mut self, content: &str) {
221        self.data.content = content.to_string();
222        self.content_is_truncated = false;
223        self.changed_values |= ChangedAttributes::Content;
224    }
225
226    /// Set a custom field for the document.
227    pub fn set_custom_field(&mut self, field: CustomFieldId, value: &str) {
228        for custom_field in &mut self.data.custom_fields {
229            if custom_field.field == field {
230                custom_field.value = value.to_string();
231                self.changed_values |= ChangedAttributes::CustomFields;
232                return;
233            }
234        }
235
236        self.data.custom_fields.push(DocumentCustomField {
237            field,
238            value: value.to_string(),
239        });
240        self.changed_values |= ChangedAttributes::CustomFields;
241    }
242
243    /// Remove a custom field from the document.
244    pub fn remove_custom_field(&mut self, field: CustomFieldId) {
245        if let Some(index) = self
246            .data
247            .custom_fields
248            .iter()
249            .position(|custom_field| custom_field.field == field)
250        {
251            self.data.custom_fields.remove(index);
252            self.changed_values |= ChangedAttributes::CustomFields;
253        }
254    }
255
256    /// Returns `true` if the document has unsaved changes.
257    #[inline]
258    #[must_use]
259    pub fn is_dirty(&self) -> bool {
260        !self.changed_values.is_empty()
261    }
262
263    /// Refresh the document from the server.
264    ///
265    /// This will discard any local changes and replace them with the server's state.
266    pub async fn reload(&mut self) -> Result<()> {
267        let document_data = self
268            .client
269            .as_ref()
270            .get_document_data_by_id(self.data.id)
271            .await?;
272
273        self.data = document_data;
274
275        self.changed_values = BitFlags::empty();
276        self.content_is_truncated = false;
277        Ok(())
278    }
279
280    /// Update the document on the server.
281    ///
282    /// This applies the currently tracked local changes to the remote Paperless document.
283    pub async fn patch(&mut self) -> Result<()> {
284        if !self.is_dirty() {
285            return Ok(());
286        }
287
288        let patch = PatchRequest {
289            title: self
290                .changed_values
291                .contains(ChangedAttributes::Title)
292                .then_some(self.data.title.clone()),
293
294            content: self
295                .changed_values
296                .contains(ChangedAttributes::Content)
297                .then_some(self.data.content.clone()),
298
299            tags: self
300                .changed_values
301                .contains(ChangedAttributes::Tags)
302                .then_some(self.data.tags.clone()),
303
304            custom_fields: self
305                .changed_values
306                .contains(ChangedAttributes::CustomFields)
307                .then_some(
308                    self.data
309                        .custom_fields
310                        .iter()
311                        .map(|field| DocumentCustomField {
312                            field: field.field,
313                            value: field.value.clone(),
314                        })
315                        .collect(),
316                ),
317            correspondent: self
318                .changed_values
319                .contains(ChangedAttributes::Correspondent)
320                .then_some(self.data.correspondent)
321                .flatten(),
322
323            document_type: self
324                .changed_values
325                .contains(ChangedAttributes::DocumentType)
326                .then_some(self.data.document_type)
327                .flatten(),
328        };
329
330        self.client
331            .request(
332                Method::PATCH,
333                &format!("/api/documents/{}/", self.data.id),
334                Some(&serde_json::to_value(patch).expect("Patch request")),
335            )
336            .await?;
337
338        self.changed_values = BitFlags::empty();
339        Ok(())
340    }
341
342    /// Get the full content of the document, replacing any truncated content.
343    pub async fn get_full_content(&mut self) -> Result<()> {
344        if !self.content_is_truncated {
345            return Ok(());
346        }
347
348        let doc = self.client.get_document_data_by_id(self.data.id).await?;
349        self.data.content = doc.content;
350        self.content_is_truncated = false;
351        Ok(())
352    }
353
354    /// Download the document to a file.
355    pub async fn download_to_file(&self, path: &Path) -> Result<()> {
356        let resp = self
357            .client
358            .request(
359                Method::GET,
360                &format!("/api/documents/{}/download/", self.data.id),
361                None,
362            )
363            .await?;
364
365        if !resp.status().is_success() {
366            return Err(Error::Other(format!(
367                "Failed to download document: {}",
368                resp.status()
369            )));
370        }
371
372        let mut stream = StreamReader::new(
373            resp.bytes_stream()
374                .map_err(|e| io::Error::other(format!("Failed to read response body: {e}"))),
375        );
376
377        let mut file = tokio::fs::File::create(path)
378            .await
379            .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
380
381        tokio::io::copy(&mut stream, &mut file)
382            .await
383            .map_err(|e| Error::Other(format!("Failed to write file: {e}")))?;
384
385        Ok(())
386    }
387
388    /// Download the document to a buffer.
389    pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
390        let resp = self
391            .client
392            .request(
393                Method::GET,
394                &format!("/api/documents/{}/download/", self.data.id),
395                None,
396            )
397            .await?;
398
399        if resp.status().is_success() {
400            let bytes = resp
401                .bytes()
402                .await
403                .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
404            Ok(bytes.to_vec())
405        } else {
406            Err(Error::Other(format!(
407                "Failed to download document: {}",
408                resp.status()
409            )))
410        }
411    }
412}
413
414impl Display for Content<'_> {
415    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416        match self {
417            Content::Full(text) => write!(f, "{text}"),
418            Content::Truncated(text) => write!(f, "{text}..."),
419        }
420    }
421}
422
423impl AsRef<str> for Content<'_> {
424    fn as_ref(&self) -> &str {
425        match self {
426            Content::Full(text) | Content::Truncated(text) => text,
427        }
428    }
429}