1use std::{borrow::Cow, collections::HashMap, path::Path, str::FromStr, sync::Arc};
4
5use enum_iterator::Sequence;
6use reqwest::{
7 Method, StatusCode,
8 header::{ACCEPT, HeaderMap, HeaderName, InvalidHeaderValue},
9 multipart,
10};
11use serde::{Deserialize, Serialize, de::DeserializeOwned};
12use tracing::{debug, trace};
13
14use crate::{
15 Error, Group, Result, SavedView, User,
16 document::{Document, DocumentData},
17 document_query::DocumentQueryBuilder,
18 dto::{CreateDto, Item, UpdateDto},
19 id::{
20 CorrespondentId, CustomFieldId, DocumentId, DocumentTypeId, GroupId, ItemId, StoragePathId,
21 TagId, TaskId, UserId,
22 },
23 metadata::{
24 correspondent::Correspondent, custom_field::CustomField, document_type::DocumentType,
25 storage_path::StoragePath, tag::Tag,
26 },
27 task::Task,
28 util,
29 workflow::Workflow,
30};
31
32#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Sequence)]
37#[non_exhaustive]
38pub enum RefreshMetaData {
39 Tags,
40 CustomFields,
41 Correspondents,
42 DocumentTypes,
43 Groups,
44 Users,
45 StoragePaths,
46}
47
48#[derive(Debug, Clone)]
50pub struct PaperlessClient {
51 pub request_full_permissions: bool,
53
54 pub request_full_content: bool,
56
57 pub(crate) base_url: Arc<str>,
58
59 client: reqwest::Client,
60 cached_data: Arc<CachedData>,
61}
62
63#[derive(Debug, Clone, Deserialize)]
65pub struct SearchHit {
66 pub score: f32,
68
69 pub highlights: Option<String>,
71
72 pub note_highlights: Option<String>,
74
75 pub rank: u32,
77}
78
79#[derive(Debug, Clone, Deserialize)]
80struct SearchResult {
81 #[serde(flatten)]
82 document_data: DocumentData,
83
84 #[serde(rename = "__search_hit__")]
85 search_hit: SearchHit,
86}
87
88#[derive(Debug, Clone)]
89struct CachedData {
90 correspondents: HashMap<CorrespondentId, Correspondent>,
91 custom_fields: HashMap<CustomFieldId, CustomField>,
92 document_types: HashMap<DocumentTypeId, DocumentType>,
93 groups: HashMap<GroupId, Group>,
94 storage_paths: HashMap<StoragePathId, StoragePath>,
95 tags: HashMap<TagId, Tag>,
96 users: HashMap<UserId, User>,
97}
98
99#[derive(Debug, Deserialize)]
100struct PaginatedResponse<T> {
101 results: Vec<T>,
102 next: Option<String>,
103}
104
105impl PaperlessClient {
106 pub fn new(
114 base_url: &str,
115 token: &str,
116 headers: Option<&HashMap<String, String>>,
117 ) -> std::result::Result<Self, String> {
118 Self::new_with_client(
119 base_url,
120 token,
121 headers,
122 reqwest::Client::builder().zstd(true),
123 )
124 }
125
126 pub fn new_with_client(
138 base_url: &str,
139 token: &str,
140 headers: Option<&HashMap<String, String>>,
141 client_builder: reqwest::ClientBuilder,
142 ) -> std::result::Result<Self, String> {
143 let mut headers_map = HeaderMap::new();
144
145 if let Some(headers) = headers {
147 for (key, value) in headers {
148 headers_map.insert(
149 HeaderName::from_str(key).map_err(|err| err.to_string())?,
150 value
151 .parse()
152 .map_err(|err: InvalidHeaderValue| err.to_string())?,
153 );
154 }
155 }
156
157 headers_map.insert(
159 HeaderName::from_str("Authorization").map_err(|err| err.to_string())?,
160 format!("Token {token}")
161 .parse()
162 .map_err(|err: InvalidHeaderValue| err.to_string())?,
163 );
164
165 Ok(Self {
166 request_full_permissions: false,
167 request_full_content: false,
168 base_url: base_url.into(),
169 client: client_builder
170 .default_headers(headers_map)
171 .build()
172 .map_err(|err| err.to_string())?,
173 cached_data: Arc::new(CachedData {
174 custom_fields: HashMap::new(),
175 correspondents: HashMap::new(),
176 document_types: HashMap::new(),
177 groups: HashMap::new(),
178 storage_paths: HashMap::new(),
179 tags: HashMap::new(),
180 users: HashMap::new(),
181 }),
182 })
183 }
184
185 #[must_use]
190 pub fn with_full_permissions(mut self, req: bool) -> Self {
191 self.request_full_permissions = req;
192 self
193 }
194
195 #[must_use]
196 pub fn with_full_content(mut self, full_content: bool) -> Self {
197 self.request_full_content = full_content;
198 self
199 }
200
201 pub async fn load_items<T: Item + DeserializeOwned>(&self) -> Result<HashMap<T::Id, T>> {
203 let endpoint = format!("/api/{}/", T::endpoint());
204 debug!(endpoint, "Loading");
205
206 let items: Vec<T> = self.fetch_all_pages(&endpoint, None).await?;
207 Ok(items.into_iter().map(|item| (item.id(), item)).collect())
208 }
209
210 fn default_query_params(&self) -> Option<HashMap<&'static str, Cow<'_, str>>> {
211 let mut params = HashMap::new();
212
213 if self.request_full_permissions {
214 params.insert(
215 crate::document_query::QUERY_PARAM_FULL_PERMISSIONS,
216 Cow::Borrowed("true"),
217 );
218 }
219 if !self.request_full_content {
220 params.insert(
221 crate::document_query::QUERY_PARAM_TRUNCATE_CONTENT,
222 Cow::Borrowed("true"),
223 );
224 }
225
226 if params.is_empty() {
227 None
228 } else {
229 Some(params)
230 }
231 }
232
233 pub async fn refresh_all(&mut self) -> Result<()> {
237 self.refresh(enum_iterator::all::<RefreshMetaData>()).await
238 }
239
240 pub async fn refresh(&mut self, data: impl IntoIterator<Item = RefreshMetaData>) -> Result<()> {
249 #[rustfmt::skip]
250 async fn inner(
251 client: &mut PaperlessClient,
252 data: &mut dyn Iterator<Item = RefreshMetaData>,
253 ) -> Result<()> {
254 let selected: std::collections::HashSet<_> = data.into_iter().collect();
255
256 if selected.is_empty() {
257 return Ok(());
258 }
259
260 let (tags, custom_fields, correspondents, document_types, groups, users, storage_paths) =
261 futures_util::try_join!(
262 async {
263 if selected.contains(&RefreshMetaData::Tags) {
264 Ok(Some(client.load_items::<Tag>().await?))
265 } else {
266 Ok::<Option<_>, Error>(None)
267 }
268 },
269 async {
270 if selected.contains(&RefreshMetaData::CustomFields) {
271 Ok(Some(client.load_items::<CustomField>().await?))
272 } else {
273 Ok(None)
274 }
275 },
276 async {
277 if selected.contains(&RefreshMetaData::Correspondents) {
278 Ok(Some(client.load_items::<Correspondent>().await?))
279 } else {
280 Ok(None)
281 }
282 },
283 async {
284 if selected.contains(&RefreshMetaData::DocumentTypes) {
285 Ok(Some(client.load_items::<DocumentType>().await?))
286 } else {
287 Ok(None)
288 }
289 },
290 async {
291 if selected.contains(&RefreshMetaData::Groups) {
292 Ok(Some(client.load_items::<Group>().await?))
293 } else {
294 Ok(None)
295 }
296 },
297 async {
298 if selected.contains(&RefreshMetaData::Users) {
299 Ok(Some(client.load_items::<User>().await?))
300 } else {
301 Ok(None)
302 }
303 },
304 async {
305 if selected.contains(&RefreshMetaData::StoragePaths) {
306 Ok(Some(client.load_items::<StoragePath>().await?))
307 } else {
308 Ok(None)
309 }
310 },
311 )?;
312
313 let cached_data = Arc::make_mut(&mut client.cached_data);
314
315 if let Some(value) = custom_fields { cached_data.custom_fields = value; }
316 if let Some(value) = correspondents { cached_data.correspondents = value; }
317 if let Some(value) = document_types { cached_data.document_types = value; }
318 if let Some(value) = groups { cached_data.groups = value; }
319 if let Some(value) = storage_paths { cached_data.storage_paths = value; }
320 if let Some(value) = tags { cached_data.tags = value; }
321 if let Some(value) = users { cached_data.users = value; }
322
323 Ok(())
324 }
325
326 inner(self, &mut data.into_iter()).await
327 }
328
329 pub async fn query_documents(&self, query: DocumentQueryBuilder) -> Result<Vec<Document>> {
331 let full_content = query.full_content;
332 let query_params = query.build();
333 let query: HashMap<&str, Cow<str>> = query_params
334 .query
335 .into_iter()
336 .map(|(k, v)| (k, Cow::Owned(v)))
337 .collect();
338
339 let documents: Vec<_> = self
340 .fetch_all_pages::<DocumentData>("/api/documents/", Some(&query))
341 .await?
342 .into_iter()
343 .map(|data| Document::new(data, Arc::new(self.clone()), !full_content))
344 .collect();
345
346 Ok(documents)
347 }
348
349 pub fn get_documents_by_tags(
351 &self,
352 tag_ids: &[TagId],
353 ) -> impl Future<Output = Result<Vec<Document>>> {
354 let query = DocumentQueryBuilder::default()
355 .full_content(self.request_full_content)
356 .full_permissions(self.request_full_permissions)
357 .tags_id_in(tag_ids.to_vec());
358
359 self.query_documents(query)
360 }
361
362 pub(crate) async fn get_document_data_by_id(
363 &self,
364 id: DocumentId,
365 full_content: Option<bool>,
366 full_permissions: Option<bool>,
367 ) -> Result<DocumentData> {
368 let mut params = self.default_query_params();
369
370 if full_content.is_some() || full_permissions.is_some() {
371 let mut updated_params = params.unwrap_or_default();
372
373 if let Some(full_content) = full_content {
374 updated_params.insert(
375 crate::document_query::QUERY_PARAM_TRUNCATE_CONTENT,
376 Cow::Owned((!full_content).to_string()),
377 );
378 }
379
380 if let Some(full_permissions) = full_permissions {
381 updated_params.insert(
382 crate::document_query::QUERY_PARAM_FULL_PERMISSIONS,
383 Cow::Owned(full_permissions.to_string()),
384 );
385 }
386
387 params = Some(updated_params);
388 }
389
390 self.request_json_no_body(
391 Method::GET,
392 &format!("/api/documents/{}/", id.0),
393 params.as_ref(),
394 )
395 .await
396 }
397
398 pub async fn get_document_by_id(
400 &self,
401 id: DocumentId,
402 full_content: Option<bool>,
403 full_permissions: Option<bool>,
404 ) -> Result<Document> {
405 let content_is_truncated = !full_content.unwrap_or(self.request_full_content);
406 Ok(Document::new(
407 self.get_document_data_by_id(id, full_content, full_permissions)
408 .await?,
409 Arc::new(self.clone()),
410 content_is_truncated,
411 ))
412 }
413
414 pub(crate) fn request_json_no_body<T: serde::de::DeserializeOwned>(
416 &self,
417 method: Method,
418 endpoint: &str,
419 query_params: Option<&HashMap<&str, Cow<str>>>,
420 ) -> impl Future<Output = Result<T>> {
421 self.request_json(method, endpoint, None::<&()>, query_params)
422 }
423
424 pub(crate) async fn request_json<T: serde::de::DeserializeOwned>(
426 &self,
427 method: Method,
428 endpoint: &str,
429 body: Option<&impl Serialize>,
430 query_params: Option<&HashMap<&str, Cow<'_, str>>>,
431 ) -> Result<T> {
432 let resp = self.request(method, endpoint, body, query_params).await?;
433
434 if tracing::enabled!(tracing::Level::TRACE) {
435 let response_text = resp.text().await.unwrap_or_default();
437 trace!(body = %response_text, "Response");
438
439 Ok(serde_json::from_str(&response_text)
440 .map_err(|e| Error::InvalidJson(format!("Failed to parse response body: {e:?}")))?)
441 } else {
442 Ok(resp
443 .json()
444 .await
445 .map_err(|e| Error::InvalidJson(format!("Failed to parse response body: {e:?}")))?)
446 }
447 }
448
449 pub(crate) fn request_no_body(
451 &self,
452 method: Method,
453 endpoint: &str,
454 query_params: Option<&HashMap<&str, Cow<'_, str>>>,
455 ) -> impl Future<Output = Result<reqwest::Response>> {
456 self.request(method, endpoint, None::<&()>, query_params)
457 }
458
459 pub(crate) async fn request(
461 &self,
462 method: Method,
463 endpoint: &str,
464 body: Option<&impl Serialize>,
465 query_params: Option<&HashMap<&str, Cow<'_, str>>>,
466 ) -> Result<reqwest::Response> {
467 let mut req = self
468 .client
469 .request(method, format!("{}{endpoint}", self.base_url))
470 .header(ACCEPT, "application/json");
471
472 if let Some(params) = query_params {
473 req = req.query(params);
474 }
475
476 if let Some(json_body) = body {
478 req = req.json(json_body);
479 }
480
481 let req = req.build().map_err(|e| Error::Request(e.into()))?;
482
483 if tracing::enabled!(tracing::Level::TRACE)
484 && let Some(body) = req.body().and_then(|b| b.as_bytes())
485 {
486 trace!(
487 method = ?req.method(),
488 url = ?req.url(),
489 body = %String::from_utf8_lossy(body),
490 "Sending request to Paperless API");
491 } else {
492 debug!(
493 method = ?req.method(),
494 url = ?req.url(),
495 "Sending request to Paperless API");
496 }
497
498 let resp = self
499 .client
500 .execute(req)
501 .await
502 .map_err(|e| Error::Other(format!("Failed to send request: {e}")))?;
503
504 debug!(status = ?resp.status(), "Response");
506
507 if resp.status() == StatusCode::NOT_FOUND {
508 return Err(Error::NotFound);
509 }
510
511 if !resp.status().is_success() {
512 return Err(Error::Response {
513 status_code: resp.status().as_u16(),
514 body: resp.text().await.unwrap_or_default(),
515 });
516 }
517
518 Ok(resp)
519 }
520
521 pub(crate) async fn fetch_all_pages<T: for<'de> Deserialize<'de>>(
522 &self,
523 endpoint: &str,
524 query_params: Option<&HashMap<&str, Cow<'_, str>>>,
525 ) -> Result<Vec<T>> {
526 let mut results = vec![];
527 let mut all_query_params = self.default_query_params().unwrap_or_default();
528 if let Some(query_params) = query_params {
529 all_query_params.extend(query_params.clone());
530 }
531
532 let mut all_query_params = Some(all_query_params);
533
534 let mut current_url = Some(endpoint.to_string());
535
536 while let Some(url) = current_url {
537 debug!("Fetching page: {url}");
538
539 let page: PaginatedResponse<T> = self
540 .request_json_no_body(Method::GET, &url, all_query_params.as_ref())
541 .await?;
542
543 results.extend(page.results);
544
545 current_url = page.next.and_then(|next_url| {
546 next_url
548 .strip_prefix(&*self.base_url)
549 .unwrap_or(&next_url)
550 .to_string()
551 .into()
552 });
553 all_query_params = None;
554 }
555
556 Ok(results)
557 }
558
559 pub async fn get_task_status(
561 &self,
562 task_id: Option<&TaskId>,
563 task_name: Option<&str>,
564 acknowledged: Option<bool>,
565 ) -> Result<Vec<Task>> {
566 let mut query = Vec::new();
567
568 if let Some(id) = task_id {
569 query.push(("task_id", id.to_string()));
570 }
571
572 if let Some(name) = task_name {
573 query.push(("task_name", name.to_string()));
574 }
575
576 if let Some(ack) = acknowledged {
577 query.push(("acknowledged", ack.to_string()));
578 }
579
580 let resp = self
581 .request_no_body(
582 Method::GET,
583 &format!(
584 "/api/tasks/?{}",
585 serde_urlencoded::to_string(&query)
586 .map_err(|e| Error::Other(format!("Failed to serialize query: {e}")))?
587 ),
588 None,
589 )
590 .await?;
591
592 let body = resp
593 .text()
594 .await
595 .map_err(|e| Error::Other(format!("Failed to read response body: {e:?}")))?;
596
597 trace!("get_task_status response: {:?}", body);
598
599 let tasks: Vec<Task> = match serde_json::from_str(&body) {
600 Ok(t) => t,
601 Err(e) => {
602 return Err(Error::InvalidJson(format!(
603 "Failed to parse response body: {e:?}"
604 )));
605 }
606 };
607
608 if tasks.is_empty() {
609 return Err(Error::NotFound);
610 }
611
612 Ok(tasks)
613 }
614
615 pub fn get_workflows(&self) -> impl Future<Output = Result<Vec<Workflow>>> {
617 self.fetch_all_pages("/api/workflows/", None)
618 }
619
620 pub fn get_saved_views(&self) -> impl Future<Output = Result<Vec<SavedView>>> {
622 self.fetch_all_pages("/api/saved_views/", None)
623 }
624
625 pub fn get_statistics(&self) -> impl Future<Output = Result<util::Statistics>> {
627 self.request_json_no_body(Method::GET, "/api/statistics/", None)
628 }
629
630 pub fn get_status(&self) -> impl Future<Output = Result<util::ServerStatus>> {
632 self.request_json_no_body(Method::GET, "/api/status/", None)
633 }
634
635 pub async fn create<T>(&self, new_item: &T) -> Result<T::BaseType>
641 where
642 T: CreateDto,
643 T::BaseType: Item,
644 {
645 let url = format!("/api/{}/", <T::BaseType as Item>::endpoint());
646 self.request_json(Method::POST, &url, Some(&new_item), None)
647 .await
648 }
649
650 pub async fn update<T>(&self, id: T::Id, update: &T) -> Result<T::BaseType>
656 where
657 T: UpdateDto,
658 T::BaseType: Item,
659 {
660 let url = format!("/api/{}/{}/", <T::BaseType as Item>::endpoint(), id);
661 self.request_json::<T::BaseType>(Method::PATCH, &url, Some(&update), None)
662 .await
663 }
664
665 pub async fn delete<T: ItemId>(&self, id: T) -> Result<()> {
669 let url = format!("/api/{}/{}/", T::endpoint(), id);
670 self.request_no_body(Method::DELETE, &url, None).await?;
671 Ok(())
672 }
673
674 pub async fn load_by_id<T: Item>(&self, id: T::Id) -> Result<Option<T>> {
678 let url = format!("/api/{}/{}/", T::endpoint(), id);
679 match self.request_json_no_body(Method::GET, &url, None).await {
680 found_item @ Ok(_) => found_item,
681 Err(Error::NotFound) => Ok(None),
682 err @ Err(_) => err,
683 }
684 }
685
686 pub async fn upload_document(&self, file_path: &Path, filename: &str) -> Result<TaskId> {
690 let stream = tokio::fs::File::open(file_path)
691 .await
692 .map_err(|e| Error::Other(format!("Failed to open file: {e}")))?;
693
694 let form = multipart::Form::new().part(
695 "document",
696 multipart::Part::stream(stream).file_name(filename.to_string()),
697 );
698
699 let url = format!("{}/api/documents/post_document/", self.base_url);
700
701 let resp = self
702 .client
703 .post(&url)
704 .multipart(form)
705 .send()
706 .await
707 .map_err(|e| Error::Other(format!("Failed to send request: {e}")))?;
708
709 let status = resp.status();
710 if !resp.status().is_success() {
711 return Err(Error::Response {
712 status_code: status.as_u16(),
713 body: resp.text().await.unwrap_or_default(),
714 });
715 }
716
717 let task_id: String = resp
718 .json()
719 .await
720 .map_err(|e| Error::Other(format!("Failed to parse task ID: {e:?}")))?;
721 Ok(TaskId(task_id))
722 }
723
724 #[inline]
726 #[must_use]
727 pub fn tags(&self) -> &HashMap<TagId, Tag> {
728 &self.cached_data.tags
729 }
730
731 #[inline]
733 #[must_use]
734 pub fn storage_paths(&self) -> &HashMap<StoragePathId, StoragePath> {
735 &self.cached_data.storage_paths
736 }
737
738 #[must_use]
740 pub fn find_tag_by_name(&self, name: &str) -> Option<&Tag> {
741 self.cached_data.tags.values().find(|tag| tag.name == name)
742 }
743
744 #[inline]
746 #[must_use]
747 pub fn document_types(&self) -> &HashMap<DocumentTypeId, DocumentType> {
748 &self.cached_data.document_types
749 }
750
751 #[must_use]
753 pub fn find_document_type_by_name(&self, name: &str) -> Option<&DocumentType> {
754 self.cached_data
755 .document_types
756 .values()
757 .find(|dt| dt.name == name)
758 }
759
760 pub async fn search(&self, search: &str) -> Result<Vec<(Document, SearchHit)>> {
762 let results = self
763 .fetch_all_pages::<SearchResult>(
764 "/api/documents/",
765 Some(&HashMap::from([("query", search.into())])),
766 )
767 .await?
768 .into_iter()
769 .map(|result| {
770 (
771 Document::new(
772 result.document_data,
773 Arc::new(self.clone()),
774 !self.request_full_content,
775 ),
776 result.search_hit,
777 )
778 })
779 .collect();
780 Ok(results)
781 }
782
783 #[inline]
785 #[must_use]
786 pub fn correspondents(&self) -> &HashMap<CorrespondentId, Correspondent> {
787 &self.cached_data.correspondents
788 }
789
790 #[inline]
792 #[must_use]
793 pub fn custom_fields(&self) -> &HashMap<CustomFieldId, CustomField> {
794 &self.cached_data.custom_fields
795 }
796
797 #[must_use]
799 pub fn find_custom_field_by_name(&self, name: &str) -> Option<&CustomField> {
800 self.cached_data
801 .custom_fields
802 .values()
803 .find(|field| field.name == name)
804 }
805
806 #[inline]
808 #[must_use]
809 pub fn users(&self) -> &HashMap<UserId, User> {
810 &self.cached_data.users
811 }
812
813 #[inline]
815 #[must_use]
816 pub fn groups(&self) -> &HashMap<GroupId, Group> {
817 &self.cached_data.groups
818 }
819}