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