Skip to content

Commit

Permalink
chore: move webdavfs from rustic_core to rustic
Browse files Browse the repository at this point in the history
  • Loading branch information
aawsome committed Nov 23, 2024
1 parent c3e2557 commit 2405f9a
Show file tree
Hide file tree
Showing 4 changed files with 362 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ open = "5.3.0"
self_update = { version = "0.39.0", default-features = false, optional = true, features = ["rustls", "archive-tar", "compression-flate2"] } # FIXME: Downgraded to 0.39.0 due to https://github.com/jaemk/self_update/issues/136
tar = "0.4.42"
toml = "0.8"
bytes = "1.8.0"
futures = "0.3.31"

[dev-dependencies]
abscissa_core = { version = "0.8.1", default-features = false, features = ["testing"] }
Expand Down
6 changes: 5 additions & 1 deletion src/commands/webdav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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()
Expand Down
353 changes: 353 additions & 0 deletions src/commands/webdav/webdavfs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
use std::fmt::{Debug, Formatter};
use std::io::SeekFrom;
#[cfg(not(windows))]
use std::os::unix::ffi::OsStrExt;
use std::sync::{Arc, OnceLock};
use std::time::SystemTime;

use bytes::{Buf, Bytes};
use futures::FutureExt;
use rustic_core::repofile::Node;
use rustic_core::vfs::{FilePolicy, OpenFile, Vfs};
use rustic_core::{IndexedFull, Repository};

use dav_server::{
davpath::DavPath,
fs::{
DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError, FsFuture, FsResult, FsStream,
OpenOptions, ReadDirMeta,
},
};

fn now() -> SystemTime {
static NOW: OnceLock<SystemTime> = OnceLock::new();
*NOW.get_or_init(SystemTime::now)
}

/// The inner state of a [`WebDavFS`] instance.
struct DavFsInner<P, S> {
/// The [`Repository`] to use
repo: Repository<P, S>,

/// The [`Vfs`] to use
vfs: Vfs,

/// The [`FilePolicy`] to use
file_policy: FilePolicy,
}

impl<P, S> Debug for DavFsInner<P, S> {
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<P, S> {
inner: Arc<DavFsInner<P, S>>,
}

impl<P, S: IndexedFull> WebDavFS<P, S> {
/// 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<P, S>, 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<Node, FsError> {
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<Vec<Node>, FsError> {
self.inner
.vfs
.dir_entries_from_path(&self.inner.repo, &path.as_pathbuf())
.map_err(|_| FsError::GeneralFailure)
}
}

impl<P, S: IndexedFull> Clone for WebDavFS<P, S> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}

impl<P: Debug + Send + Sync + 'static, S: IndexedFull + Debug + Send + Sync + 'static> DavFileSystem
for WebDavFS<P, S>
{
fn metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'_, Box<dyn DavMetaData>> {
self.symlink_metadata(davpath)
}

fn symlink_metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'_, Box<dyn DavMetaData>> {
async move {
let node = self.node_from_path(davpath)?;
let meta: Box<dyn DavMetaData> = Box::new(DavFsMetaData(node));
Ok(meta)
}
.boxed()
}

fn read_dir<'a>(
&'a self,
davpath: &'a DavPath,
_meta: ReadDirMeta,
) -> FsFuture<'_, FsStream<Box<dyn DavDirEntry>>> {
async move {
let entries = self.dir_entries_from_path(davpath)?;
let entry_iter = entries.into_iter().map(|e| {
let entry: Box<dyn DavDirEntry> = Box::new(DavFsDirEntry(e));
Ok(entry)
});
let strm: FsStream<Box<dyn DavDirEntry>> = Box::pin(futures::stream::iter(entry_iter));
Ok(strm)
}
.boxed()
}

fn open<'a>(
&'a self,
path: &'a DavPath,
options: OpenOptions,
) -> FsFuture<'_, Box<dyn DavFile>> {
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<dyn DavFile> = 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<dyn DavMetaData>> {
async move {
let meta: Box<dyn DavMetaData> = Box::new(DavFsMetaData(self.0.clone()));
Ok(meta)
}
.boxed()
}

#[cfg(not(windows))]
fn name(&self) -> Vec<u8> {
self.0.name().as_bytes().to_vec()
}

#[cfg(windows)]
fn name(&self) -> Vec<u8> {
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<P, S> {
/// The [`Node`] this file is for
node: Node,

/// The [`OpenFile`] for this file
open: OpenFile,

/// The [`DavFsInner`] this file belongs to
fs: Arc<DavFsInner<P, S>>,

/// The current seek position
seek: usize,
}

impl<P, S> Debug for DavFsFile<P, S> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "DavFile")
}
}

impl<P: Debug + Send + Sync, S: IndexedFull + Debug + Send + Sync> DavFile for DavFsFile<P, S> {
fn metadata(&mut self) -> FsFuture<'_, Box<dyn DavMetaData>> {
async move {
let meta: Box<dyn DavMetaData> = 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<dyn Buf + Send>) -> 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<SystemTime> {
Ok(now())
}
fn modified(&self) -> FsResult<SystemTime> {
Ok(self.0.meta.mtime.map_or_else(now, SystemTime::from))
}
fn accessed(&self) -> FsResult<SystemTime> {
Ok(self.0.meta.atime.map_or_else(now, SystemTime::from))
}

fn status_changed(&self) -> FsResult<SystemTime> {
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<bool> {
if self.0.is_file() {
let Some(mode) = self.0.meta.mode else {
return Ok(false);
};
return Ok((mode & 0o100) > 0);
}
Err(FsError::NotImplemented)
}
}

0 comments on commit 2405f9a

Please sign in to comment.