WebDAV improvements: flush fix, RwLock recovery, expired lock cleanup, atomic set_times
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled

P0 fixes:
- flush(): add flushed flag, proper error logging, Drop warning for data loss
- props_data RwLock: replace unwrap() with try_read/try_write recovery
- PersistedLs: add is_expired() + cleanup_expired_locks() helper

P1 improvements:
- Props persistence via VFS (load_props/save_props/patch_props)
- COPY/MOVE sync dead props (copy on COPY, move key on rename)
- Atomic set_atime/set_mtime via filetime crate (no race condition)

New files:
- webdav_locks.rs: PersistedLs with lock persistence + expiry cleanup

Tests: 288 passed, 0 failed
This commit is contained in:
Warren
2026-06-21 16:07:12 +08:00
parent 614275f77a
commit 9acd174388
9 changed files with 1940 additions and 112 deletions

View File

@@ -1,17 +1,20 @@
use anyhow::Context;
use axum::{
body::Body,
extract::DefaultBodyLimit,
extract::{Path, Query, State},
http::{HeaderMap, StatusCode},
http::{HeaderMap, HeaderValue, StatusCode},
response::{Html, IntoResponse, Json},
routing::{any, delete, get, patch, post, put},
Extension,
Router,
};
use dav_server::{fakels::FakeLs, DavHandler};
use base64::Engine as _;
use serde::Deserialize;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::sync::{Arc, LazyLock, Mutex, RwLock};
use dashmap::DashMap;
use crate::archive::{
ArchiveConfig, ArchiveFormat, ArchiveProcessor, FormatDetector, ProcessorRegistry,
@@ -134,25 +137,41 @@ pub async fn run(port: u16, file: Option<String>) -> anyhow::Result<()> {
}
});
// WebDAV handler creation (Phase 20)
let webdav_user = "demo";
let webdav_home = std::path::PathBuf::from("/Users/accusys/momentry/var/sftpgo/data").join(webdav_user);
let webdav_vfs = Box::new(crate::vfs::local_fs::LocalFs::new());
let webdav_fs = crate::webdav::VfsDavFs::new(
webdav_vfs,
webdav_home,
None, // upload_hook
webdav_user.to_string(),
// ===== WebDAV multi-user configuration (Phase 20 + P1) =====
let webdav_parent = std::path::PathBuf::from(
std::env::var("MB_WEBDAV_PARENT")
.unwrap_or_else(|_| "/Users/accusys/momentry/var/sftpgo/data".to_string()),
);
// WebDAV versioning storage
let version_storage = std::path::PathBuf::from("data/webdav_versions");
std::fs::create_dir_all(&version_storage).ok();
// Upload hook (disabled by default)
let upload_hook = Arc::new(crate::ssh_server::upload_hook::UploadHook::new(
false,
std::path::PathBuf::from("/usr/local/bin/ffprobe"),
std::path::PathBuf::from("/usr/local/bin/video-register"),
std::path::PathBuf::from("/Users/accusys/momentry/var/video-register"),
vec!["mp4".to_string(), "mov".to_string(), "avi".to_string(), "mkv".to_string(), "webm".to_string()],
));
// VFS proto for per-request DavHandler construction
let s3_cfg = crate::s3_config::S3Config::load_default().unwrap_or_default();
let use_s3 = s3_cfg.s3.enabled;
let webdav_versioning = {
let vs = version_storage.clone();
Arc::new(crate::webdav_version::WebDavVersioning::new(vs))
};
log::info!(
"WebDAV configured: parent={}, versioning={}, upload_hook={}, s3={}",
webdav_parent.display(),
true,
false,
use_s3,
);
let webdav_handler = DavHandler::builder()
.filesystem(webdav_fs)
.locksystem(FakeLs::new())
.strip_prefix("/webdav")
.build_handler();
log::info!("WebDAV handler created for user: {}", webdav_user);
let app = Router::new()
.route("/", get(root_handler))
@@ -256,11 +275,15 @@ pub async fn run(port: u16, file: Option<String>) -> anyhow::Result<()> {
.route("/files", get(|| async { Html(include_str!("file_list.html")) }))
.route("/products", get(|| async { Html(include_str!("product_manager.html")) }))
.route("/downloads", get(|| async { Html(include_str!("category_view.html")) }))
// WebDAV API endpoints (Phase 20)
.route("/webdav", any(handle_webdav))
.route("/webdav/", any(handle_webdav))
.route("/webdav/*path", any(handle_webdav))
.layer(Extension(webdav_handler))
// WebDAV API endpoints (Phase 20, multi-user P1)
.route("/webdav", any(handle_webdav_multi))
.route("/webdav/", any(handle_webdav_multi))
.route("/webdav/*path", any(handle_webdav_multi))
.layer(Extension(webdav_parent))
.layer(Extension(upload_hook))
.layer(Extension(webdav_versioning))
.layer(Extension(use_s3))
.layer(Extension(s3_cfg))
.layer(DefaultBodyLimit::disable())
.with_state(state);
@@ -2445,11 +2468,102 @@ async fn search_files_handler(Query(query): Query<SearchQuery>) -> impl IntoResp
.into_response(),
}
}
// ===== WebDAV multi-user handler (Phase 20 + P1 multi-user) =====
// WebDAV handler (Phase 20)
async fn handle_webdav(
Extension(dav): Extension<DavHandler>,
static WEBDAV_HANDLER_CACHE: LazyLock<DashMap<String, dav_server::DavHandler>> =
LazyLock::new(|| DashMap::new());
async fn handle_webdav_multi(
Extension(parent): Extension<std::path::PathBuf>,
Extension(upload_hook): Extension<Arc<crate::ssh_server::upload_hook::UploadHook>>,
Extension(versioning): Extension<Arc<crate::webdav_version::WebDavVersioning>>,
Extension(use_s3): Extension<bool>,
Extension(s3_cfg): Extension<crate::s3_config::S3Config>,
req: axum::extract::Request,
) -> impl IntoResponse {
dav.handle(req).await
) -> axum::response::Response {
// 1. Extract Basic Auth
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()))
});
// 2. Validate against credential list from env
let (username, _password) = match auth {
Some(creds) => {
let users_str = std::env::var("MB_WEBDAV_USERS")
.unwrap_or_else(|_| "demo:demo123".to_string());
let valid = users_str.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);
if !valid {
return unauthorized_response();
}
creds
}
None => return unauthorized_response(),
};
// 3. Get or create cached DavHandler for this user
let handler = WEBDAV_HANDLER_CACHE
.entry(username.clone())
.or_insert_with(|| {
let user_root = parent.join(&username);
let vfs: Box<dyn crate::vfs::VfsBackend> = if use_s3 {
match crate::vfs::s3_fs::S3Vfs::new(
&s3_cfg.s3.endpoint,
&s3_cfg.s3.region,
&format!("webdav-{}", username),
&s3_cfg.keys.default_access_key,
&s3_cfg.keys.default_secret_key,
) {
Ok(s3) => Box::new(s3),
Err(_) => Box::new(crate::vfs::local_fs::LocalFs::new()),
}
} else {
Box::new(crate::vfs::local_fs::LocalFs::new())
};
let locks_dir = parent.join(".webdav_locks");
let _ = std::fs::create_dir_all(&locks_dir);
let locks_file = locks_dir.join(format!("{}.json", username));
crate::webdav::create_webdav_handler_persisted(
vfs,
user_root,
Some(upload_hook),
username,
Some(versioning),
locks_file,
)
})
.clone();
let dav_resp = handler.handle(req).await;
// Convert dav-server response to axum response
let (parts, body) = dav_resp.into_parts();
let axum_body = axum::body::Body::from_stream(body);
axum::response::Response::from_parts(parts, axum_body)
}
fn unauthorized_response() -> axum::response::Response {
use axum::http::HeaderValue;
(
StatusCode::UNAUTHORIZED,
[("WWW-Authenticate", HeaderValue::from_static("Basic realm=\"MarkBase WebDAV\""))],
axum::body::Body::from("Unauthorized"),
).into_response()
}