Skip to main content

paperless_api/
document.rs

1use std::{fmt::Display, io, path::Path, sync::Arc};
2
3use enumflags2::{BitFlags, bitflags};
4use futures_util::TryStreamExt;
5use reqwest::Method;
6use serde::{Deserialize, Serialize};
7use tokio_util::io::StreamReader;
8
9use crate::{
10    DocumentCustomField, Error, Result, client::PaperlessClient, correspondent::CorrespondentId,
11    custom_field::CustomFieldId, document_type::DocumentTypeId, tag::TagId,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
15#[repr(transparent)]
16pub struct DocumentId(pub i32);
17
18/// Represents a document
19#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct Document {
21    /// Unique identifier of the document.
22    pub id: DocumentId,
23    title: String,
24    content: String,
25    tags: Vec<TagId>,
26    owner: i32,
27    correspondent: Option<CorrespondentId>,
28    document_type: Option<DocumentTypeId>,
29
30    /// Original file name of the document.
31    pub original_file_name: String,
32
33    /// Number of pages in the document.
34    pub page_count: u32,
35
36    custom_fields: Vec<DocumentCustomField>,
37
38    #[serde(skip)]
39    pub(crate) client: Option<Arc<PaperlessClient>>,
40
41    #[serde(skip)]
42    pub(crate) content_is_truncated: bool,
43
44    #[serde(skip)]
45    changed_values: BitFlags<ChangedAttributes>,
46}
47
48#[bitflags]
49#[repr(u8)]
50#[derive(Copy, Clone, Debug, PartialEq)]
51enum ChangedAttributes {
52    Title,
53    Content,
54    Tags,
55    CustomFields,
56    Correspondent,
57    DocumentType,
58}
59
60/// The content (OCR) of a document, either full or truncated.
61#[derive(Debug, Clone)]
62pub enum Content<'a> {
63    /// Full content of the document.
64    Full(&'a str),
65
66    /// Truncated content of the document.
67    Truncated(&'a str),
68}
69
70#[derive(Debug, Serialize)]
71struct PatchRequest {
72    #[serde(skip_serializing_if = "Option::is_none")]
73    title: Option<String>,
74
75    #[serde(skip_serializing_if = "Option::is_none")]
76    content: Option<String>,
77
78    #[serde(skip_serializing_if = "Option::is_none")]
79    tags: Option<Vec<TagId>>,
80
81    #[serde(skip_serializing_if = "Option::is_none")]
82    custom_fields: Option<Vec<DocumentCustomField>>,
83
84    #[serde(skip_serializing_if = "Option::is_none")]
85    correspondent: Option<CorrespondentId>,
86
87    #[serde(skip_serializing_if = "Option::is_none")]
88    document_type: Option<DocumentTypeId>,
89}
90
91impl std::fmt::Display for DocumentId {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(f, "{}", self.0)
94    }
95}
96
97impl Document {
98    /// Add a tag to the document.
99    pub fn add_tag(&mut self, tag_id: TagId) {
100        if !self.tags.contains(&tag_id) {
101            self.tags.push(tag_id);
102            self.changed_values |= ChangedAttributes::Tags;
103        }
104    }
105
106    /// Get all tag-ids for the document.
107    #[inline]
108    #[must_use]
109    pub fn tags(&self) -> &[TagId] {
110        &self.tags
111    }
112
113    /// Set the title of the document.
114    pub fn set_title(&mut self, title: &str) {
115        self.title = title.to_string();
116        self.changed_values |= ChangedAttributes::Title;
117    }
118
119    /// Get the title of the document.
120    #[inline]
121    #[must_use]
122    pub fn title(&self) -> &str {
123        &self.title
124    }
125
126    /// Set the content of the document.
127    pub fn set_content(&mut self, content: &str) {
128        self.content = content.to_string();
129        self.content_is_truncated = false;
130        self.changed_values |= ChangedAttributes::Content;
131    }
132
133    /// Get the content of the document.
134    #[inline]
135    #[must_use]
136    pub fn content(&self) -> Content<'_> {
137        if self.content_is_truncated {
138            Content::Truncated(&self.content)
139        } else {
140            Content::Full(&self.content)
141        }
142    }
143
144    /// Get all custom fields for the document.
145    #[inline]
146    #[must_use]
147    pub fn custom_fields(&self) -> &[DocumentCustomField] {
148        &self.custom_fields
149    }
150
151    /// Returns `true` if the document has unsaved changes.
152    #[inline]
153    #[must_use]
154    pub fn is_dirty(&self) -> bool {
155        !self.changed_values.is_empty()
156    }
157
158    /// Set a custom field for the document.
159    pub fn set_custom_field(&mut self, field: CustomFieldId, value: &str) {
160        for custom_field in &mut self.custom_fields {
161            if custom_field.field == field {
162                custom_field.value = value.to_string();
163                self.changed_values |= ChangedAttributes::CustomFields;
164                return;
165            }
166        }
167
168        self.custom_fields.push(DocumentCustomField {
169            field: field,
170            value: value.to_string(),
171        });
172        self.changed_values |= ChangedAttributes::CustomFields;
173    }
174
175    /// Update the document on the server.
176    pub async fn update(&mut self) -> Result<()> {
177        if !self.is_dirty() {
178            return Ok(());
179        }
180
181        let patch = PatchRequest {
182            title: self
183                .changed_values
184                .contains(ChangedAttributes::Title)
185                .then_some(self.title.clone()),
186
187            content: self
188                .changed_values
189                .contains(ChangedAttributes::Content)
190                .then_some(self.content.clone()),
191
192            tags: self
193                .changed_values
194                .contains(ChangedAttributes::Tags)
195                .then_some(self.tags.clone()),
196
197            custom_fields: self
198                .changed_values
199                .contains(ChangedAttributes::CustomFields)
200                .then_some(
201                    self.custom_fields
202                        .iter()
203                        .map(|field| DocumentCustomField {
204                            field: field.field,
205                            value: field.value.clone(),
206                        })
207                        .collect(),
208                ),
209            correspondent: self
210                .changed_values
211                .contains(ChangedAttributes::Correspondent)
212                .then_some(self.correspondent)
213                .flatten(),
214
215            document_type: self
216                .changed_values
217                .contains(ChangedAttributes::DocumentType)
218                .then_some(self.document_type)
219                .flatten(),
220        };
221
222        self.client
223            .as_ref()
224            .unwrap()
225            .request(
226                Method::PATCH,
227                &format!("/api/documents/{}/", self.id),
228                Some(&serde_json::to_value(patch).expect("Patch request")),
229            )
230            .await?;
231
232        self.changed_values = BitFlags::empty();
233        Ok(())
234    }
235
236    /// Get the full content of the document, replacing any truncated content.
237    pub async fn get_full_content(&mut self) -> Result<()> {
238        if !self.content_is_truncated {
239            return Ok(());
240        }
241
242        let doc = self
243            .client
244            .as_ref()
245            .unwrap()
246            .get_document_by_id(self.id)
247            .await?;
248
249        self.content = doc.content;
250        self.content_is_truncated = false;
251        Ok(())
252    }
253
254    /// Download the document to a file.
255    pub async fn download_to_file(&self, path: &Path) -> Result<()> {
256        let resp = self
257            .client
258            .as_ref()
259            .unwrap()
260            .request(
261                Method::GET,
262                &format!("/api/documents/{}/download/", self.id),
263                None,
264            )
265            .await?;
266
267        if !resp.status().is_success() {
268            return Err(Error::Other(format!(
269                "Failed to download document: {}",
270                resp.status()
271            )));
272        }
273
274        let mut stream = StreamReader::new(
275            resp.bytes_stream()
276                .map_err(|e| io::Error::other(format!("Failed to read response body: {e}"))),
277        );
278
279        let mut file = tokio::fs::File::create(path)
280            .await
281            .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
282
283        tokio::io::copy(&mut stream, &mut file)
284            .await
285            .map_err(|e| Error::Other(format!("Failed to write file: {e}")))?;
286
287        Ok(())
288    }
289
290    /// Download the document to a buffer.
291    pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
292        let resp = self
293            .client
294            .as_ref()
295            .unwrap()
296            .request(
297                Method::GET,
298                &format!("/api/documents/{}/download/", self.id),
299                None,
300            )
301            .await?;
302
303        if resp.status().is_success() {
304            let bytes = resp
305                .bytes()
306                .await
307                .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
308            Ok(bytes.to_vec())
309        } else {
310            Err(Error::Other(format!(
311                "Failed to download document: {}",
312                resp.status()
313            )))
314        }
315    }
316}
317
318impl Display for Content<'_> {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        match self {
321            Content::Full(text) => write!(f, "{text}"),
322            Content::Truncated(text) => write!(f, "{text}..."),
323        }
324    }
325}
326
327impl AsRef<str> for Content<'_> {
328    fn as_ref(&self) -> &str {
329        match self {
330            Content::Full(text) | Content::Truncated(text) => text,
331        }
332    }
333}