rustic_rs/commands/webdav/
webdavfs.rs

1#[cfg(not(windows))]
2use std::os::unix::ffi::OsStrExt;
3use std::{
4    fmt::{Debug, Formatter},
5    io::SeekFrom,
6    sync::{Arc, OnceLock},
7    time::SystemTime,
8};
9
10use bytes::{Buf, Bytes};
11use dav_server::{
12    davpath::DavPath,
13    fs::{
14        DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError, FsFuture, FsResult, FsStream,
15        OpenOptions, ReadDirMeta,
16    },
17};
18use futures::FutureExt;
19use rustic_core::{
20    IndexedFull, Repository,
21    repofile::Node,
22    vfs::{FilePolicy, OpenFile, Vfs},
23};
24use tokio::task::spawn_blocking;
25
26fn now() -> SystemTime {
27    static NOW: OnceLock<SystemTime> = OnceLock::new();
28    *NOW.get_or_init(SystemTime::now)
29}
30
31/// The inner state of a [`WebDavFS`] instance.
32struct DavFsInner<P, S> {
33    /// The [`Repository`] to use
34    repo: Repository<P, S>,
35
36    /// The [`Vfs`] to use
37    vfs: Vfs,
38
39    /// The [`FilePolicy`] to use
40    file_policy: FilePolicy,
41}
42
43impl<P, S> Debug for DavFsInner<P, S> {
44    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
45        write!(f, "DavFS")
46    }
47}
48
49/// DAV Filesystem implementation.
50///
51/// This is the main entry point for the DAV filesystem.
52/// It implements [`DavFileSystem`] and can be used to serve a [`Repository`] via DAV.
53#[derive(Debug)]
54pub struct WebDavFS<P, S> {
55    inner: Arc<DavFsInner<P, S>>,
56}
57
58impl<P: Send + Sync + 'static, S: IndexedFull + Send + Sync + 'static> WebDavFS<P, S> {
59    /// Create a new [`WebDavFS`] instance.
60    ///
61    /// # Arguments
62    ///
63    /// * `repo` - The [`Repository`] to use
64    /// * `vfs` - The [`Vfs`] to use
65    /// * `file_policy` - The [`FilePolicy`] to use
66    ///
67    /// # Returns
68    ///
69    /// A new [`WebDavFS`] instance
70    pub(crate) fn new(repo: Repository<P, S>, vfs: Vfs, file_policy: FilePolicy) -> Self {
71        let inner = DavFsInner {
72            repo,
73            vfs,
74            file_policy,
75        };
76
77        Self {
78            inner: Arc::new(inner),
79        }
80    }
81
82    /// Get a [`Node`] from the specified [`DavPath`].
83    ///
84    /// # Arguments
85    ///
86    /// * `path` - The path to get the [`Tree`] at
87    ///
88    /// # Errors
89    ///
90    /// * If the [`Tree`] could not be found
91    ///
92    /// # Returns
93    ///
94    /// The [`Node`] at the specified path
95    ///
96    /// [`Tree`]: crate::repofile::Tree
97    async fn node_from_path(&self, path: &DavPath) -> Result<Node, FsError> {
98        let inner = self.inner.clone();
99        let path = path.as_pathbuf();
100        spawn_blocking(move || {
101            inner
102                .vfs
103                .node_from_path(&inner.repo, &path)
104                .map_err(|_| FsError::GeneralFailure)
105        })
106        .await
107        .map_err(|_| FsError::GeneralFailure)?
108    }
109
110    /// Get a list of [`Node`]s from the specified directory path.
111    ///
112    /// # Arguments
113    ///
114    /// * `path` - The path to get the [`Tree`] at
115    ///
116    /// # Errors
117    ///
118    /// * If the [`Tree`] could not be found
119    ///
120    /// # Returns
121    ///
122    /// The list of [`Node`]s at the specified path
123    ///
124    /// [`Tree`]: crate::repofile::Tree
125    async fn dir_entries_from_path(&self, path: &DavPath) -> Result<Vec<Node>, FsError> {
126        let inner = self.inner.clone();
127        let path = path.as_pathbuf();
128        spawn_blocking(move || {
129            inner
130                .vfs
131                .dir_entries_from_path(&inner.repo, &path)
132                .map_err(|_| FsError::GeneralFailure)
133        })
134        .await
135        .map_err(|_| FsError::GeneralFailure)?
136    }
137}
138
139impl<P, S: IndexedFull> Clone for WebDavFS<P, S> {
140    fn clone(&self) -> Self {
141        Self {
142            inner: self.inner.clone(),
143        }
144    }
145}
146
147impl<P: Debug + Send + Sync + 'static, S: IndexedFull + Debug + Send + Sync + 'static> DavFileSystem
148    for WebDavFS<P, S>
149{
150    fn metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'_, Box<dyn DavMetaData>> {
151        self.symlink_metadata(davpath)
152    }
153
154    fn symlink_metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'_, Box<dyn DavMetaData>> {
155        async move {
156            let node = self.node_from_path(davpath).await?;
157            let meta: Box<dyn DavMetaData> = Box::new(DavFsMetaData(node));
158            Ok(meta)
159        }
160        .boxed()
161    }
162
163    fn read_dir<'a>(
164        &'a self,
165        davpath: &'a DavPath,
166        _meta: ReadDirMeta,
167    ) -> FsFuture<'_, FsStream<Box<dyn DavDirEntry>>> {
168        async move {
169            let entries = self.dir_entries_from_path(davpath).await?;
170            let entry_iter = entries.into_iter().map(|e| {
171                let entry: Box<dyn DavDirEntry> = Box::new(DavFsDirEntry(e));
172                Ok(entry)
173            });
174            let strm: FsStream<Box<dyn DavDirEntry>> = Box::pin(futures::stream::iter(entry_iter));
175            Ok(strm)
176        }
177        .boxed()
178    }
179
180    fn open<'a>(
181        &'a self,
182        path: &'a DavPath,
183        options: OpenOptions,
184    ) -> FsFuture<'_, Box<dyn DavFile>> {
185        async move {
186            if options.write
187                || options.append
188                || options.truncate
189                || options.create
190                || options.create_new
191            {
192                return Err(FsError::Forbidden);
193            }
194
195            let node = self.node_from_path(path).await?;
196            if matches!(self.inner.file_policy, FilePolicy::Forbidden) {
197                return Err(FsError::Forbidden);
198            }
199
200            let inner = self.inner.clone();
201            let node_copy = node.clone();
202            let open = spawn_blocking(move || {
203                inner
204                    .repo
205                    .open_file(&node_copy)
206                    .map_err(|_err| FsError::GeneralFailure)
207            })
208            .await
209            .map_err(|_| FsError::GeneralFailure)??;
210
211            let file: Box<dyn DavFile> = Box::new(DavFsFile {
212                node,
213                open: Arc::new(open),
214                fs: self.inner.clone(),
215                seek: 0,
216            });
217            Ok(file)
218        }
219        .boxed()
220    }
221}
222
223/// A [`DavDirEntry`] implementation for [`Node`]s.
224#[derive(Clone, Debug)]
225struct DavFsDirEntry(Node);
226
227impl DavDirEntry for DavFsDirEntry {
228    fn metadata(&self) -> FsFuture<'_, Box<dyn DavMetaData>> {
229        async move {
230            let meta: Box<dyn DavMetaData> = Box::new(DavFsMetaData(self.0.clone()));
231            Ok(meta)
232        }
233        .boxed()
234    }
235
236    #[cfg(not(windows))]
237    fn name(&self) -> Vec<u8> {
238        self.0.name().as_bytes().to_vec()
239    }
240
241    #[cfg(windows)]
242    fn name(&self) -> Vec<u8> {
243        self.0
244            .name()
245            .as_os_str()
246            .to_string_lossy()
247            .to_string()
248            .into_bytes()
249    }
250}
251
252/// A [`DavFile`] implementation for [`Node`]s.
253///
254/// This is a read-only file.
255struct DavFsFile<P, S> {
256    /// The [`Node`] this file is for
257    node: Node,
258
259    /// The [`OpenFile`] for this file
260    open: Arc<OpenFile>,
261
262    /// The [`DavFsInner`] this file belongs to
263    fs: Arc<DavFsInner<P, S>>,
264
265    /// The current seek position
266    seek: usize,
267}
268
269impl<P, S> Debug for DavFsFile<P, S> {
270    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
271        write!(f, "DavFile")
272    }
273}
274
275impl<P: Debug + Send + Sync + 'static, S: IndexedFull + Debug + Send + Sync + 'static> DavFile
276    for DavFsFile<P, S>
277{
278    fn metadata(&mut self) -> FsFuture<'_, Box<dyn DavMetaData>> {
279        async move {
280            let meta: Box<dyn DavMetaData> = Box::new(DavFsMetaData(self.node.clone()));
281            Ok(meta)
282        }
283        .boxed()
284    }
285
286    fn write_bytes(&mut self, _buf: Bytes) -> FsFuture<'_, ()> {
287        async move { Err(FsError::Forbidden) }.boxed()
288    }
289
290    fn write_buf(&mut self, _buf: Box<dyn Buf + Send>) -> FsFuture<'_, ()> {
291        async move { Err(FsError::Forbidden) }.boxed()
292    }
293
294    fn read_bytes(&mut self, count: usize) -> FsFuture<'_, Bytes> {
295        let fs = self.fs.clone();
296        let seek = self.seek;
297        let open = self.open.clone();
298        async move {
299            let data = spawn_blocking(move || {
300                fs.repo
301                    .read_file_at(&open, seek, count)
302                    .map_err(|_err| FsError::GeneralFailure)
303            })
304            .await
305            .map_err(|_| FsError::GeneralFailure)??;
306            self.seek += data.len();
307            Ok(data)
308        }
309        .boxed()
310    }
311
312    fn seek(&mut self, pos: SeekFrom) -> FsFuture<'_, u64> {
313        async move {
314            match pos {
315                SeekFrom::Start(start) => {
316                    self.seek = usize::try_from(start).expect("usize overflow should not happen");
317                }
318                SeekFrom::Current(delta) => {
319                    self.seek = usize::try_from(
320                        i64::try_from(self.seek).expect("i64 wrapped around") + delta,
321                    )
322                    .expect("usize overflow should not happen");
323                }
324                SeekFrom::End(end) => {
325                    self.seek = usize::try_from(
326                        i64::try_from(self.node.meta.size).expect("i64 wrapped around") + end,
327                    )
328                    .expect("usize overflow should not happen");
329                }
330            }
331
332            Ok(self.seek as u64)
333        }
334        .boxed()
335    }
336
337    fn flush(&mut self) -> FsFuture<'_, ()> {
338        async move { Ok(()) }.boxed()
339    }
340}
341
342/// A [`DavMetaData`] implementation for [`Node`]s.
343#[derive(Clone, Debug)]
344struct DavFsMetaData(Node);
345
346impl DavMetaData for DavFsMetaData {
347    fn len(&self) -> u64 {
348        self.0.meta.size
349    }
350    fn created(&self) -> FsResult<SystemTime> {
351        Ok(now())
352    }
353    fn modified(&self) -> FsResult<SystemTime> {
354        Ok(self.0.meta.mtime.map_or_else(now, SystemTime::from))
355    }
356    fn accessed(&self) -> FsResult<SystemTime> {
357        Ok(self.0.meta.atime.map_or_else(now, SystemTime::from))
358    }
359
360    fn status_changed(&self) -> FsResult<SystemTime> {
361        Ok(self.0.meta.ctime.map_or_else(now, SystemTime::from))
362    }
363
364    fn is_dir(&self) -> bool {
365        self.0.is_dir()
366    }
367    fn is_file(&self) -> bool {
368        self.0.is_file()
369    }
370    fn is_symlink(&self) -> bool {
371        self.0.is_symlink()
372    }
373    fn executable(&self) -> FsResult<bool> {
374        if self.0.is_file() {
375            let Some(mode) = self.0.meta.mode else {
376                return Ok(false);
377            };
378            return Ok((mode & 0o100) > 0);
379        }
380        Err(FsError::NotImplemented)
381    }
382}