1use std::{fmt::Display, sync::Arc, time::Duration};
12
13use chrono::{DateTime, NaiveDate, Utc};
14use derive_more::Display;
15use enumflags2::{BitFlags, bitflags};
16use futures_util::TryStreamExt;
17use reqwest::Method;
18use serde::{Deserialize, Serialize};
19use tokio::io::AsyncWriteExt;
20
21use paperless_api_macros::UpdateDto;
22
23use crate::{
24 Error, Result,
25 client::PaperlessClient,
26 id::{
27 CorrespondentId, CustomFieldId, DocumentId, DocumentTypeId, StoragePathId, TagId, UserId,
28 },
29 metadata::{custom_field::DocumentCustomField, permission::ItemPermissions},
30 note::Note,
31 share_link::{CreateShareLink, ShareLink, ShareLinkFileVersion},
32};
33
34#[derive(Debug, Clone)]
46pub struct Document {
47 data: DocumentData,
48
49 client: Arc<PaperlessClient>,
50 content_is_truncated: bool,
51 changed_values: BitFlags<ChangedAttributes>,
52}
53
54#[derive(Debug, Clone, Deserialize, UpdateDto)]
55#[api_info(id = DocumentId)]
56pub(crate) struct DocumentData {
57 #[dto(skip)]
58 id: DocumentId,
59
60 archive_serial_number: Option<ArchiveSerialNumber>,
61
62 #[dto(skip)]
63 original_file_name: String,
64
65 #[dto(skip)]
66 added: DateTime<Utc>,
67
68 created: Option<NaiveDate>,
69
70 #[dto(skip)]
71 modified: DateTime<Utc>,
72
73 #[dto(skip)]
74 page_count: Option<u32>,
75
76 title: String,
77 content: String,
78 tags: Vec<TagId>,
79 owner: Option<UserId>,
80 correspondent: Option<CorrespondentId>,
81 custom_fields: Vec<DocumentCustomField>,
82 document_type: Option<DocumentTypeId>,
83 storage_path: Option<StoragePathId>,
84
85 #[dto(skip)]
86 notes: Vec<Note>,
87
88 #[serde(flatten)]
89 #[dto(skip)]
90 permissions: ItemPermissions,
91
92 #[dto(skip)]
93 mime_type: Option<String>,
94}
95
96#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
97#[repr(transparent)]
98pub struct ArchiveSerialNumber(pub u32);
99
100#[bitflags]
101#[repr(u16)]
102#[derive(Copy, Clone, Debug, PartialEq)]
103enum ChangedAttributes {
104 ArchiveSerialNumber,
105 Title,
106 Content,
107 Tags,
108 CustomFields,
109 Correspondent,
110 DocumentType,
111 Created,
112 Owner,
113 StoragePath,
114
115 Deleted,
116}
117
118#[derive(Debug, Clone)]
120pub enum Content<'a> {
121 Full(&'a str),
123
124 Truncated(&'a str),
126}
127
128impl Document {
129 pub(crate) fn new(
130 data: DocumentData,
131 client: Arc<PaperlessClient>,
132 content_is_truncated: bool,
133 ) -> Self {
134 Self {
135 data,
136 client,
137 content_is_truncated,
138 changed_values: BitFlags::default(),
139 }
140 }
141
142 #[inline]
144 #[must_use]
145 pub fn id(&self) -> DocumentId {
146 self.data.id
147 }
148
149 #[inline]
151 #[must_use]
152 pub fn archive_serial_number(&self) -> Option<ArchiveSerialNumber> {
153 self.data.archive_serial_number
154 }
155
156 #[inline]
158 #[must_use]
159 pub fn added(&self) -> &DateTime<Utc> {
160 &self.data.added
161 }
162
163 #[inline]
165 #[must_use]
166 pub fn created(&self) -> Option<&NaiveDate> {
167 self.data.created.as_ref()
168 }
169
170 #[inline]
172 #[must_use]
173 pub fn modified(&self) -> &DateTime<Utc> {
174 &self.data.modified
175 }
176
177 #[inline]
179 #[must_use]
180 pub fn title(&self) -> &str {
181 &self.data.title
182 }
183
184 #[inline]
186 #[must_use]
187 pub fn original_file_name(&self) -> &str {
188 &self.data.original_file_name
189 }
190
191 #[inline]
193 #[must_use]
194 pub fn mime_type(&self) -> Option<&str> {
195 self.data.mime_type.as_deref()
196 }
197
198 #[inline]
200 #[must_use]
201 pub fn correspondent(&self) -> Option<CorrespondentId> {
202 self.data.correspondent
203 }
204
205 #[inline]
207 #[must_use]
208 pub fn owner(&self) -> Option<UserId> {
209 self.data.owner
210 }
211
212 #[inline]
214 #[must_use]
215 pub fn document_type(&self) -> Option<DocumentTypeId> {
216 self.data.document_type
217 }
218
219 #[inline]
221 #[must_use]
222 pub fn page_count(&self) -> Option<u32> {
223 self.data.page_count
224 }
225
226 #[inline]
228 #[must_use]
229 pub fn tags(&self) -> &[TagId] {
230 &self.data.tags
231 }
232
233 #[inline]
235 #[must_use]
236 pub fn custom_fields(&self) -> &[DocumentCustomField] {
237 &self.data.custom_fields
238 }
239
240 #[inline]
242 #[must_use]
243 pub fn content(&self) -> Content<'_> {
244 if self.content_is_truncated {
245 Content::Truncated(&self.data.content)
246 } else {
247 Content::Full(&self.data.content)
248 }
249 }
250
251 #[inline]
253 #[must_use]
254 pub fn storage_path(&self) -> Option<StoragePathId> {
255 self.data.storage_path
256 }
257
258 #[inline]
260 #[must_use]
261 pub fn notes(&self) -> &[Note] {
262 &self.data.notes
263 }
264
265 #[inline]
267 #[must_use]
268 pub fn permissions(&self) -> &ItemPermissions {
269 &self.data.permissions
270 }
271
272 #[inline]
274 pub fn set_archive_serial_number(
275 &mut self,
276 archive_serial_number: Option<ArchiveSerialNumber>,
277 ) {
278 self.data.archive_serial_number = archive_serial_number;
279 self.changed_values |= ChangedAttributes::ArchiveSerialNumber;
280 }
281
282 pub fn add_tag(&mut self, tag_id: TagId) {
284 if !self.data.tags.contains(&tag_id) {
285 self.data.tags.push(tag_id);
286 self.changed_values |= ChangedAttributes::Tags;
287 }
288 }
289
290 pub fn remove_tag(&mut self, tag_id: TagId) {
292 if let Some(index) = self.data.tags.iter().position(|id| *id == tag_id) {
293 self.data.tags.remove(index);
294 self.changed_values |= ChangedAttributes::Tags;
295 }
296 }
297
298 pub fn set_title(&mut self, title: impl Into<String>) {
300 self.data.title = title.into();
301 self.changed_values |= ChangedAttributes::Title;
302 }
303
304 pub fn set_content(&mut self, content: impl Into<String>) {
306 self.data.content = content.into();
307 self.content_is_truncated = false;
308 self.changed_values |= ChangedAttributes::Content;
309 }
310
311 pub fn set_custom_field(&mut self, field: CustomFieldId, value: impl Into<String>) {
313 for custom_field in &mut self.data.custom_fields {
314 if custom_field.field == field {
315 custom_field.value = value.into();
316 self.changed_values |= ChangedAttributes::CustomFields;
317 return;
318 }
319 }
320
321 self.data.custom_fields.push(DocumentCustomField {
322 field,
323 value: value.into(),
324 });
325 self.changed_values |= ChangedAttributes::CustomFields;
326 }
327
328 pub fn remove_custom_field(&mut self, field: CustomFieldId) {
330 if let Some(index) = self
331 .data
332 .custom_fields
333 .iter()
334 .position(|custom_field| custom_field.field == field)
335 {
336 self.data.custom_fields.remove(index);
337 self.changed_values |= ChangedAttributes::CustomFields;
338 }
339 }
340
341 pub fn set_created(&mut self, created: NaiveDate) {
343 self.data.created = Some(created);
344 self.changed_values |= ChangedAttributes::Created;
345 }
346
347 pub fn set_owner(&mut self, owner: UserId) {
349 self.data.owner = Some(owner);
350 self.changed_values |= ChangedAttributes::Owner;
351 }
352
353 pub fn set_correspondent(&mut self, correspondent: CorrespondentId) {
355 self.data.correspondent = Some(correspondent);
356 self.changed_values |= ChangedAttributes::Correspondent;
357 }
358
359 pub fn set_document_type(&mut self, document_type: DocumentTypeId) {
361 self.data.document_type = Some(document_type);
362 self.changed_values |= ChangedAttributes::DocumentType;
363 }
364
365 pub fn set_storage_path(&mut self, storage_path: StoragePathId) {
367 self.data.storage_path = Some(storage_path);
368 self.changed_values |= ChangedAttributes::StoragePath;
369 }
370
371 #[inline]
373 #[must_use]
374 pub fn is_dirty(&self) -> bool {
375 !self.changed_values.is_empty() && !self.changed_values.contains(ChangedAttributes::Deleted)
376 }
377
378 #[inline]
380 #[must_use]
381 pub fn is_deleted(&self) -> bool {
382 self.changed_values.contains(ChangedAttributes::Deleted)
383 }
384
385 fn fail_if_deleted(&self) -> Result<()> {
386 if self.is_deleted() {
387 Err(Error::AlreadyDeleted)
388 } else {
389 Ok(())
390 }
391 }
392
393 pub async fn refresh(&mut self) -> Result<()> {
397 let document_data = self
398 .client
399 .as_ref()
400 .get_document_data_by_id(self.data.id)
401 .await?;
402
403 self.data = document_data;
404
405 self.changed_values = BitFlags::empty();
406 self.content_is_truncated = false;
407 Ok(())
408 }
409
410 pub async fn thumbnail(&self) -> Result<Vec<u8>> {
414 let resp = self
415 .client
416 .request_no_body(
417 Method::GET,
418 &format!("/api/documents/{}/thumb/", self.data.id),
419 None,
420 )
421 .await?;
422
423 Ok(resp
424 .bytes()
425 .await
426 .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?
427 .to_vec())
428 }
429
430 pub async fn patch(&mut self) -> Result<()> {
434 if !self.is_dirty() {
435 return Ok(());
436 }
437
438 self.fail_if_deleted()?;
439
440 let patch = UpdateDocumentData {
441 title: self
442 .changed_values
443 .contains(ChangedAttributes::Title)
444 .then_some(self.data.title.clone()),
445
446 archive_serial_number: self
447 .changed_values
448 .contains(ChangedAttributes::ArchiveSerialNumber)
449 .then_some(self.data.archive_serial_number),
450
451 content: self
452 .changed_values
453 .contains(ChangedAttributes::Content)
454 .then_some(self.data.content.clone()),
455
456 tags: self
457 .changed_values
458 .contains(ChangedAttributes::Tags)
459 .then_some(self.data.tags.clone()),
460
461 custom_fields: self
462 .changed_values
463 .contains(ChangedAttributes::CustomFields)
464 .then_some(self.data.custom_fields.clone()),
465
466 correspondent: self
467 .changed_values
468 .contains(ChangedAttributes::Correspondent)
469 .then_some(self.data.correspondent),
470
471 document_type: self
472 .changed_values
473 .contains(ChangedAttributes::DocumentType)
474 .then_some(self.data.document_type),
475
476 created: self
477 .changed_values
478 .contains(ChangedAttributes::Created)
479 .then_some(self.data.created),
480
481 owner: self
482 .changed_values
483 .contains(ChangedAttributes::Owner)
484 .then_some(self.data.owner),
485
486 storage_path: self
487 .changed_values
488 .contains(ChangedAttributes::StoragePath)
489 .then_some(self.data.storage_path),
490 };
491
492 self.client
493 .request(
494 Method::PATCH,
495 &format!("/api/documents/{}/", self.data.id),
496 Some(&patch),
497 None,
498 )
499 .await?;
500
501 self.changed_values = BitFlags::empty();
502 Ok(())
503 }
504
505 pub async fn delete(&mut self) -> Result<()> {
507 self.client
508 .request_no_body(
509 Method::DELETE,
510 &format!("/api/documents/{}/", self.data.id),
511 None,
512 )
513 .await?;
514
515 self.changed_values = BitFlags::from(ChangedAttributes::Deleted);
516 Ok(())
517 }
518
519 pub async fn get_full_content(&mut self) -> Result<()> {
521 self.fail_if_deleted()?;
522
523 if !self.content_is_truncated {
524 return Ok(());
525 }
526
527 let doc = self.client.get_document_data_by_id(self.data.id).await?;
528 self.data.content = doc.content;
529 self.content_is_truncated = false;
530 Ok(())
531 }
532
533 pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
535 self.fail_if_deleted()?;
536
537 let resp = self
538 .client
539 .request_no_body(
540 Method::GET,
541 &format!("/api/documents/{}/download/", self.data.id),
542 None,
543 )
544 .await?;
545
546 if resp.status().is_success() {
547 let bytes = resp
548 .bytes()
549 .await
550 .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
551 Ok(bytes.to_vec())
552 } else {
553 Err(Error::Other(format!(
554 "Failed to download document: {}",
555 resp.status()
556 )))
557 }
558 }
559
560 pub async fn download_to_file(&self, path: &std::path::Path) -> Result<()> {
562 self.fail_if_deleted()?;
563
564 let resp = self
565 .client
566 .request_no_body(
567 Method::GET,
568 &format!("/api/documents/{}/download/", self.data.id),
569 None,
570 )
571 .await?;
572
573 if !resp.status().is_success() {
574 return Err(Error::Other(format!(
575 "Failed to download document: {}",
576 resp.status()
577 )));
578 }
579
580 let mut file = tokio::fs::File::create(path)
581 .await
582 .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
583
584 resp.bytes_stream()
585 .map_err(|e| Error::Other(format!("Failed to read document chunk: {e}")))
586 .try_fold(&mut file, |file, chunk| async move {
587 file.write_all(&chunk).await.map_err(|e| {
588 Error::Other(format!("Failed to save document chunk to file: {e}"))
589 })?;
590 Ok(file)
591 })
592 .await?;
593
594 Ok(())
595 }
596
597 pub fn generate_share_link_duration(
599 &self,
600 valid_for: Duration,
601 version: ShareLinkFileVersion,
602 ) -> impl Future<Output = Result<ShareLink>> {
603 let expires = Utc::now() + valid_for;
604 self.generate_share_link_expires(expires, version)
605 }
606
607 pub async fn generate_share_link_expires(
609 &self,
610 expires: DateTime<Utc>,
611 version: ShareLinkFileVersion,
612 ) -> Result<ShareLink> {
613 self.fail_if_deleted()?;
614
615 self.client
616 .create(&CreateShareLink {
617 document: self.id(),
618 expiration: expires,
619 file_version: version,
620 })
621 .await
622 }
623}
624
625impl Display for Content<'_> {
626 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
627 match self {
628 Content::Full(text) => write!(f, "{text}"),
629 Content::Truncated(text) => write!(f, "{text}..."),
630 }
631 }
632}
633
634impl AsRef<str> for Content<'_> {
635 fn as_ref(&self) -> &str {
636 match self {
637 Content::Full(text) | Content::Truncated(text) => text,
638 }
639 }
640}