From 3d13f2e527c22fd7ff013c8351ecea52717fa074 Mon Sep 17 00:00:00 2001 From: aawsome <37850842+aawsome@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:00:59 +0100 Subject: [PATCH] refactor: move `webdavfs` from `rustic_core` to `rustic-rs` (#1363) Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> Co-authored-by: simonsan <14062932+simonsan@users.noreply.github.com> --- Cargo.lock | 2 + Cargo.toml | 11 +- src/commands/webdav.rs | 6 +- src/commands/webdav/webdavfs.rs | 357 ++++++++++++++++++++++++++++++++ 4 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 src/commands/webdav/webdavfs.rs diff --git a/Cargo.lock b/Cargo.lock index 728c74604..1bba168f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4159,6 +4159,7 @@ dependencies = [ "aho-corasick", "anyhow", "assert_cmd", + "bytes", "bytesize", "cached 0.53.1", "cfg-if 1.0.0", @@ -4178,6 +4179,7 @@ dependencies = [ "displaydoc", "flate2", "fuse_mt", + "futures", "gethostname", "globset", "human-panic", diff --git a/Cargo.toml b/Cargo.toml index 01cb3c7bd..b7a2d515e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,14 @@ jemallocator = ["dep:jemallocator-global"] # Commands self-update = ["dep:self_update", "dep:semver"] tui = ["dep:ratatui", "dep:crossterm", "dep:tui-textarea"] -webdav = ["dep:dav-server", "dep:warp", "dep:tokio", "rustic_core/webdav"] +webdav = [ + "dep:dav-server", + "dep:warp", + "dep:tokio", + "rustic_core/webdav", + "dep:bytes", + "dep:futures", +] mount = ["dep:fuse_mt"] [[bin]] @@ -87,6 +94,7 @@ semver = { version = "1", optional = true } simplelog = "0.12" # commands +bytes = { version = "1.8.0", optional = true } bytesize = "1" cached = "0.53.1" clap = { version = "4", features = ["derive", "env", "wrap_help"] } @@ -98,6 +106,7 @@ derive_more = { version = "1", features = ["debug"] } dialoguer = "0.11.0" directories = "5" fuse_mt = { version = "0.6", optional = true } +futures = { version = "0.3.31", optional = true } gethostname = "0.5" globset = "0.4.15" human-panic = "2" diff --git a/src/commands/webdav.rs b/src/commands/webdav.rs index 1b3a65f5d..78dc94f35 100644 --- a/src/commands/webdav.rs +++ b/src/commands/webdav.rs @@ -13,6 +13,9 @@ use dav_server::{warp::dav_handler, DavHandler}; use serde::{Deserialize, Serialize}; use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs}; +use webdavfs::WebDavFS; + +mod webdavfs; #[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] @@ -132,8 +135,9 @@ impl WebDavCmd { |s| s.parse(), )?; + let webdavfs = WebDavFS::new(repo, vfs, file_access); let dav_server = DavHandler::builder() - .filesystem(vfs.into_webdav_fs(repo, file_access)) + .filesystem(Box::new(webdavfs)) .build_handler(); tokio::runtime::Builder::new_current_thread() diff --git a/src/commands/webdav/webdavfs.rs b/src/commands/webdav/webdavfs.rs new file mode 100644 index 000000000..0c3ebb545 --- /dev/null +++ b/src/commands/webdav/webdavfs.rs @@ -0,0 +1,357 @@ +#[cfg(not(windows))] +use std::os::unix::ffi::OsStrExt; +use std::{ + fmt::{Debug, Formatter}, + io::SeekFrom, + sync::{Arc, OnceLock}, + time::SystemTime, +}; + +use bytes::{Buf, Bytes}; +use dav_server::{ + davpath::DavPath, + fs::{ + DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError, FsFuture, FsResult, FsStream, + OpenOptions, ReadDirMeta, + }, +}; +use futures::FutureExt; + +use rustic_core::{ + repofile::Node, + vfs::{FilePolicy, OpenFile, Vfs}, + IndexedFull, Repository, +}; + +fn now() -> SystemTime { + static NOW: OnceLock = OnceLock::new(); + *NOW.get_or_init(SystemTime::now) +} + +/// The inner state of a [`WebDavFS`] instance. +struct DavFsInner { + /// The [`Repository`] to use + repo: Repository, + + /// The [`Vfs`] to use + vfs: Vfs, + + /// The [`FilePolicy`] to use + file_policy: FilePolicy, +} + +impl Debug for DavFsInner { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "DavFS") + } +} + +/// DAV Filesystem implementation. +/// +/// This is the main entry point for the DAV filesystem. +/// It implements [`DavFileSystem`] and can be used to serve a [`Repository`] via DAV. +#[derive(Debug)] +pub struct WebDavFS { + inner: Arc>, +} + +impl WebDavFS { + /// Create a new [`WebDavFS`] instance. + /// + /// # Arguments + /// + /// * `repo` - The [`Repository`] to use + /// * `vfs` - The [`Vfs`] to use + /// * `file_policy` - The [`FilePolicy`] to use + /// + /// # Returns + /// + /// A new [`WebDavFS`] instance + pub(crate) fn new(repo: Repository, vfs: Vfs, file_policy: FilePolicy) -> Self { + let inner = DavFsInner { + repo, + vfs, + file_policy, + }; + + Self { + inner: Arc::new(inner), + } + } + + /// Get a [`Node`] from the specified [`DavPath`]. + /// + /// # Arguments + /// + /// * `path` - The path to get the [`Tree`] at + /// + /// # Errors + /// + /// * If the [`Tree`] could not be found + /// + /// # Returns + /// + /// The [`Node`] at the specified path + /// + /// [`Tree`]: crate::repofile::Tree + fn node_from_path(&self, path: &DavPath) -> Result { + self.inner + .vfs + .node_from_path(&self.inner.repo, &path.as_pathbuf()) + .map_err(|_| FsError::GeneralFailure) + } + + /// Get a list of [`Node`]s from the specified directory path. + /// + /// # Arguments + /// + /// * `path` - The path to get the [`Tree`] at + /// + /// # Errors + /// + /// * If the [`Tree`] could not be found + /// + /// # Returns + /// + /// The list of [`Node`]s at the specified path + /// + /// [`Tree`]: crate::repofile::Tree + fn dir_entries_from_path(&self, path: &DavPath) -> Result, FsError> { + self.inner + .vfs + .dir_entries_from_path(&self.inner.repo, &path.as_pathbuf()) + .map_err(|_| FsError::GeneralFailure) + } +} + +impl Clone for WebDavFS { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl DavFileSystem + for WebDavFS +{ + fn metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'_, Box> { + self.symlink_metadata(davpath) + } + + fn symlink_metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'_, Box> { + async move { + let node = self.node_from_path(davpath)?; + let meta: Box = Box::new(DavFsMetaData(node)); + Ok(meta) + } + .boxed() + } + + fn read_dir<'a>( + &'a self, + davpath: &'a DavPath, + _meta: ReadDirMeta, + ) -> FsFuture<'_, FsStream>> { + async move { + let entries = self.dir_entries_from_path(davpath)?; + let entry_iter = entries.into_iter().map(|e| { + let entry: Box = Box::new(DavFsDirEntry(e)); + Ok(entry) + }); + let strm: FsStream> = Box::pin(futures::stream::iter(entry_iter)); + Ok(strm) + } + .boxed() + } + + fn open<'a>( + &'a self, + path: &'a DavPath, + options: OpenOptions, + ) -> FsFuture<'_, Box> { + async move { + if options.write + || options.append + || options.truncate + || options.create + || options.create_new + { + return Err(FsError::Forbidden); + } + + let node = self.node_from_path(path)?; + if matches!(self.inner.file_policy, FilePolicy::Forbidden) { + return Err(FsError::Forbidden); + } + + let open = self + .inner + .repo + .open_file(&node) + .map_err(|_err| FsError::GeneralFailure)?; + let file: Box = Box::new(DavFsFile { + node, + open, + fs: self.inner.clone(), + seek: 0, + }); + Ok(file) + } + .boxed() + } +} + +/// A [`DavDirEntry`] implementation for [`Node`]s. +#[derive(Clone, Debug)] +struct DavFsDirEntry(Node); + +impl DavDirEntry for DavFsDirEntry { + fn metadata(&self) -> FsFuture<'_, Box> { + async move { + let meta: Box = Box::new(DavFsMetaData(self.0.clone())); + Ok(meta) + } + .boxed() + } + + #[cfg(not(windows))] + fn name(&self) -> Vec { + self.0.name().as_bytes().to_vec() + } + + #[cfg(windows)] + fn name(&self) -> Vec { + self.0 + .name() + .as_os_str() + .to_string_lossy() + .to_string() + .into_bytes() + } +} + +/// A [`DavFile`] implementation for [`Node`]s. +/// +/// This is a read-only file. +struct DavFsFile { + /// The [`Node`] this file is for + node: Node, + + /// The [`OpenFile`] for this file + open: OpenFile, + + /// The [`DavFsInner`] this file belongs to + fs: Arc>, + + /// The current seek position + seek: usize, +} + +impl Debug for DavFsFile { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "DavFile") + } +} + +impl DavFile for DavFsFile { + fn metadata(&mut self) -> FsFuture<'_, Box> { + async move { + let meta: Box = Box::new(DavFsMetaData(self.node.clone())); + Ok(meta) + } + .boxed() + } + + fn write_bytes(&mut self, _buf: Bytes) -> FsFuture<'_, ()> { + async move { Err(FsError::Forbidden) }.boxed() + } + + fn write_buf(&mut self, _buf: Box) -> FsFuture<'_, ()> { + async move { Err(FsError::Forbidden) }.boxed() + } + + fn read_bytes(&mut self, count: usize) -> FsFuture<'_, Bytes> { + async move { + let data = self + .fs + .repo + .read_file_at(&self.open, self.seek, count) + .map_err(|_err| FsError::GeneralFailure)?; + self.seek += data.len(); + Ok(data) + } + .boxed() + } + + fn seek(&mut self, pos: SeekFrom) -> FsFuture<'_, u64> { + async move { + match pos { + SeekFrom::Start(start) => { + self.seek = usize::try_from(start).expect("usize overflow should not happen"); + } + SeekFrom::Current(delta) => { + self.seek = usize::try_from( + i64::try_from(self.seek).expect("i64 wrapped around") + delta, + ) + .expect("usize overflow should not happen"); + } + SeekFrom::End(end) => { + self.seek = usize::try_from( + i64::try_from(self.node.meta.size).expect("i64 wrapped around") + end, + ) + .expect("usize overflow should not happen"); + } + } + + Ok(self.seek as u64) + } + .boxed() + } + + fn flush(&mut self) -> FsFuture<'_, ()> { + async move { Ok(()) }.boxed() + } +} + +/// A [`DavMetaData`] implementation for [`Node`]s. +#[derive(Clone, Debug)] +struct DavFsMetaData(Node); + +impl DavMetaData for DavFsMetaData { + fn len(&self) -> u64 { + self.0.meta.size + } + fn created(&self) -> FsResult { + Ok(now()) + } + fn modified(&self) -> FsResult { + Ok(self.0.meta.mtime.map_or_else(now, SystemTime::from)) + } + fn accessed(&self) -> FsResult { + Ok(self.0.meta.atime.map_or_else(now, SystemTime::from)) + } + + fn status_changed(&self) -> FsResult { + Ok(self.0.meta.ctime.map_or_else(now, SystemTime::from)) + } + + fn is_dir(&self) -> bool { + self.0.is_dir() + } + fn is_file(&self) -> bool { + self.0.is_file() + } + fn is_symlink(&self) -> bool { + self.0.is_symlink() + } + fn executable(&self) -> FsResult { + if self.0.is_file() { + let Some(mode) = self.0.meta.mode else { + return Ok(false); + }; + return Ok((mode & 0o100) > 0); + } + Err(FsError::NotImplemented) + } +}