Add VirtualFs tag-mode WebDAV + MyFiles UI + Admin WebDAV endpoint
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- 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:
Warren
2026-06-22 10:38:25 +08:00
parent 37d0fe1a3c
commit 60e4329eed
7 changed files with 1596 additions and 39 deletions

View File

@@ -290,6 +290,17 @@ pub async fn run(port: u16, file: Option<String>) -> anyhow::Result<()> {
.route("/webdav", any(handle_webdav_multi))
.route("/webdav/", any(handle_webdav_multi))
.route("/webdav/*path", any(handle_webdav_multi))
// Admin WebDAV (browse all user directories)
.route("/admin-webdav", any(handle_webdav_admin))
.route("/admin-webdav/", any(handle_webdav_admin))
.route("/admin-webdav/*path", any(handle_webdav_admin))
// MyFiles User Interface (Web UI + API)
.route("/myfiles", get(crate::myfiles::ui_page))
.route("/api/v2/myfiles/:username/folders", get(crate::myfiles::list_folders).post(crate::myfiles::create_folder))
.route("/api/v2/myfiles/:username/folders/:folder_name", delete(crate::myfiles::delete_folder))
.route("/api/v2/myfiles/:username/files", get(crate::myfiles::list_files))
.route("/api/v2/myfiles/:username/tags", post(crate::myfiles::add_tag).delete(crate::myfiles::remove_tag))
.route("/api/v2/myfiles/:username/files/:filename/tags", get(crate::myfiles::file_tags))
.layer(Extension(webdav_parent))
.layer(Extension(upload_hook))
.layer(Extension(webdav_versioning))
@@ -2624,3 +2635,86 @@ fn unauthorized_response() -> axum::response::Response {
axum::body::Body::from("Unauthorized"),
).into_response()
}
static ADMIN_WEBDAV_HANDLER: LazyLock<Option<dav_server::DavHandler>> = LazyLock::new(|| {
let parent = std::env::var("MB_WEBDAV_PARENT")
.unwrap_or_else(|_| "/Users/accusys/momentry/var/sftpgo/data".to_string());
let parent_path = std::path::PathBuf::from(&parent);
if !parent_path.exists() {
return None;
}
let vfs: Box<dyn crate::vfs::VfsBackend> = Box::new(crate::vfs::local_fs::LocalFs::new());
let locks_dir = parent_path.join(".webdav_locks");
let _ = std::fs::create_dir_all(&locks_dir);
let locks_file = locks_dir.join("admin.json");
Some(crate::webdav::create_webdav_handler_persisted(
vfs,
parent_path,
None,
"admin".to_string(),
None,
locks_file,
))
});
async fn handle_webdav_admin(
Extension(upload_hook): Extension<Arc<crate::ssh_server::upload_hook::UploadHook>>,
req: axum::extract::Request,
) -> axum::response::Response {
let admin_users = std::env::var("MB_WEBDAV_ADMIN_USERS")
.unwrap_or_else(|_| "admin:admin123".to_string());
let auth = req
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.filter(|v| v.starts_with("Basic "))
.and_then(|v| {
let encoded = &v[6..];
let decoded = base64::engine::general_purpose::STANDARD.decode(encoded).ok()?;
let creds = String::from_utf8(decoded).ok()?;
let colon = creds.find(':')?;
Some((creds[..colon].to_string(), creds[colon + 1..].to_string()))
});
let valid = match auth {
Some(ref creds) => {
admin_users.split(',')
.filter_map(|entry| {
let mut parts = entry.splitn(2, ':');
let u = parts.next()?.to_string();
let p = parts.next().unwrap_or("").to_string();
Some((u, p))
})
.any(|(u, p)| u == creds.0 && p == creds.1)
}
None => false,
};
if !valid {
return unauthorized_response();
}
let handler = match ADMIN_WEBDAV_HANDLER.as_ref() {
Some(h) => h.clone(),
None => return unauthorized_response(),
};
let (mut parts, body) = req.into_parts();
let new_path = parts.uri.path().strip_prefix("/admin-webdav").unwrap_or("/");
let new_path = if new_path.is_empty() || !new_path.starts_with('/') {
format!("/{}", new_path)
} else {
new_path.to_string()
};
let builder = axum::http::Uri::builder().path_and_query(new_path.as_str());
if let Ok(uri) = builder.build() {
parts.uri = uri;
}
let req = axum::http::Request::from_parts(parts, body);
let dav_resp = handler.handle(req).await;
let (parts, body) = dav_resp.into_parts();
let axum_body = axum::body::Body::from_stream(body);
axum::response::Response::from_parts(parts, axum_body)
}