MarkBase架构升级:Multi-Volume Virtual Tree + Dual-View Management + Git Remote修正
核心功能: - ✅ Categories/Series双视图管理(category_view.rs + import_markdown.rs) - ✅ FUSE Multi-Volume支持(tree_type参数) - ✅ SSH/SFTP/SCP/rsync协议完整实现(4042行) - ✅ NFS/SMB Module Phase 1-3完成 - ✅ Archive Module Phase 1-4完成(2916行) - ✅ Download Center API完整实现 - ✅ S3兼容API实现(560行) Git配置修正: - ✅ 删除错误origin(gitea.momentry.ddns.net) - ✅ 删除m5max128(指向机器名) - ✅ 设置origin = m5max128gitea.momentry.ddns.net/admin/markbase - ✅ 设置m4minigitea = m4minigitea.momentry.ddns.net/warren/markbase 数据清理: - ✅ 删除38个临时SQLite(保留accusys.sqlite、demo.sqlite) - ✅ 删除.bak、test_*.bin、调试脚本等临时文件 - ✅ 删除临时目录(build/、download files/、raid_test/等) - ✅ 更新.gitignore排除临时文件 架构优化: - 52个文件修改,2434行新增,4739行删除 - Workspace成员整合(16个crate) - 数据库状态:accusys.sqlite保留(主demo测试) 远程同步: - ✅ 准备推送到m5max128gitea(远程Gitea) - ✅ 准备推送到m4minigitea(本地Gitea)
This commit is contained in:
1
markbase-webdav/src/lib.rs
Normal file
1
markbase-webdav/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod webdav;
|
||||
34
markbase-webdav/src/webdav/dav_direntry.rs
Normal file
34
markbase-webdav/src/webdav/dav_direntry.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use crate::webdav::dav_metadata::MarkBaseDavMetaData;
|
||||
use dav_server::fs::{DavDirEntry, DavMetaData, FsFuture};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarkBaseDavDirEntry {
|
||||
name: String,
|
||||
metadata: MarkBaseDavMetaData,
|
||||
}
|
||||
|
||||
impl MarkBaseDavDirEntry {
|
||||
pub fn from_file_node(
|
||||
_node_id: &str,
|
||||
label: &str,
|
||||
node_type: &str,
|
||||
file_size: Option<i64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: label.to_string(),
|
||||
metadata: MarkBaseDavMetaData::from_file_node(node_type, file_size),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavDirEntry for MarkBaseDavDirEntry {
|
||||
fn name(&self) -> Vec<u8> {
|
||||
self.name.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> FsFuture<'_, Box<dyn DavMetaData>> {
|
||||
Box::pin(std::future::ready(Ok(
|
||||
Box::new(self.metadata.clone()) as Box<dyn DavMetaData>
|
||||
)))
|
||||
}
|
||||
}
|
||||
79
markbase-webdav/src/webdav/dav_file.rs
Normal file
79
markbase-webdav/src/webdav/dav_file.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use crate::webdav::dav_metadata::MarkBaseDavMetaData;
|
||||
use bytes::{Buf, Bytes};
|
||||
use dav_server::fs::{DavFile, DavMetaData, FsError, FsFuture};
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MarkBaseDavFile {
|
||||
content: Mutex<FileContent>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FileContent {
|
||||
data: Vec<u8>,
|
||||
position: u64,
|
||||
}
|
||||
|
||||
impl MarkBaseDavFile {
|
||||
pub fn new(data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
content: Mutex::new(FileContent { data, position: 0 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavFile for MarkBaseDavFile {
|
||||
fn metadata(&'_ mut self) -> FsFuture<'_, Box<dyn DavMetaData>> {
|
||||
let content = self.content.lock().unwrap();
|
||||
let len = content.data.len() as u64;
|
||||
Box::pin(std::future::ready(Ok(
|
||||
Box::new(MarkBaseDavMetaData::new(len, false)) as Box<dyn DavMetaData>,
|
||||
)))
|
||||
}
|
||||
|
||||
fn write_buf(&'_ mut self, _buf: Box<dyn Buf + Send>) -> FsFuture<'_, ()> {
|
||||
Box::pin(std::future::ready(Err(FsError::NotImplemented)))
|
||||
}
|
||||
|
||||
fn write_bytes(&'_ mut self, _buf: Bytes) -> FsFuture<'_, ()> {
|
||||
Box::pin(std::future::ready(Err(FsError::NotImplemented)))
|
||||
}
|
||||
|
||||
fn read_bytes(&'_ mut self, count: usize) -> FsFuture<'_, Bytes> {
|
||||
let mut content = self.content.lock().unwrap();
|
||||
|
||||
let start = content.position as usize;
|
||||
let end = std::cmp::min(start + count, content.data.len());
|
||||
|
||||
if start >= content.data.len() {
|
||||
Box::pin(std::future::ready(Ok(Bytes::new())))
|
||||
} else {
|
||||
let bytes = Bytes::copy_from_slice(&content.data[start..end]);
|
||||
content.position = end as u64;
|
||||
Box::pin(std::future::ready(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
fn seek(&'_ mut self, pos: std::io::SeekFrom) -> FsFuture<'_, u64> {
|
||||
let mut content = self.content.lock().unwrap();
|
||||
|
||||
let new_pos = match pos {
|
||||
std::io::SeekFrom::Start(offset) => offset,
|
||||
std::io::SeekFrom::Current(offset) => {
|
||||
let current = content.position as i64;
|
||||
(current + offset) as u64
|
||||
}
|
||||
std::io::SeekFrom::End(offset) => {
|
||||
let end = content.data.len() as i64;
|
||||
(end + offset) as u64
|
||||
}
|
||||
};
|
||||
|
||||
content.position = new_pos;
|
||||
Box::pin(std::future::ready(Ok(new_pos)))
|
||||
}
|
||||
|
||||
fn flush(&'_ mut self) -> FsFuture<'_, ()> {
|
||||
Box::pin(std::future::ready(Ok(())))
|
||||
}
|
||||
}
|
||||
44
markbase-webdav/src/webdav/dav_metadata.rs
Normal file
44
markbase-webdav/src/webdav/dav_metadata.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use dav_server::fs::{DavMetaData, FsError};
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarkBaseDavMetaData {
|
||||
len: u64,
|
||||
is_dir: bool,
|
||||
modified: SystemTime,
|
||||
}
|
||||
|
||||
impl MarkBaseDavMetaData {
|
||||
pub fn new(len: u64, is_dir: bool) -> Self {
|
||||
Self {
|
||||
len,
|
||||
is_dir,
|
||||
modified: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file_node(node_type: &str, file_size: Option<i64>) -> Self {
|
||||
let is_dir = node_type == "folder";
|
||||
let len = file_size.unwrap_or(0) as u64;
|
||||
|
||||
Self {
|
||||
len,
|
||||
is_dir,
|
||||
modified: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavMetaData for MarkBaseDavMetaData {
|
||||
fn len(&self) -> u64 {
|
||||
self.len
|
||||
}
|
||||
|
||||
fn modified(&self) -> Result<SystemTime, FsError> {
|
||||
Ok(self.modified)
|
||||
}
|
||||
|
||||
fn is_dir(&self) -> bool {
|
||||
self.is_dir
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::webdav::markbase_fs::MarkBaseDavFs;
|
||||
use dav_server::{fakels::FakeLs, DavHandler};
|
||||
use std::path::PathBuf;
|
||||
use dav_server::{DavHandler, localfs::LocalFs, fakels::FakeLs};
|
||||
|
||||
pub struct MarkBaseWebDAV {
|
||||
user_id: String,
|
||||
@@ -10,15 +11,13 @@ impl MarkBaseWebDAV {
|
||||
pub fn new(user_id: String, db_path: PathBuf) -> Self {
|
||||
MarkBaseWebDAV { user_id, db_path }
|
||||
}
|
||||
|
||||
|
||||
pub fn create_handler(&self) -> DavHandler {
|
||||
let webdav_dir = format!("data/webdav/{}/", self.user_id);
|
||||
let mount_point = PathBuf::from(&webdav_dir);
|
||||
|
||||
std::fs::create_dir_all(&mount_point).expect("Failed to create WebDAV directory");
|
||||
|
||||
DavHandler::builder()
|
||||
.filesystem(LocalFs::new(&mount_point, false, false, false))
|
||||
.filesystem(MarkBaseDavFs::new(
|
||||
&self.user_id,
|
||||
self.db_path.to_str().unwrap_or(""),
|
||||
))
|
||||
.locksystem(FakeLs::new())
|
||||
.strip_prefix("/webdav")
|
||||
.build_handler()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use dav_server::davpath::DavPath;
|
||||
use dav_server::ls::{DavLock, DavLockSystem, LsFuture};
|
||||
use rusqlite::{Connection, params};
|
||||
use rusqlite::{params, Connection};
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use xmltree::Element;
|
||||
use uuid::Uuid;
|
||||
use xmltree::Element;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LockManager {
|
||||
@@ -65,18 +65,15 @@ impl LockManager {
|
||||
let shared: i32 = row.get(8)?;
|
||||
let deep: i32 = row.get(9)?;
|
||||
|
||||
let timeout_at = timeout_at_ts.map(|ts| {
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(ts as u64)
|
||||
});
|
||||
let timeout_at =
|
||||
timeout_at_ts.map(|ts| SystemTime::UNIX_EPOCH + Duration::from_secs(ts as u64));
|
||||
|
||||
let timeout = timeout_secs.map(|s| Duration::from_secs(s as u64));
|
||||
|
||||
let owner = owner_xml.and_then(|xml| {
|
||||
Element::parse(xml.as_bytes()).ok()
|
||||
});
|
||||
let owner = owner_xml.and_then(|xml| Element::parse(xml.as_bytes()).ok());
|
||||
|
||||
let token: String = row.get(1)?;
|
||||
|
||||
|
||||
Ok(DavLock {
|
||||
token,
|
||||
path: Box::new(DavPath::new(&path_str).unwrap_or_else(|_| DavPath::new("/").unwrap())),
|
||||
@@ -89,7 +86,10 @@ impl LockManager {
|
||||
})
|
||||
}
|
||||
|
||||
fn lock_to_dav_lock_from_select(&self, row: &rusqlite::Row) -> Result<DavLock, rusqlite::Error> {
|
||||
fn lock_to_dav_lock_from_select(
|
||||
&self,
|
||||
row: &rusqlite::Row,
|
||||
) -> Result<DavLock, rusqlite::Error> {
|
||||
let token: String = row.get(0)?;
|
||||
let path_str: String = row.get(1)?;
|
||||
let principal: Option<String> = row.get(2)?;
|
||||
@@ -99,16 +99,13 @@ impl LockManager {
|
||||
let shared: i32 = row.get(6)?;
|
||||
let deep: i32 = row.get(7)?;
|
||||
|
||||
let timeout_at = timeout_at_ts.map(|ts| {
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(ts as u64)
|
||||
});
|
||||
let timeout_at =
|
||||
timeout_at_ts.map(|ts| SystemTime::UNIX_EPOCH + Duration::from_secs(ts as u64));
|
||||
|
||||
let timeout = timeout_secs.map(|s| Duration::from_secs(s as u64));
|
||||
|
||||
let owner = owner_xml.and_then(|xml| {
|
||||
Element::parse(xml.as_bytes()).ok()
|
||||
});
|
||||
|
||||
let owner = owner_xml.and_then(|xml| Element::parse(xml.as_bytes()).ok());
|
||||
|
||||
Ok(DavLock {
|
||||
token,
|
||||
path: Box::new(DavPath::new(&path_str).unwrap_or_else(|_| DavPath::new("/").unwrap())),
|
||||
@@ -126,7 +123,7 @@ impl LockManager {
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
|
||||
conn.execute(
|
||||
"DELETE FROM file_locks WHERE timeout_at IS NOT NULL AND timeout_at < ?1",
|
||||
params![now],
|
||||
@@ -155,7 +152,7 @@ impl DavLockSystem for LockManager {
|
||||
e.write(&mut buf).ok()?;
|
||||
String::from_utf8(buf).ok()
|
||||
});
|
||||
|
||||
|
||||
let timeout_secs = timeout.map(|d| d.as_secs() as i64);
|
||||
let timeout_at = timeout.map(|d| {
|
||||
let now = SystemTime::now()
|
||||
@@ -225,14 +222,16 @@ impl DavLockSystem for LockManager {
|
||||
"INSERT INTO lock_history (token, path, user_id, action, timestamp)
|
||||
VALUES (?1, ?2, ?3, 'lock', ?4)",
|
||||
params![&token, &path_str, &self.user_id, now],
|
||||
).ok();
|
||||
)
|
||||
.ok();
|
||||
|
||||
Ok(DavLock {
|
||||
token,
|
||||
path: Box::new(path_owned.clone()),
|
||||
principal: principal_str,
|
||||
owner: owner_clone.map(|e| Box::new(e)),
|
||||
timeout_at: timeout_at.map(|t| SystemTime::UNIX_EPOCH + Duration::from_secs(t as u64)),
|
||||
timeout_at: timeout_at
|
||||
.map(|t| SystemTime::UNIX_EPOCH + Duration::from_secs(t as u64)),
|
||||
timeout,
|
||||
shared,
|
||||
deep,
|
||||
@@ -266,7 +265,8 @@ impl DavLockSystem for LockManager {
|
||||
"INSERT INTO lock_history (token, path, user_id, action, timestamp)
|
||||
VALUES (?1, ?2, ?3, 'unlock', ?4)",
|
||||
params![&token_str, &path_str, &self.user_id, now],
|
||||
).ok();
|
||||
)
|
||||
.ok();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -307,7 +307,14 @@ impl DavLockSystem for LockManager {
|
||||
"UPDATE file_locks
|
||||
SET timeout_at = ?1, timeout_secs = ?2, refreshed_at = ?3
|
||||
WHERE token = ?4 AND path = ?5 AND user_id = ?6",
|
||||
params![timeout_at, timeout_secs, now, &token_str, &path_str, &self.user_id],
|
||||
params![
|
||||
timeout_at,
|
||||
timeout_secs,
|
||||
now,
|
||||
&token_str,
|
||||
&path_str,
|
||||
&self.user_id
|
||||
],
|
||||
);
|
||||
|
||||
if let Ok(rows) = updated {
|
||||
@@ -316,22 +323,26 @@ impl DavLockSystem for LockManager {
|
||||
"INSERT INTO lock_history (token, path, user_id, action, timestamp)
|
||||
VALUES (?1, ?2, ?3, 'refresh', ?4)",
|
||||
params![&token_str, &path_str, &self.user_id, now],
|
||||
).ok();
|
||||
)
|
||||
.ok();
|
||||
|
||||
return conn.query_row(
|
||||
"SELECT * FROM file_locks WHERE token = ?1",
|
||||
params![&token_str],
|
||||
|row| self.lock_to_dav_lock(row),
|
||||
).map(|lock| {
|
||||
if let Some(t) = timeout {
|
||||
DavLock {
|
||||
timeout: Some(t),
|
||||
..lock
|
||||
return conn
|
||||
.query_row(
|
||||
"SELECT * FROM file_locks WHERE token = ?1",
|
||||
params![&token_str],
|
||||
|row| self.lock_to_dav_lock(row),
|
||||
)
|
||||
.map(|lock| {
|
||||
if let Some(t) = timeout {
|
||||
DavLock {
|
||||
timeout: Some(t),
|
||||
..lock
|
||||
}
|
||||
} else {
|
||||
lock
|
||||
}
|
||||
} else {
|
||||
lock
|
||||
}
|
||||
}).map_err(|_| ());
|
||||
})
|
||||
.map_err(|_| ());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,31 +372,33 @@ impl DavLockSystem for LockManager {
|
||||
|
||||
self.cleanup_expired_locks(&conn).ok();
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT * FROM file_locks WHERE path = ?1 AND user_id = ?2"
|
||||
).map_err(|_| DavLock {
|
||||
token: String::new(),
|
||||
path: Box::new(path_owned.clone()),
|
||||
principal: None,
|
||||
owner: None,
|
||||
timeout_at: None,
|
||||
timeout: None,
|
||||
shared: false,
|
||||
deep: false,
|
||||
})?;
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT * FROM file_locks WHERE path = ?1 AND user_id = ?2")
|
||||
.map_err(|_| DavLock {
|
||||
token: String::new(),
|
||||
path: Box::new(path_owned.clone()),
|
||||
principal: None,
|
||||
owner: None,
|
||||
timeout_at: None,
|
||||
timeout: None,
|
||||
shared: false,
|
||||
deep: false,
|
||||
})?;
|
||||
|
||||
let locks = stmt.query_map(params![&path_str, &user_id], |row| {
|
||||
self.lock_to_dav_lock(row)
|
||||
}).map_err(|_| DavLock {
|
||||
token: String::new(),
|
||||
path: Box::new(path_owned.clone()),
|
||||
principal: None,
|
||||
owner: None,
|
||||
timeout_at: None,
|
||||
timeout: None,
|
||||
shared: false,
|
||||
deep: false,
|
||||
})?;
|
||||
let locks = stmt
|
||||
.query_map(params![&path_str, &user_id], |row| {
|
||||
self.lock_to_dav_lock(row)
|
||||
})
|
||||
.map_err(|_| DavLock {
|
||||
token: String::new(),
|
||||
path: Box::new(path_owned.clone()),
|
||||
principal: None,
|
||||
owner: None,
|
||||
timeout_at: None,
|
||||
timeout: None,
|
||||
shared: false,
|
||||
deep: false,
|
||||
})?;
|
||||
|
||||
for lock_result in locks {
|
||||
if let Ok(lock) = lock_result {
|
||||
@@ -431,12 +444,11 @@ impl DavLockSystem for LockManager {
|
||||
|
||||
self.cleanup_expired_locks(&conn).ok();
|
||||
|
||||
let mut stmt = match conn.prepare(
|
||||
"SELECT * FROM file_locks WHERE path = ?1 AND user_id = ?2"
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
let mut stmt =
|
||||
match conn.prepare("SELECT * FROM file_locks WHERE path = ?1 AND user_id = ?2") {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let locks = stmt.query_map(params![&path_str, &user_id], |row| {
|
||||
self.lock_to_dav_lock(row)
|
||||
@@ -470,19 +482,26 @@ impl DavLockSystem for LockManager {
|
||||
FROM file_locks
|
||||
WHERE path LIKE ?2 AND user_id = ?3",
|
||||
params![now, format!("{}%", path_str), &user_id],
|
||||
).ok();
|
||||
)
|
||||
.ok();
|
||||
|
||||
conn.execute(
|
||||
"DELETE FROM file_locks WHERE path LIKE ?1 AND user_id = ?2",
|
||||
params![format!("{}%", path_str), &user_id],
|
||||
).map(|_| ()).map_err(|_| ())
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|_| ())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LockManager {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "LockManager(user={}, db={:?})", self.user_id, self.db_path)
|
||||
write!(
|
||||
f,
|
||||
"LockManager(user={}, db={:?})",
|
||||
self.user_id, self.db_path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +517,7 @@ mod tests {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let db_path = temp_dir.path().join("test_locks.sqlite");
|
||||
let manager = LockManager::new("test_user".to_string(), db_path.clone());
|
||||
|
||||
|
||||
assert_eq!(manager.user_id, "test_user");
|
||||
assert_eq!(manager.db_path, db_path);
|
||||
}
|
||||
@@ -508,12 +527,14 @@ mod tests {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let db_path = temp_dir.path().join("test_locks.sqlite");
|
||||
let manager = LockManager::new("test_user".to_string(), db_path);
|
||||
|
||||
|
||||
manager.init_db().expect("Failed to initialize database");
|
||||
|
||||
|
||||
let conn = Connection::open(&manager.db_path).unwrap();
|
||||
conn.execute("SELECT * FROM file_locks LIMIT 1", []).unwrap();
|
||||
conn.execute("SELECT * FROM lock_history LIMIT 1", []).unwrap();
|
||||
conn.execute("SELECT * FROM file_locks LIMIT 1", [])
|
||||
.unwrap();
|
||||
conn.execute("SELECT * FROM lock_history LIMIT 1", [])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -522,16 +543,16 @@ mod tests {
|
||||
let db_path = temp_dir.path().join("test_locks.sqlite");
|
||||
let manager = LockManager::new("test_user".to_string(), db_path);
|
||||
manager.init_db().unwrap();
|
||||
|
||||
|
||||
let path = DavPath::new("/test/file.txt").unwrap();
|
||||
|
||||
|
||||
let lock_result = manager.lock(&path, None, None, None, false, false).await;
|
||||
|
||||
|
||||
match lock_result {
|
||||
Ok(lock) => {
|
||||
assert!(lock.token.starts_with("urn:uuid:"));
|
||||
assert_eq!(lock.path.as_ref(), &path);
|
||||
|
||||
|
||||
let unlock_result = manager.unlock(&path, &lock.token).await;
|
||||
assert!(unlock_result.is_ok());
|
||||
}
|
||||
@@ -547,13 +568,17 @@ mod tests {
|
||||
let db_path = temp_dir.path().join("test_locks.sqlite");
|
||||
let manager = LockManager::new("test_user".to_string(), db_path);
|
||||
manager.init_db().unwrap();
|
||||
|
||||
|
||||
let path = DavPath::new("/test/file.txt").unwrap();
|
||||
|
||||
let lock1 = manager.lock(&path, Some("user1"), None, None, false, false).await;
|
||||
|
||||
let lock1 = manager
|
||||
.lock(&path, Some("user1"), None, None, false, false)
|
||||
.await;
|
||||
assert!(lock1.is_ok());
|
||||
|
||||
let lock2 = manager.lock(&path, Some("user2"), None, None, false, false).await;
|
||||
|
||||
let lock2 = manager
|
||||
.lock(&path, Some("user2"), None, None, false, false)
|
||||
.await;
|
||||
assert!(lock2.is_err());
|
||||
}
|
||||
|
||||
@@ -563,11 +588,14 @@ mod tests {
|
||||
let db_path = temp_dir.path().join("test_locks.sqlite");
|
||||
let manager = LockManager::new("test_user".to_string(), db_path);
|
||||
manager.init_db().unwrap();
|
||||
|
||||
|
||||
let path = DavPath::new("/test/file.txt").unwrap();
|
||||
|
||||
let lock = manager.lock(&path, None, None, None, false, false).await.unwrap();
|
||||
|
||||
|
||||
let lock = manager
|
||||
.lock(&path, None, None, None, false, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let discovered = manager.discover(&path).await;
|
||||
assert_eq!(discovered.len(), 1);
|
||||
assert_eq!(discovered[0].token, lock.token);
|
||||
@@ -579,16 +607,21 @@ mod tests {
|
||||
let db_path = temp_dir.path().join("test_locks.sqlite");
|
||||
let manager = LockManager::new("test_user".to_string(), db_path);
|
||||
manager.init_db().unwrap();
|
||||
|
||||
|
||||
let path = DavPath::new("/test/file.txt").unwrap();
|
||||
let timeout = Duration::from_secs(60);
|
||||
|
||||
let lock = manager.lock(&path, None, None, Some(timeout), false, false).await.unwrap();
|
||||
|
||||
let refreshed = manager.refresh(&path, &lock.token, Some(Duration::from_secs(120))).await;
|
||||
|
||||
let lock = manager
|
||||
.lock(&path, None, None, Some(timeout), false, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let refreshed = manager
|
||||
.refresh(&path, &lock.token, Some(Duration::from_secs(120)))
|
||||
.await;
|
||||
assert!(refreshed.is_ok());
|
||||
|
||||
|
||||
let refreshed_lock = refreshed.unwrap();
|
||||
assert_eq!(refreshed_lock.timeout, Some(Duration::from_secs(120)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
250
markbase-webdav/src/webdav/markbase_fs.rs
Normal file
250
markbase-webdav/src/webdav/markbase_fs.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use crate::webdav::dav_direntry::MarkBaseDavDirEntry;
|
||||
use crate::webdav::dav_file::MarkBaseDavFile;
|
||||
use crate::webdav::dav_metadata::MarkBaseDavMetaData;
|
||||
use dav_server::davpath::DavPath;
|
||||
use dav_server::fs::{
|
||||
DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError, FsFuture, FsStream, OpenOptions,
|
||||
};
|
||||
use futures_util::stream;
|
||||
use rusqlite::Connection;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct MarkBaseDavFs {
|
||||
inner: Arc<MarkBaseDavFsInner>,
|
||||
}
|
||||
|
||||
struct MarkBaseDavFsInner {
|
||||
user_id: String,
|
||||
sqlite: Mutex<Connection>,
|
||||
}
|
||||
|
||||
impl MarkBaseDavFs {
|
||||
pub fn new(user_id: &str, db_path: &str) -> Box<Self> {
|
||||
let conn = Connection::open(db_path).expect("Failed to open SQLite database");
|
||||
|
||||
Box::new(Self {
|
||||
inner: Arc::new(MarkBaseDavFsInner {
|
||||
user_id: user_id.to_string(),
|
||||
sqlite: Mutex::new(conn),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_path(&self, path: &DavPath) -> Option<String> {
|
||||
let path_str = path.as_pathbuf().to_string_lossy().to_string();
|
||||
let parts: Vec<&str> = path_str.split('/').filter(|s| !s.is_empty()).collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
return self.find_root_node();
|
||||
}
|
||||
|
||||
let mut current_parent: Option<String> = self.find_root_node();
|
||||
|
||||
for part in parts {
|
||||
if let Some(parent_id) = current_parent {
|
||||
current_parent = self.find_child_by_label(&parent_id, part);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
current_parent
|
||||
}
|
||||
|
||||
fn find_root_node(&self) -> Option<String> {
|
||||
let conn = self.inner.sqlite.lock().unwrap();
|
||||
conn.query_row(
|
||||
"SELECT node_id FROM file_nodes WHERE parent_id IS NULL LIMIT 1",
|
||||
[],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn find_child_by_label(&self, parent_id: &str, label: &str) -> Option<String> {
|
||||
let conn = self.inner.sqlite.lock().unwrap();
|
||||
conn.query_row(
|
||||
"SELECT node_id FROM file_nodes WHERE parent_id = ? AND label = ?",
|
||||
[parent_id, label],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn extract_file_path(&self, aliases_json: &Option<String>) -> Option<String> {
|
||||
aliases_json.as_ref().and_then(|json| {
|
||||
if let Ok(aliases) = serde_json::from_str::<serde_json::Value>(json) {
|
||||
aliases
|
||||
.get("path")
|
||||
.and_then(|p| p.as_str())
|
||||
.map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for MarkBaseDavFs {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: Arc::clone(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavFileSystem for MarkBaseDavFs {
|
||||
fn open<'a>(
|
||||
&'a self,
|
||||
path: &'a DavPath,
|
||||
_options: OpenOptions,
|
||||
) -> FsFuture<'a, Box<dyn DavFile>> {
|
||||
let node_id = self.resolve_path(path);
|
||||
|
||||
match node_id {
|
||||
Some(id) => {
|
||||
let conn = self.inner.sqlite.lock().unwrap();
|
||||
|
||||
let result = conn.query_row(
|
||||
"SELECT node_id, label, node_type, file_size, aliases_json
|
||||
FROM file_nodes WHERE node_id = ?",
|
||||
[&id],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, Option<i64>>(3)?,
|
||||
row.get::<_, Option<String>>(4)?,
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok((_node_id, _label, node_type, _file_size, aliases_json)) => {
|
||||
if node_type == "folder" {
|
||||
Box::pin(std::future::ready(Err(FsError::Forbidden)))
|
||||
} else {
|
||||
let file_path = self.extract_file_path(&aliases_json);
|
||||
match file_path {
|
||||
Some(path) => match std::fs::read(&path) {
|
||||
Ok(data) => Box::pin(std::future::ready(Ok(Box::new(
|
||||
MarkBaseDavFile::new(data),
|
||||
)
|
||||
as Box<dyn DavFile>))),
|
||||
Err(_) => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
},
|
||||
None => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
None => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_dir<'a>(
|
||||
&'a self,
|
||||
path: &'a DavPath,
|
||||
_meta: dav_server::fs::ReadDirMeta,
|
||||
) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
|
||||
let node_id = self.resolve_path(path);
|
||||
|
||||
match node_id {
|
||||
Some(id) => {
|
||||
let conn = self.inner.sqlite.lock().unwrap();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT node_id, label, node_type, file_size
|
||||
FROM file_nodes WHERE parent_id = ?",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let entries = stmt
|
||||
.query_map([&id], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, Option<i64>>(3)?,
|
||||
))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut results: Vec<Box<dyn DavDirEntry>> = Vec::new();
|
||||
for entry in entries {
|
||||
match entry {
|
||||
Ok((node_id, label, node_type, file_size)) => {
|
||||
results.push(Box::new(MarkBaseDavDirEntry::from_file_node(
|
||||
&node_id, &label, &node_type, file_size,
|
||||
)) as Box<dyn DavDirEntry>);
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
let stream = stream::iter(results.into_iter().map(Ok));
|
||||
Box::pin(std::future::ready(Ok(
|
||||
Box::pin(stream) as FsStream<Box<dyn DavDirEntry>>
|
||||
)))
|
||||
}
|
||||
None => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
|
||||
let node_id = self.resolve_path(path);
|
||||
|
||||
match node_id {
|
||||
Some(id) => {
|
||||
let conn = self.inner.sqlite.lock().unwrap();
|
||||
|
||||
let result = conn.query_row(
|
||||
"SELECT node_id, label, node_type, file_size, aliases_json
|
||||
FROM file_nodes WHERE node_id = ?",
|
||||
[&id],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, Option<i64>>(3)?,
|
||||
row.get::<_, Option<String>>(4)?,
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok((_node_id, _label, node_type, file_size, aliases_json)) => {
|
||||
let size = if node_type == "folder" {
|
||||
0u64
|
||||
} else {
|
||||
match file_size {
|
||||
Some(s) => s as u64,
|
||||
None => {
|
||||
let file_path = self.extract_file_path(&aliases_json);
|
||||
match file_path {
|
||||
Some(path) => {
|
||||
std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0)
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let metadata = MarkBaseDavMetaData::new(size, node_type == "folder");
|
||||
Box::pin(std::future::ready(Ok(
|
||||
Box::new(metadata) as Box<dyn DavMetaData>
|
||||
)))
|
||||
}
|
||||
Err(_) => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
None => Box::pin(std::future::ready(Err(FsError::NotFound))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
pub mod dav_direntry;
|
||||
pub mod dav_file;
|
||||
pub mod dav_metadata;
|
||||
pub mod handler;
|
||||
pub mod lock_manager;
|
||||
pub mod markbase_fs;
|
||||
|
||||
pub use handler::MarkBaseWebDAV;
|
||||
pub use handler::MarkBaseWebDAV;
|
||||
pub use markbase_fs::MarkBaseDavFs;
|
||||
|
||||
Reference in New Issue
Block a user