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#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct Document {
21 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 pub original_file_name: String,
32
33 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#[derive(Debug, Clone)]
62pub enum Content<'a> {
63 Full(&'a str),
65
66 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 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 #[inline]
108 #[must_use]
109 pub fn tags(&self) -> &[TagId] {
110 &self.tags
111 }
112
113 pub fn set_title(&mut self, title: &str) {
115 self.title = title.to_string();
116 self.changed_values |= ChangedAttributes::Title;
117 }
118
119 #[inline]
121 #[must_use]
122 pub fn title(&self) -> &str {
123 &self.title
124 }
125
126 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 #[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 #[inline]
146 #[must_use]
147 pub fn custom_fields(&self) -> &[DocumentCustomField] {
148 &self.custom_fields
149 }
150
151 #[inline]
153 #[must_use]
154 pub fn is_dirty(&self) -> bool {
155 !self.changed_values.is_empty()
156 }
157
158 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 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 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 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 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}