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