WebDAV improvements: flush fix, RwLock recovery, expired lock cleanup, atomic set_times
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:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user