1use 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#[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#[derive(Debug, Clone)]
75pub enum Content<'a> {
76 Full(&'a str),
78
79 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 #[inline]
126 #[must_use]
127 pub fn id(&self) -> DocumentId {
128 self.data.id
129 }
130
131 #[inline]
133 #[must_use]
134 pub fn title(&self) -> &str {
135 &self.data.title
136 }
137
138 #[inline]
140 #[must_use]
141 pub fn original_file_name(&self) -> &str {
142 &self.data.original_file_name
143 }
144
145 #[inline]
147 #[must_use]
148 pub fn correspondent(&self) -> Option<CorrespondentId> {
149 self.data.correspondent
150 }
151
152 #[inline]
154 #[must_use]
155 pub fn document_type(&self) -> Option<DocumentTypeId> {
156 self.data.document_type
157 }
158
159 #[inline]
161 #[must_use]
162 pub fn page_count(&self) -> u32 {
163 self.data.page_count
164 }
165
166 #[inline]
168 #[must_use]
169 pub fn tags(&self) -> &[TagId] {
170 &self.data.tags
171 }
172
173 #[inline]
175 #[must_use]
176 pub fn custom_fields(&self) -> &[DocumentCustomField] {
177 &self.data.custom_fields
178 }
179
180 #[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 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 pub fn set_title(&mut self, title: &str) {
208 self.data.title = title.to_string();
209 self.changed_values |= ChangedAttributes::Title;
210 }
211
212 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 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 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 #[inline]
251 #[must_use]
252 pub fn is_dirty(&self) -> bool {
253 !self.changed_values.is_empty()
254 }
255
256 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 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 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 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 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}