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