nextcloud_versions/
client.rs1use nextcloud_dav::{DEPTH_HEADER, DESTINATION_HEADER, DavProperty, Depth, PropfindBody};
4use nextcloud_sdk_core::{
5 ApiPath, AuthProvider, DecodeError, Error, Headers, HttpMethod, Request, StatusCode,
6 StatusError, Transport,
7};
8
9use crate::decode::decode_multistatus;
10use crate::validation::{validate_revision, validate_user};
11use crate::version::FileVersion;
12
13#[cfg(test)]
14mod tests;
15
16const XML_CONTENT_TYPE: &str = "application/xml; charset=utf-8";
18
19#[derive(Clone, Copy, Debug)]
27pub struct VersionsClient<'a, T: Transport, A: AuthProvider> {
28 transport: &'a T,
29 auth: &'a A,
30}
31
32impl<'a, T: Transport, A: AuthProvider> VersionsClient<'a, T, A> {
33 pub const fn new(transport: &'a T, auth: &'a A) -> Self {
35 Self { transport, auth }
36 }
37
38 pub const fn transport(&self) -> &'a T {
40 self.transport
41 }
42
43 pub const fn auth(&self) -> &'a A {
45 self.auth
46 }
47
48 pub async fn list(&self, user_id: &str, file_id: u64) -> Result<Vec<FileVersion>, Error> {
53 tracing::debug!(user_id, file_id, "Listing file versions");
54 let user = validate_user(user_id)?;
55 let path = ApiPath::dav(format!("versions/{user}/versions/{file_id}"))?;
56
57 let body = PropfindBody::new([
58 DavProperty::LastModified,
59 DavProperty::ContentLength,
60 DavProperty::Etag,
61 DavProperty::ResourceType,
62 ]);
63
64 let mut headers = self.authenticated_headers();
65 headers.set(DEPTH_HEADER, Depth::One.as_header_value());
66 headers.set("Content-Type", XML_CONTENT_TYPE);
67
68 let request = Request::new(HttpMethod::Propfind, path.clone())
69 .with_headers(headers)
70 .body_bytes(body.to_xml().into_bytes());
71
72 let response = self.transport.execute(request).await?;
73
74 if response.status() != StatusCode::new(207) {
75 return Err(Error::Status(StatusError::new(
76 HttpMethod::Propfind.as_str(),
77 path,
78 response.status(),
79 response.body(),
80 )));
81 }
82
83 let body_str = std::str::from_utf8(response.body())
84 .map_err(|e| Error::Decode(DecodeError::new("FileVersion", e)))?;
85
86 decode_multistatus(body_str, file_id).map_err(Error::Decode)
87 }
88
89 pub async fn get(
93 &self,
94 user_id: &str,
95 file_id: u64,
96 revision_id: &str,
97 ) -> Result<Vec<u8>, Error> {
98 tracing::debug!(
99 user_id,
100 file_id,
101 revision_id,
102 "Downloading file version content"
103 );
104 let user = validate_user(user_id)?;
105 let revision = validate_revision(revision_id)?;
106 let path = ApiPath::dav(format!("versions/{user}/versions/{file_id}/{revision}"))?;
107
108 let headers = self.authenticated_headers();
109 let request = Request::new(HttpMethod::Get, path.clone()).with_headers(headers);
110
111 let response = self.transport.execute(request).await?;
112
113 if !response.status().is_success() {
114 tracing::warn!(
115 user_id,
116 file_id,
117 revision_id,
118 status = %response.status(),
119 "Failed to download file version"
120 );
121 return Err(Error::Status(StatusError::new(
122 HttpMethod::Get.as_str(),
123 path,
124 response.status(),
125 response.body(),
126 )));
127 }
128
129 Ok(response.into_body())
130 }
131
132 pub async fn restore(
137 &self,
138 user_id: &str,
139 file_id: u64,
140 revision_id: &str,
141 ) -> Result<(), Error> {
142 tracing::info!(user_id, file_id, revision_id, "Restoring file version");
143 let user = validate_user(user_id)?;
144 let revision = validate_revision(revision_id)?;
145
146 let source = ApiPath::dav(format!("versions/{user}/versions/{file_id}/{revision}"))?;
147 let destination = ApiPath::dav(format!("versions/{user}/restore/{file_id}"))?;
148
149 let mut headers = self.authenticated_headers();
150 headers.set(DESTINATION_HEADER, destination.as_str());
151
152 let request = Request::new(HttpMethod::Move, source.clone()).with_headers(headers);
153 let response = self.transport.execute(request).await?;
154
155 if !response.status().is_success() {
156 return Err(Error::Status(StatusError::new(
157 HttpMethod::Move.as_str(),
158 source,
159 response.status(),
160 response.body(),
161 )));
162 }
163
164 Ok(())
165 }
166
167 fn authenticated_headers(&self) -> Headers {
168 let mut headers = Headers::new();
169 self.auth.authenticate(&mut headers);
170 headers
171 }
172}