Add VirtualFs tag-mode WebDAV + MyFiles UI + Admin WebDAV endpoint
- VirtualFs: SQLite-backed virtual folders (tag mode), 16 unit tests - MyFiles module: API endpoints + Web UI for folder/tag management - Admin WebDAV: /admin-webdav/*path with Basic Auth + URI prefix rewrite - CLI: webdav-folder/tag/untag/list/start --virtual-mode commands - Deployed and tested on M5Max48: PROPFIND, PUT, GET, DELETE all working
This commit is contained in:
@@ -19,13 +19,65 @@ pub enum WebdavCommand {
|
||||
port: u16,
|
||||
#[arg(short, long)]
|
||||
user: String,
|
||||
#[arg(long, help = "Enable SQLite virtual directory mode")]
|
||||
virtual_mode: bool,
|
||||
#[arg(long, help = "SQLite database path for virtual directories")]
|
||||
db: Option<String>,
|
||||
},
|
||||
|
||||
#[command(name = "webdav-folder")]
|
||||
Folder {
|
||||
#[arg(long, help = "Action: add, remove")]
|
||||
action: String,
|
||||
#[arg(long, help = "Virtual folder name (e.g. photos)")]
|
||||
name: String,
|
||||
#[arg(long, help = "Description for the folder")]
|
||||
description: Option<String>,
|
||||
#[arg(long, default_value = "data/webdav_virtual.sqlite")]
|
||||
db: String,
|
||||
},
|
||||
|
||||
#[command(name = "webdav-tag")]
|
||||
Tag {
|
||||
#[arg(long, help = "Filename to tag (relative to root)")]
|
||||
file: String,
|
||||
#[arg(long, help = "Tag name (= virtual folder name)")]
|
||||
tag: String,
|
||||
#[arg(long, default_value = "data/webdav_virtual.sqlite")]
|
||||
db: String,
|
||||
},
|
||||
|
||||
#[command(name = "webdav-untag")]
|
||||
Untag {
|
||||
#[arg(long, help = "Filename to untag")]
|
||||
file: String,
|
||||
#[arg(long, help = "Tag name to remove")]
|
||||
tag: String,
|
||||
#[arg(long, default_value = "data/webdav_virtual.sqlite")]
|
||||
db: String,
|
||||
},
|
||||
|
||||
#[command(name = "webdav-list")]
|
||||
List {
|
||||
#[arg(long, help = "List folders, or files in a folder")]
|
||||
what: Option<String>,
|
||||
#[arg(long, help = "Folder name (for listing files in a folder)")]
|
||||
folder: Option<String>,
|
||||
#[arg(long, help = "Filename (for listing tags of a file)")]
|
||||
file: Option<String>,
|
||||
#[arg(long, default_value = "data/webdav_virtual.sqlite")]
|
||||
db: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn handle_webdav_command(cmd: WebdavCommand) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
WebdavCommand::Start { port, user } => {
|
||||
// Parse username and optional password (format: "name:password")
|
||||
WebdavCommand::Start {
|
||||
port,
|
||||
user,
|
||||
virtual_mode,
|
||||
db,
|
||||
} => {
|
||||
let username = user.split(':').next().unwrap_or(&user).to_string();
|
||||
let password = user.split(':').nth(1).map(|s| s.to_string());
|
||||
|
||||
@@ -48,9 +100,103 @@ pub async fn handle_webdav_command(cmd: WebdavCommand) -> anyhow::Result<()> {
|
||||
}
|
||||
println!("Port: {}", port);
|
||||
println!("Home: {}", home_dir.display());
|
||||
if virtual_mode {
|
||||
let db_path = db.clone().unwrap_or_else(|| "data/webdav_virtual.sqlite".to_string());
|
||||
println!("Virtual mode: enabled (db: {})", db_path);
|
||||
}
|
||||
println!();
|
||||
|
||||
run_webdav_server(port, home_dir, username, password).await?;
|
||||
run_webdav_server(port, home_dir, username, password, virtual_mode, db).await?;
|
||||
}
|
||||
|
||||
WebdavCommand::Folder {
|
||||
action,
|
||||
name,
|
||||
description,
|
||||
db,
|
||||
} => {
|
||||
let vfs = crate::vfs::virtual_fs::VirtualFs::new(&db, PathBuf::from("/tmp"))?;
|
||||
let folder = if name.starts_with('/') {
|
||||
name
|
||||
} else {
|
||||
format!("/{}", name)
|
||||
};
|
||||
match action.as_str() {
|
||||
"add" => {
|
||||
let desc = description.unwrap_or_default();
|
||||
vfs.add_folder(&folder, &desc)?;
|
||||
println!("Added virtual folder: {} ({})", folder, desc);
|
||||
}
|
||||
"remove" => {
|
||||
vfs.remove_folder(&folder)?;
|
||||
println!("Removed virtual folder: {}", folder);
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!("Unknown action: {} (use add or remove)", action));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WebdavCommand::Tag { file, tag, db } => {
|
||||
let vfs = crate::vfs::virtual_fs::VirtualFs::new(&db, PathBuf::from("/tmp"))?;
|
||||
vfs.tag_file(&file, &tag)?;
|
||||
println!("Tagged '{}' with '{}'", file, tag);
|
||||
}
|
||||
|
||||
WebdavCommand::Untag { file, tag, db } => {
|
||||
let vfs = crate::vfs::virtual_fs::VirtualFs::new(&db, PathBuf::from("/tmp"))?;
|
||||
vfs.untag_file(&file, &tag)?;
|
||||
println!("Untagged '{}' from '{}'", file, tag);
|
||||
}
|
||||
|
||||
WebdavCommand::List {
|
||||
what,
|
||||
folder,
|
||||
file,
|
||||
db,
|
||||
} => {
|
||||
let vfs = crate::vfs::virtual_fs::VirtualFs::new(&db, PathBuf::from("/tmp"))?;
|
||||
match what.as_deref() {
|
||||
Some("folders") | None => {
|
||||
let folders = vfs.list_folders()?;
|
||||
if folders.is_empty() {
|
||||
println!("No virtual folders.");
|
||||
} else {
|
||||
println!("{:<30} {}", "Folder", "Description");
|
||||
println!("{}", "-".repeat(60));
|
||||
for (f, d) in folders {
|
||||
println!("{:<30} {}", f, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("files") => {
|
||||
let folder_name = folder.ok_or_else(|| anyhow::anyhow!("--folder required for listing files"))?;
|
||||
let files = vfs.list_files_in_folder(&folder_name)?;
|
||||
if files.is_empty() {
|
||||
println!("No files in folder '{}'", folder_name);
|
||||
} else {
|
||||
println!("Files in folder '{}':", folder_name);
|
||||
for f in files {
|
||||
println!(" {}", f);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("tags") => {
|
||||
let filename = file.ok_or_else(|| anyhow::anyhow!("--file required for listing tags"))?;
|
||||
let tags = vfs.list_tags_for_file(&filename)?;
|
||||
if tags.is_empty() {
|
||||
println!("No tags for file '{}'", filename);
|
||||
} else {
|
||||
println!("Tags for file '{}':", filename);
|
||||
for t in tags {
|
||||
println!(" {}", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(other) => {
|
||||
return Err(anyhow::anyhow!("Unknown list type: {} (use folders, files, or tags)", other));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -61,22 +207,36 @@ async fn run_webdav_server(
|
||||
home_dir: PathBuf,
|
||||
user: String,
|
||||
password: Option<String>,
|
||||
virtual_mode: bool,
|
||||
db: Option<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
use axum::{routing::any, Router};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
let vfs = Box::new(crate::vfs::local_fs::LocalFs::new());
|
||||
let upload_hook = None;
|
||||
let dav_handler = if virtual_mode {
|
||||
let db_path = db.unwrap_or_else(|| "data/webdav_virtual.sqlite".to_string());
|
||||
let vfs = crate::vfs::virtual_fs::VirtualFs::new(&db_path, home_dir.clone())?;
|
||||
|
||||
let dav_handler = crate::webdav::create_webdav_handler(vfs, home_dir.clone(), upload_hook, user.clone());
|
||||
let folders = vfs.list_folders().unwrap_or_default();
|
||||
println!("Virtual folders ({}):", folders.len());
|
||||
for (f, d) in &folders {
|
||||
println!(" {} - {}", f, d);
|
||||
}
|
||||
println!("Default root: {}", home_dir.display());
|
||||
println!();
|
||||
let vfs_boxed: Box<dyn crate::vfs::VfsBackend> = Box::new(vfs);
|
||||
crate::webdav::create_webdav_handler_virtual(vfs_boxed, PathBuf::from("/"), None, user.clone())
|
||||
} else {
|
||||
let vfs = Box::new(crate::vfs::local_fs::LocalFs::new());
|
||||
crate::webdav::create_webdav_handler(vfs, home_dir.clone(), None, user.clone())
|
||||
};
|
||||
|
||||
async fn webdav_auth_middleware(
|
||||
req: Request,
|
||||
next: middleware::Next,
|
||||
) -> impl IntoResponse {
|
||||
// Get credentials from extensions (without consuming)
|
||||
let expected = req.extensions().get::<crate::webdav::WebdavCredentials>().cloned();
|
||||
|
||||
|
||||
let auth = req
|
||||
.headers()
|
||||
.get("Authorization")
|
||||
@@ -107,7 +267,8 @@ async fn run_webdav_server(
|
||||
HeaderValue::from_static("Basic realm=\"MarkBase WebDAV\""),
|
||||
)],
|
||||
Body::from("Unauthorized"),
|
||||
).into_response();
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
next.run(req).await
|
||||
@@ -116,19 +277,23 @@ async fn run_webdav_server(
|
||||
let app = Router::new()
|
||||
.route("/", any(handle_dav))
|
||||
.route("/*path", any(handle_dav))
|
||||
.layer(Extension(dav_handler))
|
||||
.layer(middleware::from_fn(webdav_auth_middleware))
|
||||
.layer(Extension(crate::webdav::WebdavCredentials {
|
||||
username: user.clone(),
|
||||
password,
|
||||
}))
|
||||
.layer(Extension(dav_handler))
|
||||
.layer(middleware::from_fn(webdav_auth_middleware));
|
||||
}));
|
||||
|
||||
let addr = format!("0.0.0.0:{}", port);
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
|
||||
println!("WebDAV server listening on http://{}", addr);
|
||||
println!("Root: {}", home_dir.display());
|
||||
println!("User: {}", user);
|
||||
if virtual_mode {
|
||||
println!("Mode: Virtual (SQLite tag-based)");
|
||||
} else {
|
||||
println!("Mode: Local");
|
||||
}
|
||||
println!();
|
||||
println!("Press Ctrl+C to stop");
|
||||
|
||||
@@ -143,4 +308,4 @@ async fn handle_dav(
|
||||
req: Request,
|
||||
) -> impl IntoResponse {
|
||||
dav.handle(req).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user