|
|
|
|
@@ -1,9 +1,10 @@
|
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
use rusqlite::Connection;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::str::FromStr;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
use crate::filetree::node::{Aliases, FileNode, NodeType};
|
|
|
|
|
use crate::node::{Aliases, FileNode, NodeType};
|
|
|
|
|
|
|
|
|
|
pub mod convert;
|
|
|
|
|
pub mod mode;
|
|
|
|
|
@@ -12,10 +13,21 @@ pub mod node;
|
|
|
|
|
|
|
|
|
|
pub struct FileTree {
|
|
|
|
|
pub user_id: String,
|
|
|
|
|
pub tree_type: String,
|
|
|
|
|
pub nodes: Vec<FileNode>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CREATE_TABLES: &str = "
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct TreeType {
|
|
|
|
|
pub tree_type: String,
|
|
|
|
|
pub tree_name: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub is_system_defined: i64,
|
|
|
|
|
pub created_at: String,
|
|
|
|
|
pub updated_at: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub const CREATE_TABLES: &str = "
|
|
|
|
|
CREATE TABLE IF NOT EXISTS file_registry (
|
|
|
|
|
file_uuid TEXT PRIMARY KEY,
|
|
|
|
|
original_name TEXT NOT NULL,
|
|
|
|
|
@@ -42,7 +54,8 @@ CREATE TABLE IF NOT EXISTS file_nodes (
|
|
|
|
|
registered_at TEXT,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
|
|
|
sort_order INTEGER NOT NULL DEFAULT 0
|
|
|
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
tree_type TEXT NOT NULL DEFAULT 'untitled folder'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS file_locations (
|
|
|
|
|
@@ -53,6 +66,21 @@ CREATE TABLE IF NOT EXISTS file_locations (
|
|
|
|
|
added_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
|
|
|
UNIQUE(file_uuid, location)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS tree_registry (
|
|
|
|
|
tree_type TEXT PRIMARY KEY,
|
|
|
|
|
tree_name TEXT NOT NULL,
|
|
|
|
|
description TEXT,
|
|
|
|
|
is_system_defined INTEGER DEFAULT 0,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_file_nodes_label ON file_nodes(label);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_file_nodes_file_uuid ON file_nodes(file_uuid);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_file_nodes_parent_id ON file_nodes(parent_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_file_nodes_tree_type ON file_nodes(tree_type);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_file_locations_file_uuid ON file_locations(file_uuid);
|
|
|
|
|
";
|
|
|
|
|
|
|
|
|
|
impl FileTree {
|
|
|
|
|
@@ -75,16 +103,16 @@ impl FileTree {
|
|
|
|
|
Connection::open(&db_path).with_context(|| format!("Failed to open {}", db_path))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn load(conn: &Connection, user_id: &str) -> Result<Self> {
|
|
|
|
|
pub fn load(conn: &Connection, user_id: &str, tree_type: &str) -> Result<Self> {
|
|
|
|
|
let mut stmt = conn.prepare(
|
|
|
|
|
"SELECT node_id, label, aliases_json, file_uuid, sha256, parent_id, children_json,
|
|
|
|
|
node_type, icon, color, bg_color, file_size, registered_at,
|
|
|
|
|
created_at, updated_at, sort_order
|
|
|
|
|
FROM file_nodes ORDER BY sort_order ASC, created_at ASC",
|
|
|
|
|
FROM file_nodes WHERE tree_type = ?1 ORDER BY sort_order ASC, created_at ASC",
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
let nodes: Vec<FileNode> = stmt
|
|
|
|
|
.query_map([], |row| {
|
|
|
|
|
.query_map([tree_type], |row| {
|
|
|
|
|
let children_json: String = row.get(6)?;
|
|
|
|
|
let children: Vec<String> =
|
|
|
|
|
serde_json::from_str(&children_json).unwrap_or_default();
|
|
|
|
|
@@ -113,6 +141,7 @@ impl FileTree {
|
|
|
|
|
|
|
|
|
|
Ok(FileTree {
|
|
|
|
|
user_id: user_id.to_string(),
|
|
|
|
|
tree_type: tree_type.to_string(),
|
|
|
|
|
nodes,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
@@ -121,8 +150,8 @@ impl FileTree {
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO file_nodes (node_id, label, aliases_json, file_uuid, sha256, parent_id,
|
|
|
|
|
children_json, node_type, icon, color, bg_color, file_size, registered_at,
|
|
|
|
|
created_at, updated_at, sort_order)
|
|
|
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)",
|
|
|
|
|
created_at, updated_at, sort_order, tree_type)
|
|
|
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
|
|
|
|
|
rusqlite::params![
|
|
|
|
|
node.node_id,
|
|
|
|
|
node.label,
|
|
|
|
|
@@ -140,6 +169,7 @@ impl FileTree {
|
|
|
|
|
node.created_at,
|
|
|
|
|
node.updated_at,
|
|
|
|
|
node.sort_order,
|
|
|
|
|
self.tree_type,
|
|
|
|
|
],
|
|
|
|
|
)?;
|
|
|
|
|
self.nodes.push(node.clone());
|
|
|
|
|
@@ -421,8 +451,56 @@ impl FileTree {
|
|
|
|
|
"location_count": locs.len(),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 新增:创建虚拟树类型
|
|
|
|
|
pub fn create_tree_type(
|
|
|
|
|
conn: &Connection,
|
|
|
|
|
tree_type: &str,
|
|
|
|
|
tree_name: &str,
|
|
|
|
|
description: &str,
|
|
|
|
|
is_system_defined: bool,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO tree_registry (tree_type, tree_name, description, is_system_defined)
|
|
|
|
|
VALUES (?1, ?2, ?3, ?4)",
|
|
|
|
|
rusqlite::params![tree_type, tree_name, description, is_system_defined as i64],
|
|
|
|
|
)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 新增:获取所有虚拟树类型
|
|
|
|
|
pub fn get_all_tree_types(conn: &Connection) -> Result<Vec<TreeType>> {
|
|
|
|
|
let mut stmt = conn.prepare(
|
|
|
|
|
"SELECT tree_type, tree_name, description, is_system_defined, created_at, updated_at
|
|
|
|
|
FROM tree_registry ORDER BY created_at ASC",
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
let tree_types = stmt
|
|
|
|
|
.query_map([], |row| {
|
|
|
|
|
Ok(TreeType {
|
|
|
|
|
tree_type: row.get(0)?,
|
|
|
|
|
tree_name: row.get(1)?,
|
|
|
|
|
description: row.get(2)?,
|
|
|
|
|
is_system_defined: row.get(3)?,
|
|
|
|
|
created_at: row.get(4)?,
|
|
|
|
|
updated_at: row.get(5)?,
|
|
|
|
|
})
|
|
|
|
|
})?
|
|
|
|
|
.map(|r| r.map_err(|e: rusqlite::Error| anyhow::anyhow!(e)))
|
|
|
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
|
|
|
|
|
|
Ok(tree_types)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 新增:删除虚拟树类型(仅限用户自定义)
|
|
|
|
|
pub fn delete_tree_type(conn: &Connection, tree_type: &str) -> Result<()> {
|
|
|
|
|
conn.execute(
|
|
|
|
|
"DELETE FROM tree_registry WHERE tree_type = ?1 AND is_system_defined = 0",
|
|
|
|
|
[tree_type],
|
|
|
|
|
)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
@@ -436,7 +514,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_init_and_load_empty_tree() {
|
|
|
|
|
let (conn, user_id) = temp_db();
|
|
|
|
|
let tree = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
assert_eq!(tree.user_id, user_id);
|
|
|
|
|
assert!(tree.nodes.is_empty());
|
|
|
|
|
}
|
|
|
|
|
@@ -444,12 +522,12 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_insert_and_load_node() {
|
|
|
|
|
let (conn, user_id) = temp_db();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
|
|
|
|
|
let folder = FileTree::new_folder("Videos", None);
|
|
|
|
|
tree.insert_node(&conn, &folder).unwrap();
|
|
|
|
|
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
assert_eq!(loaded.nodes.len(), 1);
|
|
|
|
|
assert_eq!(loaded.nodes[0].label, "Videos");
|
|
|
|
|
assert_eq!(loaded.nodes[0].node_type, NodeType::Folder);
|
|
|
|
|
@@ -458,7 +536,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_update_node() {
|
|
|
|
|
let (conn, user_id) = temp_db();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
|
|
|
|
|
let mut folder = FileTree::new_folder("Videos", None);
|
|
|
|
|
tree.insert_node(&conn, &folder).unwrap();
|
|
|
|
|
@@ -468,7 +546,7 @@ mod tests {
|
|
|
|
|
folder.color = Some("#ff0000".to_string());
|
|
|
|
|
tree.update_node(&conn, &folder.node_id, &folder).unwrap();
|
|
|
|
|
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
assert_eq!(loaded.nodes[0].label, "Movies");
|
|
|
|
|
assert_eq!(loaded.nodes[0].icon, Some("📽️".to_string()));
|
|
|
|
|
assert_eq!(loaded.nodes[0].color, Some("#ff0000".to_string()));
|
|
|
|
|
@@ -477,7 +555,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_delete_node() {
|
|
|
|
|
let (conn, user_id) = temp_db();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
|
|
|
|
|
let folder = FileTree::new_folder("Temp", None);
|
|
|
|
|
let node_id = folder.node_id.clone();
|
|
|
|
|
@@ -485,14 +563,14 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
tree.delete_node(&conn, &node_id).unwrap();
|
|
|
|
|
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
assert_eq!(loaded.nodes.len(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_move_node() {
|
|
|
|
|
let (conn, user_id) = temp_db();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
|
|
|
|
|
let root = FileTree::new_folder("Root", None);
|
|
|
|
|
let child = FileTree::new_folder("Child", Some(root.node_id.clone()));
|
|
|
|
|
@@ -502,7 +580,7 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
tree.move_node(&conn, &child.node_id, None).unwrap();
|
|
|
|
|
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
let moved = loaded.nodes.iter().find(|n| n.label == "Child").unwrap();
|
|
|
|
|
assert!(moved.parent_id.is_none());
|
|
|
|
|
}
|
|
|
|
|
@@ -510,7 +588,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_update_alias() {
|
|
|
|
|
let (conn, user_id) = temp_db();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
|
|
|
|
|
let folder = FileTree::new_folder("Videos", None);
|
|
|
|
|
tree.insert_node(&conn, &folder).unwrap();
|
|
|
|
|
@@ -518,7 +596,7 @@ mod tests {
|
|
|
|
|
tree.update_node_alias(&conn, &folder.node_id, "zh_tw", "影片")
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let loaded = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
loaded.nodes[0].aliases.get("zh_tw").map(|s| s.as_str()),
|
|
|
|
|
Some("影片")
|
|
|
|
|
@@ -528,7 +606,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_build_tree() {
|
|
|
|
|
let (conn, user_id) = temp_db();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id).unwrap();
|
|
|
|
|
let mut tree = FileTree::load(&conn, &user_id, "untitled folder").unwrap();
|
|
|
|
|
|
|
|
|
|
let root = FileTree::new_folder("Root", None);
|
|
|
|
|
let child1 = FileTree::new_folder("Child1", Some(root.node_id.clone()));
|
|
|
|
|
@@ -551,3 +629,29 @@ mod tests {
|
|
|
|
|
assert_eq!(roots[0].label, "Root");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 新增:创建虚拟树类型
|
|
|
|
|
pub fn create_tree_type(
|
|
|
|
|
conn: &Connection,
|
|
|
|
|
tree_type: &str,
|
|
|
|
|
tree_name: &str,
|
|
|
|
|
description: &str,
|
|
|
|
|
is_system_defined: bool,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO tree_registry (tree_type, tree_name, description, is_system_defined)
|
|
|
|
|
VALUES (?1, ?2, ?3, ?4)",
|
|
|
|
|
rusqlite::params![tree_type, tree_name, description, is_system_defined as i64],
|
|
|
|
|
)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 新增:获取所有虚拟树类型
|
|
|
|
|
// 新增:删除虚拟树类型(仅限用户自定义)
|
|
|
|
|
pub fn delete_tree_type(conn: &Connection, tree_type: &str) -> Result<()> {
|
|
|
|
|
conn.execute(
|
|
|
|
|
"DELETE FROM tree_registry WHERE tree_type = ?1 AND is_system_defined = 0",
|
|
|
|
|
[tree_type],
|
|
|
|
|
)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|