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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user