1use std::fmt;
4
5use nextcloud_dav::{DEPTH_HEADER, DESTINATION_HEADER, OVERWRITE_HEADER, Overwrite};
6use nextcloud_sdk_core::{
7 ApiPath, AuthProvider, DecodeError, Error, Headers, HttpMethod, Request, Response, StatusCode,
8 StatusError, Transport,
9};
10
11use crate::item::TrashItem;
12use crate::path::{restore_destination_path, trash_item_path, trash_root_path};
13use crate::propfind::{build_propfind_body, decode_multistatus};
14
15const XML_CONTENT_TYPE: &str = "application/xml; charset=utf-8";
17
18#[derive(Clone, Copy, Debug)]
30pub struct TrashClient<'a, T: Transport, A: AuthProvider> {
31 transport: &'a T,
32 auth: &'a A,
33}
34
35impl<'a, T: Transport, A: AuthProvider> TrashClient<'a, T, A> {
36 pub const fn new(transport: &'a T, auth: &'a A) -> Self {
38 Self { transport, auth }
39 }
40
41 pub const fn transport(&self) -> &'a T {
43 self.transport
44 }
45
46 pub const fn auth(&self) -> &'a A {
48 self.auth
49 }
50
51 pub async fn list(&self, user_id: &str) -> Result<Vec<TrashItem>, Error> {
57 tracing::debug!(user_id, "Listing trashbin items");
58 let path = trash_root_path(user_id)?;
59 let mut headers = self.authenticated_headers();
60 headers.set(DEPTH_HEADER, "1");
61 headers.set("Content-Type", XML_CONTENT_TYPE);
62
63 let body = build_propfind_body();
64 let request = Request::new(HttpMethod::Propfind, path.clone())
65 .with_headers(headers)
66 .body_bytes(body.into_bytes());
67
68 let response = self.transport.execute(request).await?;
69
70 if response.status() != StatusCode::new(207) {
71 return Err(Error::Status(StatusError::new(
72 HttpMethod::Propfind.as_str(),
73 path,
74 response.status(),
75 response.body(),
76 )));
77 }
78
79 let text = std::str::from_utf8(response.body())
80 .map_err(|error| Error::Decode(DecodeError::new(crate::propfind::TARGET, error)))?;
81 decode_multistatus(user_id, text).map_err(Error::Decode)
82 }
83
84 pub async fn restore(
92 &self,
93 user_id: &str,
94 item_id: &str,
95 restore_path: &str,
96 ) -> Result<(), Error> {
97 tracing::info!(user_id, item_id, restore_path, "Restoring trashbin item");
98 let source_path = trash_item_path(user_id, item_id)?;
99 let destination_path = restore_destination_path(user_id, restore_path)?;
100
101 let mut headers = self.authenticated_headers();
102 headers.set(DESTINATION_HEADER, destination_path.as_str());
103 headers.set(OVERWRITE_HEADER, Overwrite::Allow.as_header_value());
104
105 let request = Request::new(HttpMethod::Move, source_path.clone()).with_headers(headers);
106 let response = self.transport.execute(request).await?;
107
108 ensure_success(HttpMethod::Move, &source_path, &response)
109 }
110
111 pub async fn delete(&self, user_id: &str, item_id: &str) -> Result<(), Error> {
117 tracing::info!(user_id, item_id, "Permanently deleting trashbin item");
118 let path = trash_item_path(user_id, item_id)?;
119 let headers = self.authenticated_headers();
120 let request = Request::new(HttpMethod::Delete, path.clone()).with_headers(headers);
121 let response = self.transport.execute(request).await?;
122
123 ensure_success(HttpMethod::Delete, &path, &response)
124 }
125
126 pub async fn empty(&self, user_id: &str) -> Result<(), Error> {
132 tracing::warn!(user_id, "Emptying trashbin");
133 let path = trash_root_path(user_id)?;
134 let headers = self.authenticated_headers();
135 let request = Request::new(HttpMethod::Delete, path.clone()).with_headers(headers);
136 let response = self.transport.execute(request).await?;
137
138 ensure_success(HttpMethod::Delete, &path, &response)
139 }
140
141 fn authenticated_headers(&self) -> Headers {
142 let mut headers = Headers::new();
143 self.auth.authenticate(&mut headers);
144 headers
145 }
146}
147
148fn ensure_success(method: HttpMethod, path: &ApiPath, response: &Response) -> Result<(), Error> {
149 if !response.status().is_success() {
150 tracing::error!(
151 method = ?method,
152 path = %path.as_str(),
153 status = %response.status(),
154 "Trashbin operation failed"
155 );
156 return Err(Error::Status(StatusError::new(
157 method.as_str(),
158 path.clone(),
159 response.status(),
160 response.body(),
161 )));
162 }
163 tracing::trace!(method = ?method, path = %path.as_str(), "Trashbin operation succeeded");
164 Ok(())
165}
166
167impl<T: Transport, A: AuthProvider> fmt::Display for TrashClient<'_, T, A> {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.write_str("TrashClient")
170 }
171}