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