Files
markbase/markbase-tauri/src-tauri/src/commands/file_ops.rs
T
Warren ceadeef329
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
Phase 2.7.3完成:文件上传功能实现
功能:
- Tauri dialog API集成(文件选择对话框)
- upload_file命令完整实现(文件复制 + 数据库注册)
- 上传按钮UI(带loading状态)
- 上传完成后自动刷新文件树

技术:
- 添加uuid依赖(UUID v4生成)
- Rust: std::fs文件复制 + rusqlite数据库注册
- Vue: @tauri-apps/api/dialog集成
- Vite: 修复dialog API外部化配置

状态:Phase 2完成100%
2026-06-13 16:09:58 +08:00

296 lines
8.7 KiB
Rust

use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use rusqlite::Connection;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TreeNode {
pub id: String,
pub name: String,
pub node_type: String,
pub size: Option<u64>,
pub children: Vec<TreeNode>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FileUploadResult {
pub success: bool,
pub message: String,
pub file_id: Option<String>,
}
#[tauri::command]
pub async fn get_tree(
user_id: String,
tree_type: String,
) -> Result<TreeNode, 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 node_id, label, node_type, parent_id, file_size
FROM file_nodes
WHERE tree_type = ?1
ORDER BY sort_order ASC"
).map_err(|e| format!("Failed to prepare statement: {}", e))?;
let nodes = stmt.query_map([&tree_type], |row| {
Ok((
row.get::<_, String>(0)?, // node_id
row.get::<_, String>(1)?, // label
row.get::<_, String>(2)?, // node_type
row.get::<_, Option<String>>(3)?, // parent_id
row.get::<_, Option<i64>>(4)?, // file_size
))
}).map_err(|e| format!("Failed to query nodes: {}", e))?;
let mut all_nodes: Vec<(String, String, String, Option<String>, Option<i64>)> = vec
![];
for node in nodes {
let node_data = node.map_err(|e| format!("Failed to get node: {}", e))?;
all_nodes.push(node_data);
}
let root_node = build_tree(&all_nodes, None)?;
Ok(root_node)
}
#[tauri::command]
pub async fn list_files(
user_id: String,
tree_type: String,
parent_id: String,
) -> Result<Vec<TreeNode>, 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 node_id, label, node_type, file_size
FROM file_nodes
WHERE tree_type = ?1 AND parent_id = ?2
ORDER BY sort_order ASC"
).map_err(|e| format!("Failed to prepare statement: {}", e))?;
let files = stmt.query_map([&tree_type, &parent_id], |row| {
Ok(TreeNode {
id: row.get::<_, String>(0)?,
name: row.get::<_, String>(1)?,
node_type: row.get::<_, String>(2)?,
size: row.get::<_, Option<i64>>(3)?.map(|s| s as u64),
children: vec
![],
})
}).map_err(|e| format!("Failed to query files: {}", e))?;
let mut result: Vec<TreeNode> = vec
![];
for file in files {
let file_node = file.map_err(|e| format!("Failed to get file: {}", e))?;
result.push(file_node);
}
Ok(result)
}
#[tauri::command]
pub async fn upload_file(
user_id: String,
source_path: String,
target_path: String,
tree_type: String,
) -> Result<FileUploadResult, String> {
use std::fs;
use uuid::Uuid;
let demo_dir = "/Users/accusys/momentry/var/sftpgo/data/demo";
let target_file = PathBuf::from(demo_dir).join(&target_path);
if !Path::new(&source_path).exists() {
return Err(format!("Source file not found: {}", source_path));
}
fs::copy(&source_path, &target_file)
.map_err(|e| format!("Failed to copy file: {}", e))?;
let db_path = PathBuf::from("data/users")
.join(format!("{}.sqlite", user_id));
let conn = Connection::open(&db_path)
.map_err(|e| format!("Failed to open database: {}", e))?;
let file_uuid = Uuid::new_v4().to_string();
let file_name = Path::new(&target_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
conn.execute(
"INSERT INTO file_registry (file_uuid, original_name, file_path, file_size, registered_at, last_seen_at, status)
VALUES (?1, ?2, ?3, ?4, datetime('now'), datetime('now'), 'active')",
rusqlite::params![&file_uuid, &file_name, target_file.to_string_lossy().to_string(), fs::metadata(&target_file).map(|m| m.len() as i64).unwrap_or(0)]
).map_err(|e| format!("Failed to register file: {}", e))?;
Ok(FileUploadResult {
success: true,
message: format!("File uploaded to: {}", target_path),
file_id: Some(file_uuid),
})
}
#[tauri::command]
pub async fn search_files(
user_id: String,
tree_type: String,
query: String,
) -> Result<Vec<TreeNode>, 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 node_id, label, node_type, file_size
FROM file_nodes
WHERE tree_type = ?1 AND label LIKE ?2
ORDER BY label ASC"
).map_err(|e| format!("Failed to prepare statement: {}", e))?;
let search_pattern = format!("%{}%", query);
let files = stmt.query_map([&tree_type, &search_pattern], |row| {
Ok(TreeNode {
id: row.get::<_, String>(0)?,
name: row.get::<_, String>(1)?,
node_type: row.get::<_, String>(2)?,
size: row.get::<_, Option<i64>>(3)?.map(|s| s as u64),
children: vec
![],
})
}).map_err(|e| format!("Failed to search files: {}", e))?;
let mut result: Vec<TreeNode> = vec
![];
for file in files {
let file_node = file.map_err(|e| format!("Failed to get file: {}", e))?;
result.push(file_node);
}
Ok(result)
}
#[tauri::command]
pub async fn download_file(
user_id: String,
file_uuid: String,
) -> Result<String, 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 file_path: String = conn.query_row(
"SELECT fr.file_path
FROM file_registry fr
JOIN file_nodes fn ON fr.file_uuid = fn.file_uuid
WHERE fn.node_id = ?1",
[&file_uuid],
|row| row.get(0)
).map_err(|e| format!("Failed to query file path: {}", e))?;
Ok(file_path)
}
#[tauri::command]
pub async fn open_file(file_path: String) -> Result<(), String> {
use std::process::Command;
#[cfg(target_os = "macos")]
{
Command::new("open")
.arg(&file_path)
.spawn()
.map_err(|e| format!("Failed to open file: {}", e))?;
}
#[cfg(target_os = "windows")]
{
Command::new("cmd")
.args(&["/C", "start", &file_path])
.spawn()
.map_err(|e| format!("Failed to open file: {}", e))?;
}
#[cfg(target_os = "linux")]
{
Command::new("xdg-open")
.arg(&file_path)
.spawn()
.map_err(|e| format!("Failed to open file: {}", e))?;
}
Ok(())
}
fn build_tree(
nodes: &[(String, String, String, Option<String>, Option<i64>)],
parent_id: Option<&str>,
) -> Result<TreeNode, String> {
for (node_id, label, node_type, node_parent_id, file_size) in nodes {
let is_match = match (parent_id, node_parent_id) {
(None, None) => true,
(Some(p), Some(np)) => p == np,
_ => false,
};
if is_match {
let children = nodes.iter()
.filter(|(_, _, _, np, _)| np.as_deref() == Some(node_id.as_str()))
.map(|(cid, clabel, ctype, _, csize)| {
TreeNode {
id: cid.clone(),
name: clabel.clone(),
node_type: ctype.clone(),
size: csize.map(|s| s as u64),
children: vec
![],
}
})
.collect();
return Ok(TreeNode {
id: node_id.clone(),
name: label.clone(),
node_type: node_type.clone(),
size: file_size.map(|s| s as u64),
children,
});
}
}
Err("Root node not found".to_string())
}