Skip to main content

nextcloud_trashbin/
client.rs

1//! WebDAV client for Nextcloud trashbin management.
2
3use 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
15/// Content-Type header value for XML request payloads.
16const XML_CONTENT_TYPE: &str = "application/xml; charset=utf-8";
17
18/// WebDAV client for interacting with Nextcloud's trashbin endpoints.
19///
20/// Executes calls through any [`Transport`] and applies credentials via an [`AuthProvider`].
21/// Both are held by reference, allowing the client to be freely cloned or borrowed across
22/// tasks without runtime dispatch or connection pooling overhead.
23///
24/// Supported operations:
25/// - [`list`](Self::list) — inspects deleted items
26/// - [`restore`](Self::restore) — restores an item to its original or specified path
27/// - [`delete`](Self::delete) — permanently deletes a specific item
28/// - [`empty`](Self::empty) — permanently deletes all items in the trashbin
29#[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    /// Binds a trashbin client to a transport and credential provider.
37    pub const fn new(transport: &'a T, auth: &'a A) -> Self {
38        Self { transport, auth }
39    }
40
41    /// The transport this client sends requests through.
42    pub const fn transport(&self) -> &'a T {
43        self.transport
44    }
45
46    /// The credential provider applied to every request.
47    pub const fn auth(&self) -> &'a A {
48        self.auth
49    }
50
51    /// Lists all items currently in `user_id`'s trashbin.
52    ///
53    /// Sends a `PROPFIND` request with `Depth: 1` to `/remote.php/dav/trashbin/{user_id}/trash/`.
54    ///
55    /// The response must be `207 Multi-Status`; any other status becomes [`Error::Status`].
56    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    /// Restores a deleted item from the trashbin.
85    ///
86    /// Sends a `MOVE` request from `/remote.php/dav/trashbin/{user_id}/trash/{item_id}`
87    /// to `/remote.php/dav/trashbin/{user_id}/restore/{restore_path}` (or the restore
88    /// folder if `restore_path` is empty).
89    ///
90    /// Any non-`2xx` status returns [`Error::Status`].
91    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    /// Permanently deletes a single item from the trashbin.
112    ///
113    /// Sends a `DELETE` request to `/remote.php/dav/trashbin/{user_id}/trash/{item_id}`.
114    ///
115    /// Any non-`2xx` status returns [`Error::Status`].
116    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    /// Permanently deletes all items in the user's trashbin.
127    ///
128    /// Sends a `DELETE` request to `/remote.php/dav/trashbin/{user_id}/trash/`.
129    ///
130    /// Any non-`2xx` status returns [`Error::Status`].
131    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}