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 let user = validate_user(user_id)?;
54 let path = ApiPath::dav(format!("versions/{user}/versions/{file_id}"))?;
55
56 let body = PropfindBody::new([
57 DavProperty::LastModified,
58 DavProperty::ContentLength,
59 DavProperty::Etag,
60 DavProperty::ResourceType,
61 ]);
62
63 let mut headers = self.authenticated_headers();
64 headers.set(DEPTH_HEADER, Depth::One.as_header_value());
65 headers.set("Content-Type", XML_CONTENT_TYPE);
66
67 let request = Request::new(HttpMethod::Propfind, path.clone())
68 .with_headers(headers)
69 .body_bytes(body.to_xml().into_bytes());
70
71 let response = self.transport.execute(request).await?;
72
73 if response.status() != StatusCode::new(207) {
74 return Err(Error::Status(StatusError::new(
75 HttpMethod::Propfind.as_str(),
76 path,
77 response.status(),
78 response.body(),
79 )));
80 }
81
82 let body_str = std::str::from_utf8(response.body())
83 .map_err(|e| Error::Decode(DecodeError::new("FileVersion", e)))?;
84
85 decode_multistatus(body_str, file_id).map_err(Error::Decode)
86 }
87
88 pub async fn get(
92 &self,
93 user_id: &str,
94 file_id: u64,
95 revision_id: &str,
96 ) -> Result<Vec<u8>, Error> {
97 let user = validate_user(user_id)?;
98 let revision = validate_revision(revision_id)?;
99 let path = ApiPath::dav(format!("versions/{user}/versions/{file_id}/{revision}"))?;
100
101 let headers = self.authenticated_headers();
102 let request = Request::new(HttpMethod::Get, path.clone()).with_headers(headers);
103
104 let response = self.transport.execute(request).await?;
105
106 if !response.status().is_success() {
107 return Err(Error::Status(StatusError::new(
108 HttpMethod::Get.as_str(),
109 path,
110 response.status(),
111 response.body(),
112 )));
113 }
114
115 Ok(response.into_body())
116 }
117
118 pub async fn restore(
123 &self,
124 user_id: &str,
125 file_id: u64,
126 revision_id: &str,
127 ) -> Result<(), Error> {
128 let user = validate_user(user_id)?;
129 let revision = validate_revision(revision_id)?;
130
131 let source = ApiPath::dav(format!("versions/{user}/versions/{file_id}/{revision}"))?;
132 let destination = ApiPath::dav(format!("versions/{user}/restore/{file_id}"))?;
133
134 let mut headers = self.authenticated_headers();
135 headers.set(DESTINATION_HEADER, destination.as_str());
136
137 let request = Request::new(HttpMethod::Move, source.clone()).with_headers(headers);
138 let response = self.transport.execute(request).await?;
139
140 if !response.status().is_success() {
141 return Err(Error::Status(StatusError::new(
142 HttpMethod::Move.as_str(),
143 source,
144 response.status(),
145 response.body(),
146 )));
147 }
148
149 Ok(())
150 }
151
152 fn authenticated_headers(&self) -> Headers {
153 let mut headers = Headers::new();
154 self.auth.authenticate(&mut headers);
155 headers
156 }
157}