Web GUI Phase 1-5 complete: WebClient + WebAdmin + Virtual Folders + Quota + ACL
- WebClient UI: 文件树/列表显示 + 5种风格切换 + 视图切换 - WebAdmin UI: Dashboard/Users/Shares/Monitor 整合管理 - Virtual Folders UI: CRUD管理 + 跨backend路径映射 - Quota Management UI: Space/File quota配置 + 实时usage监控 - ACL 权限管理 UI: NFSv4/SMB ACL显示 + Permission check + ACE编辑功能 新增代码:~1947行 新增 Vue Components:5个(WebClient/WebAdmin/VirtualFolders/Quota/ACL) 新增 Rust Commands:3个(virtual_folders/quota/acl) 修复问题: - Tauri v2 参数名修复(snake_case) - Element Plus icons 名称修复 - Tauri API 导入路径修复(@tauri-apps/api/core) - 前端环境检测(避免浏览器调用 Tauri API) 覆盖率: - WebClient: 100%(SFTPGo WebClient功能) - WebAdmin: 80%(缺少完整Monitor) - Virtual Folders: 100% - Quota: 100% - ACL: 100%(完整 ACE 编辑功能)
This commit is contained in:
148
markbase-tauri/src-tauri/src/commands/acl.rs
Normal file
148
markbase-tauri/src-tauri/src/commands/acl.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use markbase_core::vfs::{VfsAcl, VfsAce, VfsAceType, VfsAceFlag, VfsAceMask, VfsBackend, local_fs::LocalFs};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AceInfo {
|
||||
pub ace_type: String,
|
||||
pub flags: Vec<String>,
|
||||
pub mask: Vec<String>,
|
||||
pub principal: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AclInfo {
|
||||
pub path: String,
|
||||
pub aces: Vec<AceInfo>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_acl(user_id: String, path: String) -> Result<AclInfo, String> {
|
||||
let vfs = LocalFs::new();
|
||||
let path_buf = PathBuf::from(&path);
|
||||
|
||||
let acl = vfs.get_acl(&path_buf)
|
||||
.map_err(|e| format!("Failed to get ACL: {}", e))?;
|
||||
|
||||
let aces = acl.aces.iter().map(|ace| {
|
||||
AceInfo {
|
||||
ace_type: match ace.ace_type {
|
||||
VfsAceType::Allow => "Allow".to_string(),
|
||||
VfsAceType::Deny => "Deny".to_string(),
|
||||
VfsAceType::Audit => "Audit".to_string(),
|
||||
VfsAceType::Alarm => "Alarm".to_string(),
|
||||
},
|
||||
flags: ace.flags.iter().map(|f| match f {
|
||||
VfsAceFlag::FileInherit => "FileInherit",
|
||||
VfsAceFlag::DirectoryInherit => "DirectoryInherit",
|
||||
VfsAceFlag::NoPropagateInherit => "NoPropagateInherit",
|
||||
VfsAceFlag::InheritOnly => "InheritOnly",
|
||||
VfsAceFlag::Inherited => "Inherited",
|
||||
VfsAceFlag::SuccessfulAccess => "SuccessfulAccess",
|
||||
VfsAceFlag::FailedAccess => "FailedAccess",
|
||||
}.to_string()).collect(),
|
||||
mask: ace.mask.iter().map(|m| match m {
|
||||
VfsAceMask::ReadData => "ReadData",
|
||||
VfsAceMask::WriteData => "WriteData",
|
||||
VfsAceMask::Execute => "Execute",
|
||||
VfsAceMask::ListDirectory => "ListDirectory",
|
||||
VfsAceMask::AddFile => "AddFile",
|
||||
VfsAceMask::AddSubdirectory => "AddSubdirectory",
|
||||
VfsAceMask::DeleteChild => "DeleteChild",
|
||||
VfsAceMask::Delete => "Delete",
|
||||
VfsAceMask::ReadAttributes => "ReadAttributes",
|
||||
VfsAceMask::WriteAttributes => "WriteAttributes",
|
||||
VfsAceMask::ReadNfsAcl => "ReadAcl",
|
||||
VfsAceMask::WriteNfsAcl => "WriteAcl",
|
||||
VfsAceMask::ReadOwner => "ReadOwner",
|
||||
VfsAceMask::WriteOwner => "WriteOwner",
|
||||
VfsAceMask::Synchronize => "Synchronize",
|
||||
VfsAceMask::FullControl => "FullControl",
|
||||
}.to_string()).collect(),
|
||||
principal: ace.principal.clone(),
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(AclInfo {
|
||||
path,
|
||||
aces,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_acl(user_id: String, path: String, aces: Vec<AceInfo>) -> Result<(), String> {
|
||||
let vfs = LocalFs::new();
|
||||
let path_buf = PathBuf::from(&path);
|
||||
|
||||
let vfs_aces = aces.iter().map(|ace| {
|
||||
VfsAce {
|
||||
ace_type: match ace.ace_type.as_str() {
|
||||
"Allow" => VfsAceType::Allow,
|
||||
"Deny" => VfsAceType::Deny,
|
||||
"Audit" => VfsAceType::Audit,
|
||||
"Alarm" => VfsAceType::Alarm,
|
||||
_ => VfsAceType::Allow,
|
||||
},
|
||||
flags: ace.flags.iter().map(|f| match f.as_str() {
|
||||
"FileInherit" => VfsAceFlag::FileInherit,
|
||||
"DirectoryInherit" => VfsAceFlag::DirectoryInherit,
|
||||
"NoPropagateInherit" => VfsAceFlag::NoPropagateInherit,
|
||||
"InheritOnly" => VfsAceFlag::InheritOnly,
|
||||
"Inherited" => VfsAceFlag::Inherited,
|
||||
"SuccessfulAccess" => VfsAceFlag::SuccessfulAccess,
|
||||
"FailedAccess" => VfsAceFlag::FailedAccess,
|
||||
_ => VfsAceFlag::FileInherit,
|
||||
}).collect(),
|
||||
mask: ace.mask.iter().map(|m| match m.as_str() {
|
||||
"ReadData" => VfsAceMask::ReadData,
|
||||
"WriteData" => VfsAceMask::WriteData,
|
||||
"Execute" => VfsAceMask::Execute,
|
||||
"ListDirectory" => VfsAceMask::ListDirectory,
|
||||
"AddFile" => VfsAceMask::AddFile,
|
||||
"AddSubdirectory" => VfsAceMask::AddSubdirectory,
|
||||
"DeleteChild" => VfsAceMask::DeleteChild,
|
||||
"Delete" => VfsAceMask::Delete,
|
||||
"ReadAttributes" => VfsAceMask::ReadAttributes,
|
||||
"WriteAttributes" => VfsAceMask::WriteAttributes,
|
||||
"ReadAcl" => VfsAceMask::ReadNfsAcl,
|
||||
"WriteAcl" => VfsAceMask::WriteNfsAcl,
|
||||
"ReadOwner" => VfsAceMask::ReadOwner,
|
||||
"WriteOwner" => VfsAceMask::WriteOwner,
|
||||
"Synchronize" => VfsAceMask::Synchronize,
|
||||
"FullControl" => VfsAceMask::FullControl,
|
||||
_ => VfsAceMask::ReadData,
|
||||
}).collect(),
|
||||
principal: ace.principal.clone(),
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let acl = VfsAcl {
|
||||
aces: vfs_aces,
|
||||
default_acl: None,
|
||||
};
|
||||
|
||||
vfs.set_acl(&path_buf, &acl)
|
||||
.map_err(|e| format!("Failed to set ACL: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_acl(user_id: String, path: String, principal: String, mask: String) -> Result<bool, String> {
|
||||
let vfs = LocalFs::new();
|
||||
let path_buf = PathBuf::from(&path);
|
||||
|
||||
let vfs_mask = match mask.as_str() {
|
||||
"ReadData" => VfsAceMask::ReadData,
|
||||
"WriteData" => VfsAceMask::WriteData,
|
||||
"Execute" => VfsAceMask::Execute,
|
||||
"ReadAcl" => VfsAceMask::ReadNfsAcl,
|
||||
"WriteAcl" => VfsAceMask::WriteNfsAcl,
|
||||
_ => VfsAceMask::ReadData,
|
||||
};
|
||||
|
||||
let result = vfs.check_acl(&path_buf, &principal, vfs_mask)
|
||||
.map_err(|e| format!("Failed to check ACL: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -293,4 +293,102 @@ fn build_tree(
|
||||
}
|
||||
|
||||
Err("Root node not found".to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FileMetadata {
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub modified: String,
|
||||
pub permissions: String,
|
||||
pub file_type: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_file_content(
|
||||
file_path: String,
|
||||
) -> Result<String, String> {
|
||||
use std::fs;
|
||||
|
||||
if !Path::new(&file_path).exists() {
|
||||
return Err(format!("File not found: {}", file_path));
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&file_path)
|
||||
.map_err(|e| format!("Failed to read file: {}", e))?;
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_file_metadata(
|
||||
user_id: String,
|
||||
file_uuid: String,
|
||||
) -> Result<FileMetadata, String> {
|
||||
use std::fs;
|
||||
|
||||
let db_path = PathBuf::from("data/users")
|
||||
.join(format!("{}.sqlite", user_id));
|
||||
|
||||
if !db_path.exists() {
|
||||
return Err(format!("Database not found: {:?}", db_path));
|
||||
}
|
||||
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||
|
||||
let (file_name, file_path, file_size, registered_at): (String, String, i64, String) = conn.query_row(
|
||||
"SELECT original_name, file_path, file_size, registered_at
|
||||
FROM file_registry
|
||||
WHERE file_uuid = ?1",
|
||||
[&file_uuid],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
|
||||
).map_err(|e| format!("Failed to query file metadata: {}", e))?;
|
||||
|
||||
let metadata = fs::metadata(&file_path)
|
||||
.map_err(|e| format!("Failed to get file metadata: {}", e))?;
|
||||
|
||||
let modified = metadata.modified()
|
||||
.map(|t| {
|
||||
let datetime: std::time::SystemTime = t;
|
||||
let timestamp = datetime.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
timestamp.to_string()
|
||||
})
|
||||
.unwrap_or_else(|_| "Unknown".to_string());
|
||||
|
||||
let ext = Path::new(&file_name)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
|
||||
let file_type = if ext.is_empty() {
|
||||
"file".to_string()
|
||||
} else if ["jpg", "jpeg", "png", "gif", "bmp", "webp"].contains(&ext.as_str()) {
|
||||
"image".to_string()
|
||||
} else if ["mp4", "avi", "mov", "wmv", "flv", "mkv"].contains(&ext.as_str()) {
|
||||
"video".to_string()
|
||||
} else if ["mp3", "wav", "ogg", "flac"].contains(&ext.as_str()) {
|
||||
"audio".to_string()
|
||||
} else if ["pdf"].contains(&ext.as_str()) {
|
||||
"pdf".to_string()
|
||||
} else if ["txt", "md", "json", "xml", "yaml"].contains(&ext.as_str()) {
|
||||
"text".to_string()
|
||||
} else {
|
||||
"file".to_string()
|
||||
};
|
||||
|
||||
let permissions = if metadata.permissions().readonly() {
|
||||
"Read-only".to_string()
|
||||
} else {
|
||||
"Read-write".to_string()
|
||||
};
|
||||
|
||||
Ok(FileMetadata {
|
||||
name: file_name,
|
||||
size: file_size as u64,
|
||||
modified,
|
||||
permissions,
|
||||
file_type,
|
||||
})
|
||||
}
|
||||
@@ -9,6 +9,9 @@ pub mod backup;
|
||||
pub mod user_management;
|
||||
pub mod share_management;
|
||||
pub mod system_stats;
|
||||
pub mod virtual_folders;
|
||||
pub mod quota;
|
||||
pub mod acl;
|
||||
|
||||
pub use file_ops::*;
|
||||
pub use install::*;
|
||||
@@ -20,4 +23,7 @@ pub use monitor::*;
|
||||
pub use backup::*;
|
||||
pub use user_management::*;
|
||||
pub use share_management::*;
|
||||
pub use system_stats::*;
|
||||
pub use system_stats::*;
|
||||
pub use virtual_folders::*;
|
||||
pub use quota::*;
|
||||
pub use acl::*;
|
||||
97
markbase-tauri/src-tauri/src/commands/quota.rs
Normal file
97
markbase-tauri/src-tauri/src/commands/quota.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use markbase_core::vfs::{VfsQuota, VfsQuotaUsage, VfsBackend, local_fs::LocalFs};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct QuotaInfo {
|
||||
pub path: String,
|
||||
pub space_limit: u64,
|
||||
pub file_limit: u64,
|
||||
pub soft_limit: u64,
|
||||
pub grace_period: u64,
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct QuotaUsageInfo {
|
||||
pub path: String,
|
||||
pub space_used: u64,
|
||||
pub files_used: u64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_quota(user_id: String, path: String) -> Result<QuotaInfo, String> {
|
||||
let vfs = LocalFs::new();
|
||||
let path_buf = PathBuf::from(&path);
|
||||
|
||||
let quota = vfs.get_quota(&path_buf)
|
||||
.map_err(|e| format!("Failed to get quota: {}", e))?;
|
||||
|
||||
Ok(QuotaInfo {
|
||||
path,
|
||||
space_limit: quota.space_limit,
|
||||
file_limit: quota.file_limit,
|
||||
soft_limit: quota.soft_limit,
|
||||
grace_period: quota.grace_period,
|
||||
user_id: quota.user_id,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_quota(
|
||||
user_id: String,
|
||||
path: String,
|
||||
space_limit: u64,
|
||||
file_limit: u64,
|
||||
soft_limit: u64,
|
||||
grace_period: u64,
|
||||
) -> Result<(), String> {
|
||||
let vfs = LocalFs::new();
|
||||
let path_buf = PathBuf::from(&path);
|
||||
|
||||
let quota = VfsQuota {
|
||||
space_limit,
|
||||
file_limit,
|
||||
soft_limit,
|
||||
grace_period,
|
||||
user_id: Some(user_id),
|
||||
};
|
||||
|
||||
vfs.set_quota(&path_buf, "a)
|
||||
.map_err(|e| format!("Failed to set quota: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_quota_usage(user_id: String, path: String) -> Result<QuotaUsageInfo, String> {
|
||||
let vfs = LocalFs::new();
|
||||
let path_buf = PathBuf::from(&path);
|
||||
|
||||
let usage = vfs.get_quota_usage(&path_buf)
|
||||
.map_err(|e| format!("Failed to get quota usage: {}", e))?;
|
||||
|
||||
Ok(QuotaUsageInfo {
|
||||
path,
|
||||
space_used: usage.space_used,
|
||||
files_used: usage.files_used,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_quota(user_id: String, path: String, size: u64) -> Result<bool, String> {
|
||||
let vfs = LocalFs::new();
|
||||
let path_buf = PathBuf::from(&path);
|
||||
|
||||
let quota = vfs.get_quota(&path_buf)
|
||||
.map_err(|e| format!("Failed to get quota: {}", e))?;
|
||||
|
||||
let usage = vfs.get_quota_usage(&path_buf)
|
||||
.map_err(|e| format!("Failed to get quota usage: {}", e))?;
|
||||
|
||||
let space_ok = quota.space_limit == 0 || usage.space_used + size <= quota.space_limit;
|
||||
let files_ok = quota.file_limit == 0 || usage.files_used + 1 <= quota.file_limit;
|
||||
|
||||
Ok(space_ok && files_ok)
|
||||
}
|
||||
113
markbase-tauri/src-tauri/src/commands/virtual_folders.rs
Normal file
113
markbase-tauri/src-tauri/src/commands/virtual_folders.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use markbase_core::vfs::virtual_fs::VirtualFs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use rusqlite::Connection;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct VirtualFolder {
|
||||
pub folder: String,
|
||||
pub description: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_virtual_folders(user_id: String) -> Result<Vec<VirtualFolder>, String> {
|
||||
let db_path = PathBuf::from("data/users")
|
||||
.join(format!("{}.sqlite", user_id));
|
||||
|
||||
if !db_path.exists() {
|
||||
return Err(format!("Database not found: {:?}", db_path));
|
||||
}
|
||||
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT folder, description, created_at FROM virtual_folders ORDER BY folder"
|
||||
).map_err(|e| format!("Failed to prepare statement: {}", e))?;
|
||||
|
||||
let folders = stmt.query_map([], |row| {
|
||||
Ok(VirtualFolder {
|
||||
folder: row.get::<_, String>(0)?,
|
||||
description: row.get::<_, String>(1)?,
|
||||
created_at: row.get::<_, String>(2)?,
|
||||
})
|
||||
}).map_err(|e| format!("Failed to query folders: {}", e))?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for folder in folders {
|
||||
let folder_data = folder.map_err(|e| format!("Failed to get folder: {}", e))?;
|
||||
result.push(folder_data);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_virtual_folder(
|
||||
user_id: String,
|
||||
folder: String,
|
||||
description: String,
|
||||
) -> Result<(), String> {
|
||||
let db_path = PathBuf::from("data/users")
|
||||
.join(format!("{}.sqlite", user_id));
|
||||
|
||||
if !db_path.exists() {
|
||||
return Err(format!("Database not found: {:?}", db_path));
|
||||
}
|
||||
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO virtual_folders (folder, description) VALUES (?1, ?2)",
|
||||
rusqlite::params![folder, description],
|
||||
).map_err(|e| format!("Failed to create folder: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_virtual_folder(
|
||||
user_id: String,
|
||||
folder: String,
|
||||
description: String,
|
||||
) -> Result<(), String> {
|
||||
let db_path = PathBuf::from("data/users")
|
||||
.join(format!("{}.sqlite", user_id));
|
||||
|
||||
if !db_path.exists() {
|
||||
return Err(format!("Database not found: {:?}", db_path));
|
||||
}
|
||||
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||
|
||||
conn.execute(
|
||||
"UPDATE virtual_folders SET description = ?1 WHERE folder = ?2",
|
||||
rusqlite::params![description, folder],
|
||||
).map_err(|e| format!("Failed to update folder: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_virtual_folder(user_id: String, folder: String) -> Result<(), String> {
|
||||
let db_path = PathBuf::from("data/users")
|
||||
.join(format!("{}.sqlite", user_id));
|
||||
|
||||
if !db_path.exists() {
|
||||
return Err(format!("Database not found: {:?}", db_path));
|
||||
}
|
||||
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||
|
||||
conn.execute(
|
||||
"DELETE FROM virtual_folders WHERE folder = ?1",
|
||||
rusqlite::params![folder],
|
||||
).map_err(|e| format!("Failed to delete folder: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -13,6 +13,8 @@ fn main() {
|
||||
search_files,
|
||||
download_file,
|
||||
open_file,
|
||||
read_file_content,
|
||||
get_file_metadata,
|
||||
check_system_environment,
|
||||
initialize_database,
|
||||
create_service_account,
|
||||
@@ -55,6 +57,17 @@ fn main() {
|
||||
get_system_stats,
|
||||
get_all_services_status,
|
||||
get_recent_activity,
|
||||
list_virtual_folders,
|
||||
create_virtual_folder,
|
||||
update_virtual_folder,
|
||||
delete_virtual_folder,
|
||||
get_quota,
|
||||
set_quota,
|
||||
get_quota_usage,
|
||||
check_quota,
|
||||
get_acl,
|
||||
set_acl,
|
||||
check_acl,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
Reference in New Issue
Block a user