brume/concrete/nextcloud/
mod.rs1use std::{
4 error::Error,
5 fmt::{Display, Formatter},
6 io::{self},
7 string::FromUtf8Error,
8 sync::Arc,
9};
10
11use bytes::Bytes;
12use futures::{Stream, TryStream, TryStreamExt};
13use reqwest::Body;
14use reqwest_dav::{Auth, Client, ClientBuilder, Depth};
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18mod dav;
19
20use crate::{
21 update::{IsModified, ModificationState},
22 vfs::{Vfs, VirtualPath, VirtualPathError},
23};
24
25use dav::{TagError, dav_parse_entity_tag, dav_parse_vfs};
26
27use super::{FSBackend, FsBackendError, FsInstanceDescription, Named};
28
29const NC_DAV_PATH_STR: &str = "/remote.php/dav/files/";
30
31#[derive(Error, Debug)]
33pub enum NextcloudFsError {
34 #[error("a path provided by the server is invalid")]
35 InvalidPath(#[from] VirtualPathError),
36 #[error("a tag provided by the server is invalid")]
37 InvalidTag(#[from] TagError),
38 #[error("the structure of the nextcloud FS is not valid")]
39 BadStructure,
40 #[error("failed to decode server provided url")]
41 UrlDecode(#[from] FromUtf8Error),
42 #[error("a dav protocol error occurred during communication with the nextcloud server")]
43 ProtocolError(#[from] reqwest_dav::Error),
44 #[error("io error while sending or receiving a file")]
45 IoError(#[from] io::Error),
46}
47
48impl NextcloudFsError {
49 pub fn protocol_error_message(&self) -> Option<String> {
51 match self {
52 NextcloudFsError::ProtocolError(reqwest_dav::Error::Reqwest(error)) => {
53 if let Some(source) = error.source() {
54 if let Some(source2) = source.source() {
55 Some(source2.to_string())
56 } else {
57 Some(source.to_string())
58 }
59 } else {
60 Some(error.to_string())
61 }
62 }
63 _ => None,
64 }
65 }
66}
67
68impl From<reqwest::Error> for NextcloudFsError {
69 fn from(value: reqwest::Error) -> Self {
70 Self::ProtocolError(value.into())
71 }
72}
73
74impl From<NextcloudFsError> for FsBackendError {
75 fn from(value: NextcloudFsError) -> Self {
76 Self(Arc::new(value))
77 }
78}
79
80#[derive(Debug)]
82pub struct NextcloudFs {
83 client: Client,
84 name: String,
85}
86
87impl NextcloudFs {
88 pub fn new(url: &str, login: &str, password: &str) -> Result<Self, NextcloudFsError> {
90 let name = login.to_string();
91 let client = ClientBuilder::new()
92 .set_host(format!("{}{}{}/", url, NC_DAV_PATH_STR, &name))
93 .set_auth(Auth::Basic(login.to_string(), password.to_string()))
94 .build()?;
95
96 Ok(Self {
97 client,
98 name: name.to_string(),
99 })
100 }
101}
102
103impl FSBackend for NextcloudFs {
104 type SyncInfo = NextcloudSyncInfo;
105
106 type IoError = NextcloudFsError;
107
108 type CreationInfo = NextcloudFsCreationInfo;
109
110 type Description = NextcloudFsDescription;
111
112 async fn validate(info: &Self::CreationInfo) -> Result<(), Self::IoError> {
113 let nextcloud: Self = info.clone().try_into()?;
115 nextcloud
116 .client
117 .list("", Depth::Number(0))
118 .await
119 .map(|_| ())
120 .map_err(|e| e.into())
121 }
122
123 fn description(&self) -> Self::Description {
124 NextcloudFsDescription {
125 server_url: self
126 .client
127 .host
128 .trim_end_matches('/')
129 .trim_end_matches(&self.name)
130 .trim_end_matches(&NC_DAV_PATH_STR)
131 .to_string(),
132 name: self.name.clone(),
133 }
134 }
135
136 async fn get_sync_info(&self, path: &VirtualPath) -> Result<Self::SyncInfo, Self::IoError> {
137 let elements = self.client.list(path.into(), Depth::Number(0)).await?;
138
139 let elem = elements.first().ok_or(NextcloudFsError::BadStructure)?;
140
141 dav_parse_entity_tag(elem.clone())
142 }
143
144 async fn load_virtual(&self) -> Result<Vfs<Self::SyncInfo>, Self::IoError> {
145 let elements = self.client.list("", Depth::Infinity).await?;
146
147 let vfs_root = dav_parse_vfs(elements, &self.name)?;
148
149 Ok(Vfs::new(vfs_root))
150 }
151
152 async fn read_file(
153 &self,
154 path: &VirtualPath,
155 ) -> Result<impl Stream<Item = Result<Bytes, Self::IoError>> + 'static, Self::IoError> {
156 Ok(self
157 .client
158 .get(path.into())
159 .await?
160 .bytes_stream()
161 .map_err(|e| e.into()))
162 }
163
164 async fn write_file<Data: TryStream + Send + 'static>(
165 &self,
166 path: &VirtualPath,
167 data: Data,
168 ) -> Result<Self::SyncInfo, Self::IoError>
169 where
170 Data::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
171 Bytes: From<Data::Ok>,
172 {
173 let body = Body::wrap_stream(data);
174
175 self.client.put(path.into(), body).await?;
176
177 let mut entities = self.client.list(path.into(), Depth::Number(0)).await?;
179 entities
180 .pop()
181 .ok_or(NextcloudFsError::BadStructure)
182 .and_then(dav_parse_entity_tag)
183 }
184
185 async fn rm(&self, path: &VirtualPath) -> Result<(), Self::IoError> {
186 self.client.delete(path.into()).await.map_err(|e| e.into())
187 }
188
189 async fn mkdir(&self, path: &VirtualPath) -> Result<Self::SyncInfo, Self::IoError> {
190 self.client.mkcol(path.into()).await?;
191
192 let mut entities = self.client.list(path.into(), Depth::Number(0)).await?;
194 entities
195 .pop()
196 .ok_or(NextcloudFsError::BadStructure)
197 .and_then(dav_parse_entity_tag)
198 }
199
200 async fn rmdir(&self, path: &VirtualPath) -> Result<(), Self::IoError> {
201 self.client.delete(path.into()).await.map_err(|e| e.into())
202 }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct NextcloudSyncInfo {
213 tag: u128,
214}
215
216impl NextcloudSyncInfo {
217 pub fn new(tag: u128) -> Self {
218 Self { tag }
219 }
220}
221
222impl Named for NextcloudSyncInfo {
223 const TYPE_NAME: &'static str = "Nextcloud";
224}
225
226impl IsModified for NextcloudSyncInfo {
227 fn modification_state(&self, reference: &Self) -> ModificationState {
228 if self.tag != reference.tag {
229 ModificationState::Modified
230 } else {
231 ModificationState::RecursiveUnmodified
232 }
233 }
234}
235
236impl<'a> From<&'a NextcloudSyncInfo> for NextcloudSyncInfo {
237 fn from(value: &'a NextcloudSyncInfo) -> Self {
238 value.to_owned()
239 }
240}
241
242impl<'a> From<&'a NextcloudSyncInfo> for () {
243 fn from(_value: &'a NextcloudSyncInfo) -> Self {}
244}
245
246#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)]
248pub struct NextcloudFsDescription {
249 server_url: String,
250 name: String,
251}
252
253impl FsInstanceDescription for NextcloudFsDescription {
254 fn name(&self) -> &str {
255 &self.name
256 }
257}
258
259impl Display for NextcloudFsDescription {
260 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
261 write!(f, "url: {}, folder: {}", self.server_url, self.name)
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct NextcloudFsCreationInfo {
268 server_url: String,
269 login: String,
270 password: String,
271}
272
273impl NextcloudFsCreationInfo {
274 pub fn new(server_url: &str, login: &str, password: &str) -> Self {
275 Self {
276 server_url: server_url.to_string(),
277 login: login.to_string(),
278 password: password.to_string(),
279 }
280 }
281}
282
283impl From<NextcloudFsCreationInfo> for NextcloudFsDescription {
284 fn from(value: NextcloudFsCreationInfo) -> Self {
285 Self {
286 server_url: value.server_url,
287 name: value.login,
288 }
289 }
290}
291
292impl TryFrom<NextcloudFsCreationInfo> for NextcloudFs {
293 type Error = <NextcloudFs as FSBackend>::IoError;
294
295 fn try_from(value: NextcloudFsCreationInfo) -> Result<Self, Self::Error> {
296 Self::new(&value.server_url, &value.login, &value.password)
297 }
298}