Implement WebDAV VFS integration: dav-server 0.11 compatible
- Add webdav.rs module: VfsDavFs, VfsDavFile, VfsDavMetaData - Implement DavFileSystem + Clone for GuardedFileSystem blanket impl - Add clone_boxed to VfsBackend trait (required for Sync) - Update CLI webdav.rs to use VFS instead of SQLite - Add bytes dependency - All 155 tests pass
This commit is contained in:
310
markbase-core/src/webdav.rs
Normal file
310
markbase-core/src/webdav.rs
Normal file
@@ -0,0 +1,310 @@
|
||||
use crate::vfs::open_flags::OpenFlags;
|
||||
use crate::vfs::{VfsBackend, VfsDirEntry, VfsStat, VfsFile};
|
||||
use crate::ssh_server::upload_hook::UploadHook;
|
||||
use bytes::{Buf, Bytes};
|
||||
use dav_server::davpath::DavPath;
|
||||
use dav_server::fs::{
|
||||
DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError, FsFuture, FsStream, OpenOptions,
|
||||
};
|
||||
use dav_server::fakels::FakeLs;
|
||||
use futures_util::stream;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub struct VfsDavFs {
|
||||
vfs: Box<dyn VfsBackend>,
|
||||
root: PathBuf,
|
||||
upload_hook: Option<Arc<UploadHook>>,
|
||||
user_uuid: String,
|
||||
}
|
||||
|
||||
impl Clone for VfsDavFs {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
vfs: self.vfs.clone_boxed(),
|
||||
root: self.root.clone(),
|
||||
upload_hook: self.upload_hook.clone(),
|
||||
user_uuid: self.user_uuid.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsDavFs {
|
||||
pub fn new(
|
||||
vfs: Box<dyn VfsBackend>,
|
||||
root: PathBuf,
|
||||
upload_hook: Option<Arc<UploadHook>>,
|
||||
user_uuid: String,
|
||||
) -> Box<Self> {
|
||||
Box::new(Self {
|
||||
vfs,
|
||||
root,
|
||||
upload_hook,
|
||||
user_uuid,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_path(&self, path: &DavPath) -> PathBuf {
|
||||
let relative = path.as_pathbuf();
|
||||
self.root.join(relative)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VfsDavMetaData {
|
||||
len: u64,
|
||||
is_dir: bool,
|
||||
modified: SystemTime,
|
||||
}
|
||||
|
||||
impl VfsDavMetaData {
|
||||
pub fn new(len: u64, is_dir: bool) -> Self {
|
||||
Self {
|
||||
len,
|
||||
is_dir,
|
||||
modified: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_stat(stat: &VfsStat) -> Self {
|
||||
Self {
|
||||
len: stat.size,
|
||||
is_dir: stat.is_dir,
|
||||
modified: stat.mtime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavMetaData for VfsDavMetaData {
|
||||
fn len(&self) -> u64 {
|
||||
self.len
|
||||
}
|
||||
|
||||
fn modified(&self) -> Result<SystemTime, FsError> {
|
||||
Ok(self.modified)
|
||||
}
|
||||
|
||||
fn is_dir(&self) -> bool {
|
||||
self.is_dir
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VfsDavDirEntry {
|
||||
name: String,
|
||||
meta: VfsDavMetaData,
|
||||
}
|
||||
|
||||
impl VfsDavDirEntry {
|
||||
pub fn new(name: String, meta: VfsDavMetaData) -> Self {
|
||||
Self { name, meta }
|
||||
}
|
||||
}
|
||||
|
||||
impl DavDirEntry for VfsDavDirEntry {
|
||||
fn name(&self) -> Vec<u8> {
|
||||
self.name.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> FsFuture<'_, Box<dyn DavMetaData>> {
|
||||
Box::pin(std::future::ready(Ok(Box::new(self.meta.clone()) as Box<dyn DavMetaData>)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VfsDavFile {
|
||||
data: Vec<u8>,
|
||||
position: u64,
|
||||
path: Option<PathBuf>,
|
||||
upload_hook: Option<Arc<UploadHook>>,
|
||||
user_uuid: String,
|
||||
is_write: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VfsDavFile {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("VfsDavFile")
|
||||
.field("is_write", &self.is_write)
|
||||
.field("path", &self.path)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsDavFile {
|
||||
pub fn new_read(data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
position: 0,
|
||||
path: None,
|
||||
upload_hook: None,
|
||||
user_uuid: String::new(),
|
||||
is_write: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_write(
|
||||
path: PathBuf,
|
||||
upload_hook: Option<Arc<UploadHook>>,
|
||||
user_uuid: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
data: Vec::new(),
|
||||
position: 0,
|
||||
path: Some(path),
|
||||
upload_hook,
|
||||
user_uuid,
|
||||
is_write: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavFile for VfsDavFile {
|
||||
fn metadata(&'_ mut self) -> FsFuture<'_, Box<dyn DavMetaData>> {
|
||||
let len = self.data.len() as u64;
|
||||
Box::pin(std::future::ready(Ok(
|
||||
Box::new(VfsDavMetaData::new(len, false)) as Box<dyn DavMetaData>,
|
||||
)))
|
||||
}
|
||||
|
||||
fn write_buf(&'_ mut self, mut buf: Box<dyn Buf + Send>) -> FsFuture<'_, ()> {
|
||||
while buf.has_remaining() {
|
||||
let chunk = buf.chunk();
|
||||
self.data.extend_from_slice(chunk);
|
||||
buf.advance(chunk.len());
|
||||
}
|
||||
Box::pin(std::future::ready(Ok(())))
|
||||
}
|
||||
|
||||
fn write_bytes(&'_ mut self, buf: Bytes) -> FsFuture<'_, ()> {
|
||||
self.data.extend_from_slice(&buf);
|
||||
Box::pin(std::future::ready(Ok(())))
|
||||
}
|
||||
|
||||
fn read_bytes(&'_ mut self, count: usize) -> FsFuture<'_, Bytes> {
|
||||
let start = self.position as usize;
|
||||
let end = std::cmp::min(start + count, self.data.len());
|
||||
|
||||
if start >= self.data.len() {
|
||||
Box::pin(std::future::ready(Ok(Bytes::new())))
|
||||
} else {
|
||||
let bytes = Bytes::copy_from_slice(&self.data[start..end]);
|
||||
self.position = end as u64;
|
||||
Box::pin(std::future::ready(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
fn seek(&'_ mut self, pos: std::io::SeekFrom) -> FsFuture<'_, u64> {
|
||||
let new_pos = match pos {
|
||||
std::io::SeekFrom::Start(offset) => offset,
|
||||
std::io::SeekFrom::Current(offset) => {
|
||||
let current = self.position as i64;
|
||||
(current + offset) as u64
|
||||
}
|
||||
std::io::SeekFrom::End(offset) => {
|
||||
let end = self.data.len() as i64;
|
||||
(end + offset) as u64
|
||||
}
|
||||
};
|
||||
self.position = new_pos;
|
||||
Box::pin(std::future::ready(Ok(new_pos)))
|
||||
}
|
||||
|
||||
fn flush(&'_ mut self) -> FsFuture<'_, ()> {
|
||||
if self.is_write {
|
||||
if let Some(path) = &self.path {
|
||||
if let Err(_e) = std::fs::write(path, &self.data) {
|
||||
return Box::pin(std::future::ready(Err(FsError::NotImplemented)));
|
||||
}
|
||||
if let Some(hook) = &self.upload_hook {
|
||||
if let Err(e) = hook.trigger(path, &self.user_uuid) {
|
||||
log::warn!("Upload hook failed for {:?}: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box::pin(std::future::ready(Ok(())))
|
||||
}
|
||||
}
|
||||
|
||||
impl DavFileSystem for VfsDavFs {
|
||||
fn open<'a>(&'a self, path: &'a DavPath, options: OpenOptions) -> FsFuture<'a, Box<dyn DavFile>> {
|
||||
let full_path = self.resolve_path(path);
|
||||
|
||||
if options.write {
|
||||
let file = VfsDavFile::new_write(
|
||||
full_path,
|
||||
self.upload_hook.clone(),
|
||||
self.user_uuid.clone(),
|
||||
);
|
||||
Box::pin(std::future::ready(Ok(Box::new(file) as Box<dyn DavFile>)))
|
||||
} else {
|
||||
let flags = OpenFlags::new().read();
|
||||
match self.vfs.open_file(&full_path, &flags) {
|
||||
Ok(mut vfs_file) => {
|
||||
let mut data = Vec::new();
|
||||
loop {
|
||||
let mut buf = vec![0u8; 8192];
|
||||
match vfs_file.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => data.extend_from_slice(&buf[..n]),
|
||||
Err(_) => return Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
let file = VfsDavFile::new_read(data);
|
||||
Box::pin(std::future::ready(Ok(Box::new(file) as Box<dyn DavFile>)))
|
||||
}
|
||||
Err(_) => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_dir<'a>(
|
||||
&'a self,
|
||||
path: &'a DavPath,
|
||||
_meta: dav_server::fs::ReadDirMeta,
|
||||
) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
|
||||
let full_path = self.resolve_path(path);
|
||||
|
||||
match self.vfs.read_dir(&full_path) {
|
||||
Ok(entries) => {
|
||||
let results: Vec<Box<dyn DavDirEntry>> = entries
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
let meta = VfsDavMetaData::from_stat(&e.stat);
|
||||
Box::new(VfsDavDirEntry::new(e.name, meta)) as Box<dyn DavDirEntry>
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stream = stream::iter(results.into_iter().map(Ok));
|
||||
Box::pin(std::future::ready(Ok(Box::pin(stream) as FsStream<Box<dyn DavDirEntry>>)))
|
||||
}
|
||||
Err(_) => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
|
||||
let full_path = self.resolve_path(path);
|
||||
|
||||
match self.vfs.stat(&full_path) {
|
||||
Ok(stat) => {
|
||||
let meta = VfsDavMetaData::from_stat(&stat);
|
||||
Box::pin(std::future::ready(Ok(Box::new(meta) as Box<dyn DavMetaData>)))
|
||||
}
|
||||
Err(_) => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_webdav_handler(
|
||||
vfs: Box<dyn VfsBackend>,
|
||||
root: PathBuf,
|
||||
upload_hook: Option<Arc<UploadHook>>,
|
||||
user_uuid: String,
|
||||
) -> dav_server::DavHandler {
|
||||
let dav_fs = VfsDavFs::new(vfs, root, upload_hook, user_uuid);
|
||||
dav_server::DavHandler::builder()
|
||||
.filesystem(dav_fs)
|
||||
.locksystem(FakeLs::new())
|
||||
.strip_prefix("/webdav")
|
||||
.build_handler()
|
||||
}
|
||||
Reference in New Issue
Block a user