1use std::{fmt::Display, io, path::Path, sync::Arc};
12
13use enumflags2::{BitFlags, bitflags};
14use futures_util::TryStreamExt;
15use reqwest::Method;
16use serde::{Deserialize, Serialize};
17use tokio_util::io::StreamReader;
18
19use crate::{
20 DocumentCustomField, Error, Result, client::PaperlessClient, correspondent::CorrespondentId,
21 custom_field::CustomFieldId, document_type::DocumentTypeId, tag::TagId, user::UserId,
22};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
25#[repr(transparent)]
26pub struct DocumentId(pub i32);
27
28#[derive(Debug, Clone)]
40pub struct Document {
41 data: DocumentData,
42 client: Arc<PaperlessClient>,
43 content_is_truncated: bool,
44 changed_values: BitFlags<ChangedAttributes>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub(crate) struct DocumentData {
49 id: DocumentId,
50 original_file_name: String,
51 page_count: u32,
52 title: String,
53 content: String,
54 tags: Vec<TagId>,
55 owner: Option<UserId>,
56 correspondent: Option<CorrespondentId>,
57 custom_fields: Vec<DocumentCustomField>,
58 document_type: Option<DocumentTypeId>,
59}
60
61#[bitflags]
62#[repr(u8)]
63#[derive(Copy, Clone, Debug, PartialEq)]
64enum ChangedAttributes {
65 Title,
66 Content,
67 Tags,
68 CustomFields,
69 Correspondent,
70 DocumentType,
71}
72
73#[derive(Debug, Clone)]
75pub enum Content<'a> {
76 Full(&'a str),
78
79 Truncated(&'a str),
81}
82
83#[derive(Debug, Serialize)]
84struct PatchRequest {
85 #[serde(skip_serializing_if = "Option::is_none")]
86 title: Option<String>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
89 content: Option<String>,
90
91 #[serde(skip_serializing_if = "Option::is_none")]
92 tags: Option<Vec<TagId>>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
95 custom_fields: Option<Vec<DocumentCustomField>>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
98 correspondent: Option<CorrespondentId>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
101 document_type: Option<DocumentTypeId>,
102}
103
104impl std::fmt::Display for DocumentId {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 write!(f, "{}", self.0)
107 }
108}
109
110impl Document {
111 pub(crate) fn new(
112 data: DocumentData,
113 client: Arc<PaperlessClient>,
114 content_is_truncated: bool,
115 ) -> Self {
116 Self {
117 data,
118 client,
119 content_is_truncated,
120 changed_values: BitFlags::default(),
121 }
122 }
123
124 #[inline]
126 #[must_use]
127 pub fn id(&self) -> DocumentId {
128 self.data.id
129 }
130
131 #[inline]
133 #[must_use]
134 pub fn title(&self) -> &str {
135 &self.data.title
136 }
137
138 #[inline]
140 #[must_use]
141 pub fn original_file_name(&self) -> &str {
142 &self.data.original_file_name
143 }
144
145 #[inline]
147 #[must_use]
148 pub fn correspondent(&self) -> Option<CorrespondentId> {
149 self.data.correspondent
150 }
151
152 #[inline]
154 #[must_use]
155 pub fn owner(&self) -> Option<UserId> {
156 self.data.owner
157 }
158
159 #[inline]
161 #[must_use]
162 pub fn document_type(&self) -> Option<DocumentTypeId> {
163 self.data.document_type
164 }
165
166 #[inline]
168 #[must_use]
169 pub fn page_count(&self) -> u32 {
170 self.data.page_count
171 }
172
173 #[inline]
175 #[must_use]
176 pub fn tags(&self) -> &[TagId] {
177 &self.data.tags
178 }
179
180 #[inline]
182 #[must_use]
183 pub fn custom_fields(&self) -> &[DocumentCustomField] {
184 &self.data.custom_fields
185 }
186
187 #[inline]
189 #[must_use]
190 pub fn content(&self) -> Content<'_> {
191 if self.content_is_truncated {
192 Content::Truncated(&self.data.content)
193 } else {
194 Content::Full(&self.data.content)
195 }
196 }
197
198 pub fn add_tag(&mut self, tag_id: TagId) {
200 if !self.data.tags.contains(&tag_id) {
201 self.data.tags.push(tag_id);
202 self.changed_values |= ChangedAttributes::Tags;
203 }
204 }
205
206 pub fn remove_tag(&mut self, tag_id: TagId) {
207 if let Some(index) = self.data.tags.iter().position(|id| *id == tag_id) {
208 self.data.tags.remove(index);
209 self.changed_values |= ChangedAttributes::Tags;
210 }
211 }
212
213 pub fn set_title(&mut self, title: &str) {
215 self.data.title = title.to_string();
216 self.changed_values |= ChangedAttributes::Title;
217 }
218
219 pub fn set_content(&mut self, content: &str) {
221 self.data.content = content.to_string();
222 self.content_is_truncated = false;
223 self.changed_values |= ChangedAttributes::Content;
224 }
225
226 pub fn set_custom_field(&mut self, field: CustomFieldId, value: &str) {
228 for custom_field in &mut self.data.custom_fields {
229 if custom_field.field == field {
230 custom_field.value = value.to_string();
231 self.changed_values |= ChangedAttributes::CustomFields;
232 return;
233 }
234 }
235
236 self.data.custom_fields.push(DocumentCustomField {
237 field,
238 value: value.to_string(),
239 });
240 self.changed_values |= ChangedAttributes::CustomFields;
241 }
242
243 pub fn remove_custom_field(&mut self, field: CustomFieldId) {
245 if let Some(index) = self
246 .data
247 .custom_fields
248 .iter()
249 .position(|custom_field| custom_field.field == field)
250 {
251 self.data.custom_fields.remove(index);
252 self.changed_values |= ChangedAttributes::CustomFields;
253 }
254 }
255
256 #[inline]
258 #[must_use]
259 pub fn is_dirty(&self) -> bool {
260 !self.changed_values.is_empty()
261 }
262
263 pub async fn reload(&mut self) -> Result<()> {
267 let document_data = self
268 .client
269 .as_ref()
270 .get_document_data_by_id(self.data.id)
271 .await?;
272
273 self.data = document_data;
274
275 self.changed_values = BitFlags::empty();
276 self.content_is_truncated = false;
277 Ok(())
278 }
279
280 pub async fn patch(&mut self) -> Result<()> {
284 if !self.is_dirty() {
285 return Ok(());
286 }
287
288 let patch = PatchRequest {
289 title: self
290 .changed_values
291 .contains(ChangedAttributes::Title)
292 .then_some(self.data.title.clone()),
293
294 content: self
295 .changed_values
296 .contains(ChangedAttributes::Content)
297 .then_some(self.data.content.clone()),
298
299 tags: self
300 .changed_values
301 .contains(ChangedAttributes::Tags)
302 .then_some(self.data.tags.clone()),
303
304 custom_fields: self
305 .changed_values
306 .contains(ChangedAttributes::CustomFields)
307 .then_some(
308 self.data
309 .custom_fields
310 .iter()
311 .map(|field| DocumentCustomField {
312 field: field.field,
313 value: field.value.clone(),
314 })
315 .collect(),
316 ),
317 correspondent: self
318 .changed_values
319 .contains(ChangedAttributes::Correspondent)
320 .then_some(self.data.correspondent)
321 .flatten(),
322
323 document_type: self
324 .changed_values
325 .contains(ChangedAttributes::DocumentType)
326 .then_some(self.data.document_type)
327 .flatten(),
328 };
329
330 self.client
331 .request(
332 Method::PATCH,
333 &format!("/api/documents/{}/", self.data.id),
334 Some(&serde_json::to_value(patch).expect("Patch request")),
335 )
336 .await?;
337
338 self.changed_values = BitFlags::empty();
339 Ok(())
340 }
341
342 pub async fn get_full_content(&mut self) -> Result<()> {
344 if !self.content_is_truncated {
345 return Ok(());
346 }
347
348 let doc = self.client.get_document_data_by_id(self.data.id).await?;
349 self.data.content = doc.content;
350 self.content_is_truncated = false;
351 Ok(())
352 }
353
354 pub async fn download_to_file(&self, path: &Path) -> Result<()> {
356 let resp = self
357 .client
358 .request(
359 Method::GET,
360 &format!("/api/documents/{}/download/", self.data.id),
361 None,
362 )
363 .await?;
364
365 if !resp.status().is_success() {
366 return Err(Error::Other(format!(
367 "Failed to download document: {}",
368 resp.status()
369 )));
370 }
371
372 let mut stream = StreamReader::new(
373 resp.bytes_stream()
374 .map_err(|e| io::Error::other(format!("Failed to read response body: {e}"))),
375 );
376
377 let mut file = tokio::fs::File::create(path)
378 .await
379 .map_err(|e| Error::Other(format!("Failed to create file: {e}")))?;
380
381 tokio::io::copy(&mut stream, &mut file)
382 .await
383 .map_err(|e| Error::Other(format!("Failed to write file: {e}")))?;
384
385 Ok(())
386 }
387
388 pub async fn download_to_buffer(&self) -> Result<Vec<u8>> {
390 let resp = self
391 .client
392 .request(
393 Method::GET,
394 &format!("/api/documents/{}/download/", self.data.id),
395 None,
396 )
397 .await?;
398
399 if resp.status().is_success() {
400 let bytes = resp
401 .bytes()
402 .await
403 .map_err(|e| Error::Other(format!("Failed to read response body: {e}")))?;
404 Ok(bytes.to_vec())
405 } else {
406 Err(Error::Other(format!(
407 "Failed to download document: {}",
408 resp.status()
409 )))
410 }
411 }
412}
413
414impl Display for Content<'_> {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 match self {
417 Content::Full(text) => write!(f, "{text}"),
418 Content::Truncated(text) => write!(f, "{text}..."),
419 }
420 }
421}
422
423impl AsRef<str> for Content<'_> {
424 fn as_ref(&self) -> &str {
425 match self {
426 Content::Full(text) | Content::Truncated(text) => text,
427 }
428 }
429}