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        let path = trash_root_path(user_id)?;
58        let mut headers = self.authenticated_headers();
59        headers.set(DEPTH_HEADER, "1");
60        headers.set("Content-Type", XML_CONTENT_TYPE);
61
62        let body = build_propfind_body();
63        let request = Request::new(HttpMethod::Propfind, path.clone())
64            .with_headers(headers)
65            .body_bytes(body.into_bytes());
66
67        let response = self.transport.execute(request).await?;
68
69        if response.status() != StatusCode::new(207) {
70            return Err(Error::Status(StatusError::new(
71                HttpMethod::Propfind.as_str(),
72                path,
73                response.status(),
74                response.body(),
75            )));
76        }
77
78        let text = std::str::from_utf8(response.body())
79            .map_err(|error| Error::Decode(DecodeError::new(crate::propfind::TARGET, error)))?;
80        decode_multistatus(user_id, text).map_err(Error::Decode)
81    }
82
83    /// Restores a deleted item from the trashbin.
84    ///
85    /// Sends a `MOVE` request from `/remote.php/dav/trashbin/{user_id}/trash/{item_id}`
86    /// to `/remote.php/dav/trashbin/{user_id}/restore/{restore_path}` (or the restore
87    /// folder if `restore_path` is empty).
88    ///
89    /// Any non-`2xx` status returns [`Error::Status`].
90    pub async fn restore(
91        &self,
92        user_id: &str,
93        item_id: &str,
94        restore_path: &str,
95    ) -> Result<(), Error> {
96        let source_path = trash_item_path(user_id, item_id)?;
97        let destination_path = restore_destination_path(user_id, restore_path)?;
98
99        let mut headers = self.authenticated_headers();
100        headers.set(DESTINATION_HEADER, destination_path.as_str());
101        headers.set(OVERWRITE_HEADER, Overwrite::Allow.as_header_value());
102
103        let request = Request::new(HttpMethod::Move, source_path.clone()).with_headers(headers);
104        let response = self.transport.execute(request).await?;
105
106        ensure_success(HttpMethod::Move, &source_path, &response)
107    }
108
109    /// Permanently deletes a single item from the trashbin.
110    ///
111    /// Sends a `DELETE` request to `/remote.php/dav/trashbin/{user_id}/trash/{item_id}`.
112    ///
113    /// Any non-`2xx` status returns [`Error::Status`].
114    pub async fn delete(&self, user_id: &str, item_id: &str) -> Result<(), Error> {
115        let path = trash_item_path(user_id, item_id)?;
116        let headers = self.authenticated_headers();
117        let request = Request::new(HttpMethod::Delete, path.clone()).with_headers(headers);
118        let response = self.transport.execute(request).await?;
119
120        ensure_success(HttpMethod::Delete, &path, &response)
121    }
122
123    /// Permanently deletes all items in the user's trashbin.
124    ///
125    /// Sends a `DELETE` request to `/remote.php/dav/trashbin/{user_id}/trash/`.
126    ///
127    /// Any non-`2xx` status returns [`Error::Status`].
128    pub async fn empty(&self, user_id: &str) -> Result<(), Error> {
129        let path = trash_root_path(user_id)?;
130        let headers = self.authenticated_headers();
131        let request = Request::new(HttpMethod::Delete, path.clone()).with_headers(headers);
132        let response = self.transport.execute(request).await?;
133
134        ensure_success(HttpMethod::Delete, &path, &response)
135    }
136
137    fn authenticated_headers(&self) -> Headers {
138        let mut headers = Headers::new();
139        self.auth.authenticate(&mut headers);
140        headers
141    }
142}
143
144fn ensure_success(method: HttpMethod, path: &ApiPath, response: &Response) -> Result<(), Error> {
145    if !response.status().is_success() {
146        return Err(Error::Status(StatusError::new(
147            method.as_str(),
148            path.clone(),
149            response.status(),
150            response.body(),
151        )));
152    }
153    Ok(())
154}
155
156impl<T: Transport, A: AuthProvider> fmt::Display for TrashClient<'_, T, A> {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.write_str("TrashClient")
159    }
160}