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)]
55pub(crate) struct DocumentData {
56 #[dto(skip)]
57 id: DocumentId,
58
59 archive_serial_number: Option<ArchiveSerialNumber>,
60
61 #[dto(skip)]
62 original_file_name: String,
63
64 #[dto(skip)]
65 added: DateTime<Utc>,
66
67 created: Option<NaiveDate>,
68
69 #[dto(skip)]
70 modified: DateTime<Utc>,
71
72 #[dto(skip)]
73 page_count: Option<u32>,
74
75 title: String,
76 content: String,
77 tags: Vec<TagId>,
78 owner: Option<UserId>,
79 correspondent: Option<CorrespondentId>,
80 custom_fields: Vec<DocumentCustomField>,
81 document_type: Option<DocumentTypeId>,
82 storage_path: Option<StoragePathId>,
83
84 #[dto(skip)]
85 notes: Vec<Note>,
86
87 #[serde(flatten)]
88 #[dto(skip)]
89 permissions: ItemPermissions,
90
91 #[dto(skip)]
92 mime_type: Option<String>,
93}
94
95#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
96#[repr(transparent)]
97pub struct ArchiveSerialNumber(pub u32);
98
99#[bitflags]
100#[repr(u16)]
101#[derive(Copy, Clone, Debug, PartialEq)]
102enum ChangedAttributes {
103 ArchiveSerialNumber,
104 Title,
105 Content,
106 Tags,
107 CustomFields,
108 Correspondent,
109 DocumentType,
110 Created,
111 Owner,
112 StoragePath,
113
114 Deleted,
115}
116
117#[derive(Debug, Clone)]
119pub enum Content<'a> {
120 Full(&'a str),
122
123 Truncated(&'a str),
125}
126
127impl Document {
128 pub(crate) fn new(
129 data: DocumentData,
130 client: Arc<PaperlessClient>,
131 content_is_truncated: bool,
132 ) -> Self {
133 Self {
134 data,
135 client,
136 content_is_truncated,
137 changed_values: BitFlags::default(),
138 }
139 }
140
141 #[inline]
143 #[must_use]
144 pub fn id(&self) -> DocumentId {
145 self.data.id
146 }
147
148 #[inline]
150 #[must_use]
151 pub fn archive_serial_number(&self) -> Option<ArchiveSerialNumber> {
152 self.data.archive_serial_number
153 }
154
155 #[inline]
157 #[must_use]
158 pub fn added(&self) -> &DateTime<Utc> {
159 &self.data.added
160 }
161
162 #[inline]
164 #[must_use]
165 pub fn created(&self) -> Option<&NaiveDate> {
166 self.data.created.as_ref()
167 }
168
169 #[inline]
171 #[must_use]
172 pub fn modified(&self) -> &DateTime<Utc> {
173 &self.data.modified
174 }
175
176 #[inline]
178 #[must_use]
179 pub fn title(&self) -> &str {
180 &self.data.title
181 }
182
183 #[inline]
185 #[must_use]
186 pub fn original_file_name(&self) -> &str {
187 &self.data.original_file_name
188 }
189
190 #[inline]
192 #[must_use]
193 pub fn mime_type(&self) -> Option<&str> {
194 self.data.mime_type.as_deref()
195 }
196
197 #[inline]
199 #[must_use]
200 pub fn correspondent(&self) -> Option<CorrespondentId> {
201 self.data.correspondent
202 }
203
204 #[inline]
206 #[must_use]
207 pub fn owner(&self) -> Option<UserId> {
208 self.data.owner
209 }
210
211 #[inline]
213 #[must_use]
214 pub fn document_type(&self) -> Option<DocumentTypeId> {
215 self.data.document_type
216 }
217
218 #[inline]
220 #[must_use]
221 pub fn page_count(&self) -> Option<u32> {
222 self.data.page_count
223 }
224
225 #[inline]
227 #[must_use]
228 pub fn tags(&self) -> &[TagId] {
229 &self.data.tags
230 }
231
232 #[inline]
234 #[must_use]
235 pub fn custom_fields(&self) -> &[DocumentCustomField] {
236 &self.data.custom_fields
237 }
238
239 #[inline]
241 #[must_use]
242 pub fn content(&self) -> Content<'_> {
243 if self.content_is_truncated {
244 Content::Truncated(&self.data.content)
245 } else {
246 Content::Full(&self.data.content)
247 }
248 }
249
250 #[inline]
252 #[must_use]
253 pub fn storage_path(&self) -> Option<StoragePathId> {
254 self.data.storage_path
255 }
256
257 #[inline]
259 #[must_use]
260 pub fn notes(&self) -> &[Note] {
261 &self.data.notes
262 }
263
264 #[inline]
266 #[must_use]
267 pub fn permissions(&self) -> &ItemPermissions {
268 &self.data.permissions
269 }
270
271 #[inline]
273 pub fn set_archive_serial_number(
274 &mut self,
275 archive_serial_number: Option<ArchiveSerialNumber>,
276 ) {
277 self.data.archive_serial_number = archive_serial_number;
278 self.changed_values |= ChangedAttributes::ArchiveSerialNumber;
279 }
280
281 pub fn add_tag(&mut self, tag_id: TagId) {
283 if !self.data.tags.contains(&tag_id) {
284 self.data.tags.push(tag_id);
285 self.changed_values |= ChangedAttributes::Tags;
286 }
287 }
288
289 pub fn remove_tag(&mut self, tag_id: TagId) {
291 if let Some(index) = self.data.tags.iter().position(|id| *id == tag_id) {
292 self.data.tags.remove(index);
293 self.changed_values |= ChangedAttributes::Tags;
294 }
295 }
296
297 pub fn set_title(&mut self, title: impl Into<String>) {
299 self.data.title = title.into();
300 self.changed_values |= ChangedAttributes::Title;
301 }
302
303 pub fn set_content(&mut self, content: impl Into<String>) {
305 self.data.content = content.into();
306 self.content_is_truncated = false;
307 self.changed_values |= ChangedAttributes::Content;
308 }
309
310 pub fn set_custom_field(&mut self, field: CustomFieldId, value: impl Into<String>) {
312 for custom_field in &mut self.data.custom_fields {
313 if custom_field.field == field {
314 custom_field.value = value.into();
315 self.changed_values |= ChangedAttributes::CustomFields;
316 return;
317 }
318 }
319
320 self.data.custom_fields.push(DocumentCustomField {
321 field,
322 value: value.into(),
323 });
324 self.changed_values |= ChangedAttributes::CustomFields;
325 }
326
327 pub fn remove_custom_field(&mut self, field: CustomFieldId) {
329 if let Some(index) = self
330 .data
331 .custom_fields
332 .iter()
333 .position(|custom_field| custom_field.field == field)
334 {
335 self.data.custom_fields.remove(index);
336 self.changed_values |= ChangedAttributes::CustomFields;
337 }
338 }
339
340 pub fn set_created(&mut self, created: NaiveDate) {
342 self.data.created = Some(created);
343 self.changed_values |= ChangedAttributes::Created;
344 }
345
346 pub fn set_owner(&mut self, owner: UserId) {
348 self.data.owner = Some(owner);
349 self.changed_values |= ChangedAttributes::Owner;
350 }
351
352 pub fn set_correspondent(&mut self, correspondent: CorrespondentId) {
354 self.data.correspondent = Some(correspondent);
355 self.changed_values |= ChangedAttributes::Correspondent;
356 }
357
358 pub fn set_document_type(&mut self, document_type: DocumentTypeId) {
360 self.data.document_type = Some(document_type);
361 self.changed_values |= ChangedAttributes::DocumentType;
362 }
363
364 pub fn set_storage_path(&mut self, storage_path: StoragePathId) {
366 self.data.storage_path = Some(storage_path);
367 self.changed_values |= ChangedAttributes::StoragePath;
368 }
369
370 #[inline]
372 #[must_use]
373 pub fn is_dirty(&self) -> bool {
374 !self.changed_values.is_empty() && !self.changed_values.contains(ChangedAttributes::Deleted)
375 }
376
377 #[inline]
379 #[must_use]
380 pub fn is_deleted(&self) -> bool {
381 self.changed_values.contains(ChangedAttributes::Deleted)
382 }
383
384 fn fail_if_deleted(&self) -> Result<()> {
385 if self.is_deleted() {
386 Err(Error::AlreadyDeleted)
387 } else {
388 Ok(())
389 }
390 }
391
392 pub async fn refresh(&mut self) -> Result<()> {
396 let document_data = self
397 .client
398 .as_ref()
399 .get_document_data_by_id(self.data.id)
400 .await?;
401
402 self.data = document_data;
403
404 self.changed_values = BitFlags::empty();
405 self.content_is_truncated = false;
406 Ok(())
407 }
408
409 pub async fn thumbnail(&self) -> Result<Vec<u8>> {
413 let resp = self
414 .client
415 .request(
416 Method::GET,
417 &format!("/api/documents/{}/thumb/", self.data.id),
418 None,
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(&serde_json::to_value(&patch).map_err(|e| Error::Other(e.to_string()))?),
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(
509 Method::DELETE,
510 &format!("/api/documents/{}/", self.data.id),
511 None,
512 None,
513 )
514 .await?;
515
516 self.changed_values = BitFlags::from(ChangedAttributes::Deleted);
517 Ok(())
518 }
519
520 pub async fn get_full_content(&mut self) -> Result<()> {
522 self.fail_if_deleted()?;
523
524 if !self.content_is_truncated {
525 return Ok(());
526 }
527
528 let doc = self.client.get_document_data_by_id(self.data.id).await?;
529 self.data.content = doc.content;
530 self.content_is_truncated = false;
531 Ok(())
532 }
533
534 pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
536 self.fail_if_deleted()?;
537
538 let resp = self
539 .client
540 .request(
541 Method::GET,
542 &format!("/api/documents/{}/download/", self.data.id),
543 None,
544 None,
545 )
546 .await?;
547
548 if resp.status().is_success() {
549 let bytes = resp
550 .bytes()
551 .await
552 .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
553 Ok(bytes.to_vec())
554 } else {
555 Err(Error::Other(format!(
556 "Failed to download document: {}",
557 resp.status()
558 )))
559 }
560 }
561
562 pub async fn download_to_file(&self, path: &std::path::Path) -> Result<()> {
564 self.fail_if_deleted()?;
565
566 let resp = self
567 .client
568 .request(
569 Method::GET,
570 &format!("/api/documents/{}/download/", self.data.id),
571 None,
572 None,
573 )
574 .await?;
575
576 if !resp.status().is_success() {
577 return Err(Error::Other(format!(
578 "Failed to download document: {}",
579 resp.status()
580 )));
581 }
582
583 let mut file = tokio::fs::File::create(path)
584 .await
585 .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
586
587 resp.bytes_stream()
588 .map_err(|e| Error::Other(format!("Failed to read document chunk: {e}")))
589 .try_fold(&mut file, |file, chunk| async move {
590 file.write_all(&chunk).await.map_err(|e| {
591 Error::Other(format!("Failed to save document chunk to file: {e}"))
592 })?;
593 Ok(file)
594 })
595 .await?;
596
597 Ok(())
598 }
599
600 pub fn generate_share_link_duration(
602 &self,
603 valid_for: Duration,
604 version: ShareLinkFileVersion,
605 ) -> impl Future<Output = Result<ShareLink>> {
606 let expires = Utc::now() + valid_for;
607 self.generate_share_link_expires(expires, version)
608 }
609
610 pub async fn generate_share_link_expires(
612 &self,
613 expires: DateTime<Utc>,
614 version: ShareLinkFileVersion,
615 ) -> Result<ShareLink> {
616 self.fail_if_deleted()?;
617
618 let mut share_link = self
619 .client
620 .request_json::<ShareLink>(
621 Method::POST,
622 "/api/share_links/",
623 Some(
624 &serde_json::to_value(&CreateShareLink {
625 document: self.id(),
626 expiration: expires,
627 file_version: version,
628 })
629 .map_err(|e| Error::Other(e.to_string()))?,
630 ),
631 None,
632 )
633 .await?;
634
635 share_link.base_url = self.client.base_url.clone();
636 Ok(share_link)
637 }
638}
639
640impl Display for Content<'_> {
641 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642 match self {
643 Content::Full(text) => write!(f, "{text}"),
644 Content::Truncated(text) => write!(f, "{text}..."),
645 }
646 }
647}
648
649impl AsRef<str> for Content<'_> {
650 fn as_ref(&self) -> &str {
651 match self {
652 Content::Full(text) | Content::Truncated(text) => text,
653 }
654 }
655}