From 0258097029fbd8ba48b59d886c9cea303e852deb Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 6 Oct 2025 03:25:19 +0800 Subject: Redesign framework canvas with modular architecture - Replace complex server-client diagram with clean modular structure - Organize components into Client, Protocols, Datas, File Storage, and Client/Server groups - Add core components: CORE, IPC, MCP, GUI, AI-Models, and CLI - Simplify relationships between virtual files, sheets, and local workspace --- FRAMEWORK_CANVAS.canvas | 93 ++++++++++++++++++------------------------------- 1 file changed, 34 insertions(+), 59 deletions(-) diff --git a/FRAMEWORK_CANVAS.canvas b/FRAMEWORK_CANVAS.canvas index 6ccb97e..26ec7bb 100644 --- a/FRAMEWORK_CANVAS.canvas +++ b/FRAMEWORK_CANVAS.canvas @@ -1,65 +1,40 @@ { "nodes":[ - {"id":"1d3ec658666499c1","type":"group","x":-1360,"y":-521,"width":946,"height":391,"color":"5","label":"Sheets Manager"}, - {"id":"84179fd3d7bd4a40","type":"group","x":-1187,"y":-20,"width":600,"height":391,"color":"4","label":"Verify Server"}, - {"id":"f6d50a7965b39a10","type":"group","x":-306,"y":-521,"width":546,"height":391,"color":"1","label":"Vault Server"}, - {"id":"2d372fa421b04a86","type":"group","x":-1340,"y":-220,"width":900,"height":80,"color":"5","label":"Sheets"}, - {"id":"c9d997e3609afbc7","type":"text","text":"Remote Machine","x":-306,"y":-720,"width":206,"height":50}, - {"id":"b6d1139fc7ce8f2e","type":"text","text":"File System","x":-277,"y":-860,"width":148,"height":60}, - {"id":"e4d781029c03267d","type":"text","text":"USER_SHEET_1","x":-1100,"y":-200,"width":200,"height":50}, - {"id":"60a64de812f8f309","type":"text","text":"USER_SHEET_2","x":-880,"y":-200,"width":200,"height":50}, - {"id":"e56cf63de6613e4b","type":"text","text":"...","x":-660,"y":-200,"width":200,"height":50}, - {"id":"9c88ae31931a06e3","type":"text","text":"**Sheets Manager**","x":-980,"y":-491,"width":180,"height":50,"color":"5"}, - {"id":"8ccf62ef395b29a5","type":"text","text":"MAIN_SHEET","x":-1320,"y":-200,"width":200,"height":50,"color":"1"}, - {"id":"01b33cfd10e1dabe","type":"text","text":"VIRTUAL FILE","x":-286,"y":-501,"width":166,"height":60}, - {"id":"712eca4371ac8771","type":"text","text":"HISTORY","x":64,"y":-501,"width":126,"height":60}, - {"id":"ec99c88ed47f4ae3","type":"text","text":"**Vault Server**","x":37,"y":-200,"width":180,"height":50,"color":"1"}, - {"id":"47f3144bfa56c450","type":"text","text":"Gate Server","x":-160,"y":140,"width":250,"height":60}, - {"id":"eaeeabe33dfc79cc","type":"text","text":"Cached Sheet","x":-1165,"y":600,"width":250,"height":60,"color":"5"}, - {"id":"59b10d87aeb8d173","type":"text","text":"Local File Map","x":-1165,"y":740,"width":250,"height":60}, - {"id":"6c88b3b3171c2cfa","type":"text","text":"Local File","x":-1165,"y":880,"width":250,"height":60,"color":"3"}, - {"id":"331629e5f4b37caa","type":"text","text":"Pub Key DB","x":-1144,"y":0,"width":135,"height":60}, - {"id":"8e33c2586ef8e262","type":"text","text":"Member Info","x":-807,"y":0,"width":200,"height":60}, - {"id":"ec301cee0ad7fae3","type":"text","text":"**Verify Server**","x":-1167,"y":301,"width":180,"height":50,"color":"4"}, - {"id":"a2d30e3b136fe67f","type":"text","text":"Local Machine","x":-800,"y":880,"width":250,"height":60}, - {"id":"f470377233f67831","type":"text","text":"Client","x":-160,"y":880,"width":250,"height":60}, - {"id":"b7b7094136fe8b92","type":"text","text":"Private Key","x":-800,"y":1060,"width":250,"height":60,"color":"3"}, - {"id":"e0151221c4e6aa9e","type":"text","text":"Member Info","x":-160,"y":1200,"width":250,"height":60,"color":"4"}, - {"id":"4ddd85f291925ef3","type":"text","text":"Cmd","x":440,"y":880,"width":200,"height":60}, - {"id":"40743e1c3e19017b","type":"text","text":"Gui","x":440,"y":980,"width":200,"height":60}, - {"id":"feedb8b5f1d0992b","type":"text","text":"","x":840,"y":910,"width":250,"height":60} + {"id":"6108936ff70a6df6","type":"group","x":1540,"y":-480,"width":290,"height":420,"color":"5","label":"Client"}, + {"id":"61beb9dd4858ab56","type":"group","x":1200,"y":-480,"width":290,"height":420,"color":"4","label":"Protocols"}, + {"id":"7b15c8da64a275dd","type":"group","x":20,"y":-480,"width":280,"height":420,"color":"2","label":"Datas"}, + {"id":"5452663e24219b57","type":"group","x":-360,"y":-480,"width":280,"height":420,"color":"1","label":"File Storage"}, + {"id":"08041e1ed8adbf23","type":"group","x":420,"y":-480,"width":277,"height":420,"color":"3","label":"Client / Server"}, + {"id":"ab048c225f92a0b5","type":"text","text":"REAL_FILES","x":-340,"y":-333,"width":240,"height":60,"color":"1"}, + {"id":"9becfba9bd34cf72","type":"text","text":"LOCAL_FILES","x":-340,"y":-200,"width":240,"height":60,"color":"1"}, + {"id":"4a3d9c7d37cc4554","type":"text","text":"SHEETS","x":40,"y":-460,"width":240,"height":60,"color":"2"}, + {"id":"953a678fd5f65526","type":"text","text":"VIRTUAL_FILES","x":40,"y":-333,"width":240,"height":60,"color":"2"}, + {"id":"50615e432257da4e","type":"text","text":"LOCAL_CLONED_SHEET","x":40,"y":-140,"width":240,"height":60,"color":"2"}, + {"id":"6cb37b9c27eef97b","type":"text","text":"VAULT","x":440,"y":-333,"width":237,"height":60,"color":"3"}, + {"id":"fa764e1f4b5523af","type":"text","text":"LOCAL_WORKSPACE","x":440,"y":-200,"width":237,"height":60,"color":"3"}, + {"id":"e9f3d1cb18045858","type":"text","text":"CORE","x":820,"y":-273,"width":240,"height":60}, + {"id":"53fc44e76bcbd0a0","type":"text","text":"IPC","x":1220,"y":-273,"width":250,"height":60,"color":"4"}, + {"id":"79182419068c2908","type":"text","text":"MCP","x":1220,"y":-140,"width":250,"height":60,"color":"4"}, + {"id":"7f945acd64826a42","type":"text","text":"GUI","x":1560,"y":-273,"width":250,"height":60,"color":"5"}, + {"id":"cd1286af92172d06","type":"text","text":"AI - Models","x":1560,"y":-140,"width":250,"height":60,"color":"5"}, + {"id":"1e0a1eea644ecea3","type":"text","text":"CLI","x":1560,"y":-420,"width":250,"height":60,"color":"5"} ], "edges":[ - {"id":"105b992dfa317c73","fromNode":"ec99c88ed47f4ae3","fromSide":"left","toNode":"01b33cfd10e1dabe","toSide":"bottom","color":"1","label":"Storage"}, - {"id":"0e328a66572211a2","fromNode":"712eca4371ac8771","fromSide":"left","toNode":"01b33cfd10e1dabe","toSide":"right","color":"1","label":"Map / Track"}, - {"id":"ed0b1b992f5a9579","fromNode":"ec99c88ed47f4ae3","fromSide":"top","toNode":"712eca4371ac8771","toSide":"bottom","color":"1","label":"Record"}, - {"id":"305730a683227e3e","fromNode":"ec301cee0ad7fae3","fromSide":"top","toNode":"331629e5f4b37caa","toSide":"bottom","color":"4","label":"Record / Verify"}, - {"id":"23ba15a42ff7f1b8","fromNode":"ec301cee0ad7fae3","fromSide":"right","toNode":"8e33c2586ef8e262","toSide":"bottom","color":"4","label":"Cache"}, - {"id":"993945c6912d637c","fromNode":"9c88ae31931a06e3","fromSide":"bottom","toNode":"8ccf62ef395b29a5","toSide":"top","color":"5","label":"Manage"}, - {"id":"88055e57577ce258","fromNode":"9c88ae31931a06e3","fromSide":"bottom","toNode":"e4d781029c03267d","toSide":"top","color":"5","label":"Manage"}, - {"id":"083789a30bd301a2","fromNode":"9c88ae31931a06e3","fromSide":"bottom","toNode":"60a64de812f8f309","toSide":"top","color":"5","label":"Manage"}, - {"id":"65254ae75c8ab69a","fromNode":"9c88ae31931a06e3","fromSide":"bottom","toNode":"e56cf63de6613e4b","toSide":"top","color":"5","label":"Manage"}, - {"id":"2fd4ec961f8af067","fromNode":"47f3144bfa56c450","fromSide":"left","toNode":"84179fd3d7bd4a40","toSide":"right","color":"4","label":"Request"}, - {"id":"3fcb47e7db89faa9","fromNode":"e56cf63de6613e4b","fromSide":"right","toNode":"01b33cfd10e1dabe","toSide":"left","color":"#ffffff","label":"Map"}, - {"id":"a2cdeef7808ab059","fromNode":"01b33cfd10e1dabe","fromSide":"top","toNode":"c9d997e3609afbc7","toSide":"bottom"}, - {"id":"a42517e228c1e28d","fromNode":"c9d997e3609afbc7","fromSide":"top","toNode":"b6d1139fc7ce8f2e","toSide":"bottom"}, - {"id":"741c0dd03a76da8e","fromNode":"84179fd3d7bd4a40","fromSide":"top","toNode":"2d372fa421b04a86","toSide":"bottom","color":"4","label":"Operate"}, - {"id":"c0914905c9302d65","fromNode":"f6d50a7965b39a10","fromSide":"bottom","toNode":"47f3144bfa56c450","toSide":"top","color":"1","label":"Download File"}, - {"id":"0254b65ccaec894f","fromNode":"84179fd3d7bd4a40","fromSide":"right","toNode":"f6d50a7965b39a10","toSide":"bottom","color":"1","label":"Upload File"}, - {"id":"13ad94c252b8a47f","fromNode":"01b33cfd10e1dabe","fromSide":"right","toNode":"712eca4371ac8771","toSide":"left","color":"1"}, - {"id":"ea7d39c89d879e44","fromNode":"eaeeabe33dfc79cc","fromSide":"bottom","toNode":"59b10d87aeb8d173","toSide":"top","label":"Track Name / Path"}, - {"id":"0c22ac4d4c6b07b2","fromNode":"59b10d87aeb8d173","fromSide":"bottom","toNode":"6c88b3b3171c2cfa","toSide":"top","label":"Track Changes"}, - {"id":"30ec55c70139d964","fromNode":"6c88b3b3171c2cfa","fromSide":"top","toNode":"59b10d87aeb8d173","toSide":"bottom"}, - {"id":"f3ece26dd90c59fc","fromNode":"59b10d87aeb8d173","fromSide":"top","toNode":"eaeeabe33dfc79cc","toSide":"bottom"}, - {"id":"2a8dd8c2728e4ea8","fromNode":"84179fd3d7bd4a40","fromSide":"bottom","toNode":"eaeeabe33dfc79cc","toSide":"top","color":"4","label":"Sync Sheet"}, - {"id":"30b56dee8eaee8e0","fromNode":"6c88b3b3171c2cfa","fromSide":"right","toNode":"a2d30e3b136fe67f","toSide":"left"}, - {"id":"6d161a8eb1f548ea","fromNode":"a2d30e3b136fe67f","fromSide":"right","toNode":"f470377233f67831","toSide":"left"}, - {"id":"53de2e970f2d0369","fromNode":"b7b7094136fe8b92","fromSide":"right","toNode":"f470377233f67831","toSide":"bottom","label":"Attach"}, - {"id":"d09329144c5553cb","fromNode":"b7b7094136fe8b92","fromSide":"top","toNode":"a2d30e3b136fe67f","toSide":"bottom"}, - {"id":"c6e0c0de4c952411","fromNode":"e0151221c4e6aa9e","fromSide":"top","toNode":"f470377233f67831","toSide":"bottom","label":"Attach"}, - {"id":"8970328240b97cca","fromNode":"47f3144bfa56c450","fromSide":"right","toNode":"f470377233f67831","toSide":"right","color":"1","label":"Receive Result / File"}, - {"id":"f3ce9e450f86b2bd","fromNode":"f470377233f67831","fromSide":"top","toNode":"47f3144bfa56c450","toSide":"bottom","color":"4","label":"Connect \nRequest \nUpload File"}, - {"id":"77293990bd66ac7c","fromNode":"4ddd85f291925ef3","fromSide":"left","toNode":"f470377233f67831","toSide":"right","label":"Invoke"}, - {"id":"9db8ee2737251355","fromNode":"40743e1c3e19017b","fromSide":"left","toNode":"f470377233f67831","toSide":"right","label":"Invoke"} + {"id":"a9c05bbdb4004166","fromNode":"e9f3d1cb18045858","fromSide":"left","toNode":"6cb37b9c27eef97b","toSide":"right"}, + {"id":"deac8e1cf7c842ca","fromNode":"6cb37b9c27eef97b","fromSide":"left","toNode":"4a3d9c7d37cc4554","toSide":"right"}, + {"id":"807e796d5bce5449","fromNode":"953a678fd5f65526","fromSide":"left","toNode":"ab048c225f92a0b5","toSide":"right","label":"Link"}, + {"id":"8e7e03fd68275492","fromNode":"6cb37b9c27eef97b","fromSide":"left","toNode":"953a678fd5f65526","toSide":"right"}, + {"id":"0a61912dcd911a8b","fromNode":"e9f3d1cb18045858","fromSide":"left","toNode":"fa764e1f4b5523af","toSide":"right"}, + {"id":"f152956a93934f57","fromNode":"fa764e1f4b5523af","fromSide":"left","toNode":"9becfba9bd34cf72","toSide":"right"}, + {"id":"d69dce6728df4d67","fromNode":"9becfba9bd34cf72","fromSide":"right","toNode":"50615e432257da4e","toSide":"left","label":"Mapping"}, + {"id":"a2be52fcfa51b570","fromNode":"4a3d9c7d37cc4554","fromSide":"bottom","toNode":"953a678fd5f65526","toSide":"top","label":"Mapping"}, + {"id":"058a9f7ada6e1dd7","fromNode":"953a678fd5f65526","fromSide":"bottom","toNode":"50615e432257da4e","toSide":"top","label":"Clone"}, + {"id":"b54e283c3dfbf38c","fromNode":"fa764e1f4b5523af","fromSide":"left","toNode":"50615e432257da4e","toSide":"right","label":"Using"}, + {"id":"09ae4a12be630cc2","fromNode":"fa764e1f4b5523af","fromSide":"top","toNode":"6cb37b9c27eef97b","toSide":"bottom","label":"Connect"}, + {"id":"97b042a476817b7e","fromNode":"7f945acd64826a42","fromSide":"left","toNode":"53fc44e76bcbd0a0","toSide":"right"}, + {"id":"7e021ee8877a3278","fromNode":"53fc44e76bcbd0a0","fromSide":"left","toNode":"e9f3d1cb18045858","toSide":"right"}, + {"id":"ce688f1b81b7cf2e","fromNode":"1e0a1eea644ecea3","fromSide":"left","toNode":"e9f3d1cb18045858","toSide":"right"}, + {"id":"ab34939a3302f7fa","fromNode":"cd1286af92172d06","fromSide":"left","toNode":"79182419068c2908","toSide":"right"}, + {"id":"ce8c9124000a75f8","fromNode":"79182419068c2908","fromSide":"left","toNode":"e9f3d1cb18045858","toSide":"right"} ] } \ No newline at end of file -- cgit From 547c154ce30d52526ff58d1fc310344ccc1b3a18 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 6 Oct 2025 03:25:29 +0800 Subject: Add framework diagram to README documentation - Include FRAMEWORK_CANVAS.png in both English and Chinese README - Position diagram after introduction for better visual explanation - Enhance documentation with visual representation of system architecture --- README.md | 2 ++ README_zh_CN.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index d94a29a..7484def 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ ​ `JustEnoughVCS` adheres to the "**Just Enough**" philosophy, aiming to achieve collaborative security through architectural design. Centered around a **Virtual File System** and **Sheet Isolation**, it provides each creator with a focused, distraction-free workspace, making collaboration natural and simple. +![img](docs/images/FRAMEWORK_CANVAS.png) + ## Virtual File System ​ The Virtual File System is the foundation of `JustEnoughVCS`. Each file is identified by a globally unique `VirtualFileId`, decoupled from its physical path. It comprehensively records: diff --git a/README_zh_CN.md b/README_zh_CN.md index b3cff12..4781723 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -10,6 +10,8 @@ ​ `JustEnoughVCS` 遵循"**Just Enough**"的理念,旨在通过架构设计来实现协作安全。它以**虚拟文件系统**和**表隔离**为核心,为每个创作者提供专注、无干扰的工作空间,让协作变得自然且简单。 +![img](docs/images/FRAMEWORK_CANVAS.png) + ## 虚拟文件系统 (Virtual File System) ​ 虚拟文件系统是 `JustEnoughVCS` 的基础。每个文件由一个全局唯一的 `VirtualFileId` 标识,与其物理路径解耦。它全面记录: -- cgit From ce4545a21d435d63827fb972406e749354ac687a Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 6 Oct 2025 03:25:36 +0800 Subject: Add documentation directory with framework diagram - Create docs/images directory for project documentation - Include FRAMEWORK_CANVAS.png image file - Prepare structure for future documentation assets --- docs/images/FRAMEWORK_CANVAS.png | Bin 0 -> 182753 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/FRAMEWORK_CANVAS.png diff --git a/docs/images/FRAMEWORK_CANVAS.png b/docs/images/FRAMEWORK_CANVAS.png new file mode 100644 index 0000000..9f7ebbe Binary files /dev/null and b/docs/images/FRAMEWORK_CANVAS.png differ -- cgit From 87c3ec3fdcbd2294c3b9258d28ff47959e6eff68 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 6 Oct 2025 04:11:34 +0800 Subject: Move vcs crate to vcs_data for better separation of concerns - Rename vcs crate to vcs_data to clearly define data layer - Maintain all existing data structures and functionality - Update dependencies to include action_system integration - Preserve test structure in vcs_data_test directory --- crates/vcs_data/Cargo.toml | 22 + crates/vcs_data/src/constants.rs | 54 +++ crates/vcs_data/src/current.rs | 78 ++++ crates/vcs_data/src/data.rs | 5 + crates/vcs_data/src/data/local.rs | 100 +++++ crates/vcs_data/src/data/local/config.rs | 53 +++ crates/vcs_data/src/data/member.rs | 69 +++ crates/vcs_data/src/data/sheet.rs | 347 +++++++++++++++ crates/vcs_data/src/data/user.rs | 28 ++ crates/vcs_data/src/data/user/accounts.rs | 164 +++++++ crates/vcs_data/src/data/vault.rs | 146 +++++++ crates/vcs_data/src/data/vault/config.rs | 77 ++++ crates/vcs_data/src/data/vault/member.rs | 140 ++++++ crates/vcs_data/src/data/vault/sheets.rs | 268 ++++++++++++ crates/vcs_data/src/data/vault/virtual_file.rs | 473 +++++++++++++++++++++ crates/vcs_data/src/lib.rs | 5 + crates/vcs_data/todo.txt | 36 ++ crates/vcs_data/vcs_data_test/Cargo.toml | 13 + crates/vcs_data/vcs_data_test/lib.rs | 11 + crates/vcs_data/vcs_data_test/src/lib.rs | 27 ++ ...local_workspace_setup_and_account_management.rs | 248 +++++++++++ ...st_sheet_creation_management_and_persistence.rs | 307 +++++++++++++ .../src/test_vault_setup_and_member_register.rs | 67 +++ .../src/test_virtual_file_creation_and_update.rs | 162 +++++++ 24 files changed, 2900 insertions(+) create mode 100644 crates/vcs_data/Cargo.toml create mode 100644 crates/vcs_data/src/constants.rs create mode 100644 crates/vcs_data/src/current.rs create mode 100644 crates/vcs_data/src/data.rs create mode 100644 crates/vcs_data/src/data/local.rs create mode 100644 crates/vcs_data/src/data/local/config.rs create mode 100644 crates/vcs_data/src/data/member.rs create mode 100644 crates/vcs_data/src/data/sheet.rs create mode 100644 crates/vcs_data/src/data/user.rs create mode 100644 crates/vcs_data/src/data/user/accounts.rs create mode 100644 crates/vcs_data/src/data/vault.rs create mode 100644 crates/vcs_data/src/data/vault/config.rs create mode 100644 crates/vcs_data/src/data/vault/member.rs create mode 100644 crates/vcs_data/src/data/vault/sheets.rs create mode 100644 crates/vcs_data/src/data/vault/virtual_file.rs create mode 100644 crates/vcs_data/src/lib.rs create mode 100644 crates/vcs_data/todo.txt create mode 100644 crates/vcs_data/vcs_data_test/Cargo.toml create mode 100644 crates/vcs_data/vcs_data_test/lib.rs create mode 100644 crates/vcs_data/vcs_data_test/src/lib.rs create mode 100644 crates/vcs_data/vcs_data_test/src/test_local_workspace_setup_and_account_management.rs create mode 100644 crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs create mode 100644 crates/vcs_data/vcs_data_test/src/test_vault_setup_and_member_register.rs create mode 100644 crates/vcs_data/vcs_data_test/src/test_virtual_file_creation_and_update.rs diff --git a/crates/vcs_data/Cargo.toml b/crates/vcs_data/Cargo.toml new file mode 100644 index 0000000..07f1a6a --- /dev/null +++ b/crates/vcs_data/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "vcs_data" +edition = "2024" +version.workspace = true + +[dependencies] +tcp_connection = { path = "../utils/tcp_connection" } +cfg_file = { path = "../utils/cfg_file", features = ["default"] } +string_proc = { path = "../utils/string_proc" } +action_system = { path = "../system_action" } + +# Identity +uuid = { version = "1.18.1", features = ["v4", "serde"] } + +# Serialization +serde = { version = "1.0.219", features = ["derive"] } + +# Async & Networking +tokio = { version = "1.46.1", features = ["full"] } + +# Filesystem +dirs = "6.0.0" diff --git a/crates/vcs_data/src/constants.rs b/crates/vcs_data/src/constants.rs new file mode 100644 index 0000000..5e147c4 --- /dev/null +++ b/crates/vcs_data/src/constants.rs @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------------------- +// + +// Project +pub const PATH_TEMP: &str = "./.temp/"; + +// Default Port +pub const PORT: u16 = 25331; + +// Vault Host Name +pub const VAULT_HOST_NAME: &str = "host"; + +// Server +// Server - Vault (Main) +pub const SERVER_FILE_VAULT: &str = "./vault.toml"; // crates::env::vault::vault_config + +// Server - Sheets +pub const REF_SHEET_NAME: &str = "ref"; +pub const SERVER_PATH_SHEETS: &str = "./sheets/"; +pub const SERVER_FILE_SHEET: &str = "./sheets/{sheet-name}.yaml"; + +// Server - Members +pub const SERVER_PATH_MEMBERS: &str = "./members/"; +pub const SERVER_PATH_MEMBER_PUB: &str = "./key/"; +pub const SERVER_FILE_MEMBER_INFO: &str = "./members/{member_id}.toml"; // crates::env::member::manager +pub const SERVER_FILE_MEMBER_PUB: &str = "./key/{member_id}.pem"; // crates::utils::tcp_connection::instance + +// Server - Virtual File Storage +pub const SERVER_PATH_VF_TEMP: &str = "./.temp/{temp_name}"; +pub const SERVER_PATH_VF_ROOT: &str = "./storage/"; +pub const SERVER_PATH_VF_STORAGE: &str = "./storage/{vf_index}/{vf_id}/"; +pub const SERVER_FILE_VF_VERSION_INSTANCE: &str = "./storage/{vf_index}/{vf_id}/{vf_version}.rf"; +pub const SERVER_FILE_VF_META: &str = "./storage/{vf_index}/{vf_id}/meta.yaml"; + +pub const SERVER_FILE_README: &str = "./README.md"; + +// ------------------------------------------------------------------------------------- + +// Client +pub const CLIENT_PATH_WORKSPACE_ROOT: &str = "./.jv/"; + +// Client - Workspace (Main) +pub const CLIENT_FILE_WORKSPACE: &str = "./.jv/workspace.toml"; // crates::env::local::local_config + +// Client - Other +pub const CLIENT_FILE_IGNOREFILES: &str = ".jgnore .gitignore"; // Support gitignore file. +pub const CLIENT_FILE_README: &str = "./README.md"; + +// ------------------------------------------------------------------------------------- + +// User - Verify (Documents path) +pub const USER_FILE_ACCOUNTS: &str = "./accounts/"; +pub const USER_FILE_KEY: &str = "./accounts/{self_id}_private.pem"; +pub const USER_FILE_MEMBER: &str = "./accounts/{self_id}.toml"; diff --git a/crates/vcs_data/src/current.rs b/crates/vcs_data/src/current.rs new file mode 100644 index 0000000..97b5058 --- /dev/null +++ b/crates/vcs_data/src/current.rs @@ -0,0 +1,78 @@ +use crate::constants::*; +use std::io::{self, Error}; +use std::{env::set_current_dir, path::PathBuf}; + +/// Find the nearest vault or local workspace and correct the `current_dir` to it +pub fn correct_current_dir() -> Result<(), io::Error> { + if let Some(local_workspace) = current_local_path() { + set_current_dir(local_workspace)?; + return Ok(()); + } + if let Some(vault) = current_vault_path() { + set_current_dir(vault)?; + return Ok(()); + } + Err(Error::new( + io::ErrorKind::NotFound, + "Could not find any vault or local workspace!", + )) +} + +/// Get the nearest Vault directory from `current_dir` +pub fn current_vault_path() -> Option { + let current_dir = std::env::current_dir().ok()?; + find_vault_path(current_dir) +} + +/// Get the nearest local workspace from `current_dir` +pub fn current_local_path() -> Option { + let current_dir = std::env::current_dir().ok()?; + find_local_path(current_dir) +} + +/// Get the nearest Vault directory from the specified path +pub fn find_vault_path(path: impl Into) -> Option { + let mut current_path = path.into(); + let vault_file = SERVER_FILE_VAULT; + + loop { + let vault_toml_path = current_path.join(vault_file); + if vault_toml_path.exists() { + return Some(current_path); + } + + if let Some(parent) = current_path.parent() { + current_path = parent.to_path_buf(); + } else { + break; + } + } + + None +} + +/// Get the nearest local workspace from the specified path +pub fn find_local_path(path: impl Into) -> Option { + let mut current_path = path.into(); + let workspace_dir = CLIENT_PATH_WORKSPACE_ROOT; + + loop { + let jvc_path = current_path.join(workspace_dir); + if jvc_path.exists() { + return Some(current_path); + } + + if let Some(parent) = current_path.parent() { + current_path = parent.to_path_buf(); + } else { + break; + } + } + + None +} + +/// Get the system's document directory and join with .just_enough_vcs +pub fn current_doc_dir() -> Option { + dirs::document_dir().map(|path| path.join(".just_enough_vcs")) +} diff --git a/crates/vcs_data/src/data.rs b/crates/vcs_data/src/data.rs new file mode 100644 index 0000000..ed9383a --- /dev/null +++ b/crates/vcs_data/src/data.rs @@ -0,0 +1,5 @@ +pub mod local; +pub mod member; +pub mod sheet; +pub mod user; +pub mod vault; diff --git a/crates/vcs_data/src/data/local.rs b/crates/vcs_data/src/data/local.rs new file mode 100644 index 0000000..1c99832 --- /dev/null +++ b/crates/vcs_data/src/data/local.rs @@ -0,0 +1,100 @@ +use std::{env::current_dir, path::PathBuf}; + +use cfg_file::config::ConfigFile; +use tokio::fs; + +use crate::{ + constants::{CLIENT_FILE_README, CLIENT_FILE_WORKSPACE}, + current::{current_local_path, find_local_path}, + data::local::config::LocalConfig, +}; + +pub mod config; + +pub struct LocalWorkspace { + config: LocalConfig, + local_path: PathBuf, +} + +impl LocalWorkspace { + /// Get the path of the local workspace. + pub fn local_path(&self) -> &PathBuf { + &self.local_path + } + + /// Initialize local workspace. + pub fn init(config: LocalConfig, local_path: impl Into) -> Option { + let local_path = find_local_path(local_path)?; + Some(Self { config, local_path }) + } + + /// Initialize local workspace in the current directory. + pub fn init_current_dir(config: LocalConfig) -> Option { + let local_path = current_local_path()?; + Some(Self { config, local_path }) + } + + /// Setup local workspace + pub async fn setup_local_workspace( + local_path: impl Into, + ) -> Result<(), std::io::Error> { + let local_path: PathBuf = local_path.into(); + + // Ensure directory is empty + if local_path.exists() && local_path.read_dir()?.next().is_some() { + return Err(std::io::Error::new( + std::io::ErrorKind::DirectoryNotEmpty, + "DirectoryNotEmpty", + )); + } + + // 1. Setup config + let config = LocalConfig::default(); + LocalConfig::write_to(&config, local_path.join(CLIENT_FILE_WORKSPACE)).await?; + + // 2. Setup README.md + let readme_content = "\ +# JustEnoughVCS Local Workspace + +This directory is a **Local Workspace** managed by `JustEnoughVCS`. All files and subdirectories within this scope can be version-controlled using the `JustEnoughVCS` CLI or GUI tools, with the following exceptions: + +- The `.jv` directory +- Any files or directories excluded via `.jgnore` or `.gitignore` + +> ⚠️ **Warning** +> +> Files in this workspace will be uploaded to the upstream server. Please ensure you fully trust this server before proceeding. + +## Access Requirements + +To use `JustEnoughVCS` with this workspace, you must have: + +- **A registered user ID** with the upstream server +- **Your private key** properly configured locally +- **Your public key** stored in the server's public key directory + +Without these credentials, the server will reject all access requests. + +## Support + +- **Permission or access issues?** → Contact your server administrator +- **Tooling problems or bugs?** → Reach out to the development team via [GitHub Issues](https://github.com/JustEnoughVCS/VersionControl/issues) +- **Documentation**: Visit our repository for full documentation + +------ + +*Thank you for using JustEnoughVCS!* +".to_string() + .trim() + .to_string(); + fs::write(local_path.join(CLIENT_FILE_README), readme_content).await?; + + Ok(()) + } + + /// Setup local workspace in current directory + pub async fn setup_local_workspacecurrent_dir() -> Result<(), std::io::Error> { + Self::setup_local_workspace(current_dir()?).await?; + Ok(()) + } +} diff --git a/crates/vcs_data/src/data/local/config.rs b/crates/vcs_data/src/data/local/config.rs new file mode 100644 index 0000000..5444047 --- /dev/null +++ b/crates/vcs_data/src/data/local/config.rs @@ -0,0 +1,53 @@ +use cfg_file::ConfigFile; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; + +use crate::constants::CLIENT_FILE_WORKSPACE; +use crate::constants::PORT; +use crate::data::member::MemberId; + +#[derive(Serialize, Deserialize, ConfigFile)] +#[cfg_file(path = CLIENT_FILE_WORKSPACE)] +pub struct LocalConfig { + /// The upstream address, representing the upstream address of the local workspace, + /// to facilitate timely retrieval of new updates from the upstream source. + upstream_addr: SocketAddr, + + /// The member ID used by the current local workspace. + /// This ID will be used to verify access permissions when connecting to the upstream server. + using_account: MemberId, +} + +impl Default for LocalConfig { + fn default() -> Self { + Self { + upstream_addr: SocketAddr::V4(std::net::SocketAddrV4::new( + std::net::Ipv4Addr::new(127, 0, 0, 1), + PORT, + )), + using_account: "unknown".to_string(), + } + } +} + +impl LocalConfig { + /// Set the vault address. + pub fn set_vault_addr(&mut self, addr: SocketAddr) { + self.upstream_addr = addr; + } + + /// Get the vault address. + pub fn vault_addr(&self) -> SocketAddr { + self.upstream_addr + } + + /// Set the currently used account + pub fn set_current_account(&mut self, account: MemberId) { + self.using_account = account; + } + + /// Get the currently used account + pub fn current_account(&self) -> MemberId { + self.using_account.clone() + } +} diff --git a/crates/vcs_data/src/data/member.rs b/crates/vcs_data/src/data/member.rs new file mode 100644 index 0000000..b5136a1 --- /dev/null +++ b/crates/vcs_data/src/data/member.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; + +use cfg_file::ConfigFile; +use serde::{Deserialize, Serialize}; +use string_proc::snake_case; + +pub type MemberId = String; + +#[derive(Debug, Eq, Clone, ConfigFile, Serialize, Deserialize)] +pub struct Member { + /// Member ID, the unique identifier of the member + id: String, + + /// Member metadata + metadata: HashMap, +} + +impl Default for Member { + fn default() -> Self { + Self::new("default_user") + } +} + +impl PartialEq for Member { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl std::fmt::Display for Member { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.id) + } +} + +impl std::convert::AsRef for Member { + fn as_ref(&self) -> &str { + &self.id + } +} + +impl Member { + /// Create member struct by id + pub fn new(new_id: impl Into) -> Self { + Self { + id: snake_case!(new_id.into()), + metadata: HashMap::new(), + } + } + + /// Get member id + pub fn id(&self) -> String { + self.id.clone() + } + + /// Get metadata + pub fn metadata(&self, key: impl Into) -> Option<&String> { + self.metadata.get(&key.into()) + } + + /// Set metadata + pub fn set_metadata( + &mut self, + key: impl AsRef, + value: impl Into, + ) -> Option { + self.metadata.insert(key.as_ref().to_string(), value.into()) + } +} diff --git a/crates/vcs_data/src/data/sheet.rs b/crates/vcs_data/src/data/sheet.rs new file mode 100644 index 0000000..a6220c9 --- /dev/null +++ b/crates/vcs_data/src/data/sheet.rs @@ -0,0 +1,347 @@ +use std::{collections::HashMap, path::PathBuf}; + +use cfg_file::{ConfigFile, config::ConfigFile}; +use serde::{Deserialize, Serialize}; +use string_proc::simple_processer::sanitize_file_path; + +use crate::{ + constants::SERVER_FILE_SHEET, + data::{ + member::MemberId, + vault::{Vault, virtual_file::VirtualFileId}, + }, +}; + +pub type SheetName = String; +pub type SheetPathBuf = PathBuf; +pub type InputName = String; +pub type InputRelativePathBuf = PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize, Eq)] +pub struct InputPackage { + /// Name of the input package + pub name: InputName, + + /// The sheet from which this input package was created + pub from: SheetName, + + /// Files in this input package with their relative paths and virtual file IDs + pub files: Vec<(InputRelativePathBuf, VirtualFileId)>, +} + +impl PartialEq for InputPackage { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +const SHEET_NAME: &str = "{sheet-name}"; + +pub struct Sheet<'a> { + /// The name of the current sheet + pub(crate) name: SheetName, + + /// Sheet data + pub(crate) data: SheetData, + + /// Sheet path + pub(crate) vault_reference: &'a Vault, +} + +#[derive(Default, Serialize, Deserialize, ConfigFile)] +pub struct SheetData { + /// The holder of the current sheet, who has full operation rights to the sheet mapping + pub(crate) holder: MemberId, + + /// Inputs + pub(crate) inputs: Vec, + + /// Mapping of sheet paths to virtual file IDs + pub(crate) mapping: HashMap, +} + +impl<'a> Sheet<'a> { + /// Get the holder of this sheet + pub fn holder(&self) -> &MemberId { + &self.data.holder + } + + /// Get the inputs of this sheet + pub fn inputs(&self) -> &Vec { + &self.data.inputs + } + + /// Get the names of the inputs of this sheet + pub fn input_names(&self) -> Vec { + self.data + .inputs + .iter() + .map(|input| input.name.clone()) + .collect() + } + + /// Get the mapping of this sheet + pub fn mapping(&self) -> &HashMap { + &self.data.mapping + } + + /// Add an input package to the sheet + pub fn add_input(&mut self, input_package: InputPackage) -> Result<(), std::io::Error> { + if self.data.inputs.iter().any(|input| input == &input_package) { + return Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!("Input package '{}' already exists", input_package.name), + )); + } + self.data.inputs.push(input_package); + Ok(()) + } + + /// Deny and remove an input package from the sheet + pub fn deny_input(&mut self, input_name: &InputName) -> Option { + self.data + .inputs + .iter() + .position(|input| input.name == *input_name) + .map(|pos| self.data.inputs.remove(pos)) + } + + /// Accept an input package and insert to the sheet + pub fn accept_import( + &mut self, + input_name: &InputName, + insert_to: &SheetPathBuf, + ) -> Result<(), std::io::Error> { + // Remove inputs + let input = self + .inputs() + .iter() + .position(|input| input.name == *input_name) + .map(|pos| self.data.inputs.remove(pos)); + + // Ensure input is not empty + let Some(input) = input else { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Empty inputs.", + )); + }; + + // Insert to sheet + for (relative_path, virtual_file_id) in input.files { + let _ = self.add_mapping(insert_to.join(relative_path), virtual_file_id); + } + + Ok(()) + } + + /// Add (or Edit) a mapping entry to the sheet + /// + /// This operation performs safety checks to ensure the member has the right to add the mapping: + /// 1. If the virtual file ID doesn't exist in the vault, the mapping is added directly + /// 2. If the virtual file exists, check if the member has edit rights to the virtual file + /// 3. If member has edit rights, the mapping is not allowed to be modified and returns an error + /// 4. If member doesn't have edit rights, the mapping is allowed (member is giving up the file) + /// + /// Note: Full validation adds overhead - avoid frequent calls + pub async fn add_mapping( + &mut self, + sheet_path: SheetPathBuf, + virtual_file_id: VirtualFileId, + ) -> Result<(), std::io::Error> { + // Check if the virtual file exists in the vault + if self.vault_reference.virtual_file(&virtual_file_id).is_err() { + // Virtual file doesn't exist, add the mapping directly + self.data.mapping.insert(sheet_path, virtual_file_id); + return Ok(()); + } + + // Check if the holder has edit rights to the virtual file + match self + .vault_reference + .has_virtual_file_edit_right(self.holder(), &virtual_file_id) + .await + { + Ok(false) => { + // Holder doesn't have rights, add the mapping (member is giving up the file) + self.data.mapping.insert(sheet_path, virtual_file_id); + Ok(()) + } + Ok(true) => { + // Holder has edit rights, don't allow modifying the mapping + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Member has edit rights to the virtual file, cannot modify mapping", + )) + } + Err(_) => { + // Error checking rights, don't allow modifying the mapping + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to check virtual file edit rights", + )) + } + } + } + + /// Remove a mapping entry from the sheet + /// + /// This operation performs safety checks to ensure the member has the right to remove the mapping: + /// 1. Member must NOT have edit rights to the virtual file to release it (ensuring clear ownership) + /// 2. If the virtual file doesn't exist, the mapping is removed but no ID is returned + /// 3. If member has no edit rights and the file exists, returns the removed virtual file ID + /// + /// Note: Full validation adds overhead - avoid frequent calls + pub async fn remove_mapping(&mut self, sheet_path: &SheetPathBuf) -> Option { + let virtual_file_id = match self.data.mapping.get(sheet_path) { + Some(id) => id, + None => { + // The mapping entry doesn't exist, nothing to remove + return None; + } + }; + + // Check if the virtual file exists in the vault + if self.vault_reference.virtual_file(virtual_file_id).is_err() { + // Virtual file doesn't exist, remove the mapping and return None + self.data.mapping.remove(sheet_path); + return None; + } + + // Check if the holder has edit rights to the virtual file + match self + .vault_reference + .has_virtual_file_edit_right(self.holder(), virtual_file_id) + .await + { + Ok(false) => { + // Holder doesn't have rights, remove and return the virtual file ID + self.data.mapping.remove(sheet_path) + } + Ok(true) => { + // Holder has edit rights, don't remove the mapping + None + } + Err(_) => { + // Error checking rights, don't remove the mapping + None + } + } + } + + /// Persist the sheet to disk + /// + /// Why not use a reference? + /// Because I don't want a second instance of the sheet to be kept in memory. + /// If needed, please deserialize and reload it. + pub async fn persist(self) -> Result<(), std::io::Error> { + SheetData::write_to(&self.data, self.sheet_path()).await + } + + /// Get the path to the sheet file + pub fn sheet_path(&self) -> PathBuf { + Sheet::sheet_path_with_name(self.vault_reference, &self.name) + } + + /// Get the path to the sheet file with the given name + pub fn sheet_path_with_name(vault: &Vault, name: impl AsRef) -> PathBuf { + vault + .vault_path() + .join(SERVER_FILE_SHEET.replace(SHEET_NAME, name.as_ref())) + } + + /// Export files from the current sheet as an InputPackage for importing into other sheets + /// + /// This is the recommended way to create InputPackages. It takes a list of sheet paths + /// and generates an InputPackage with optimized relative paths by removing the longest + /// common prefix from all provided paths, then placing the files under a directory + /// named with the output_name. + /// + /// # Example + /// Given paths: + /// - `MyProject/Art/Character/Model/final.fbx` + /// - `MyProject/Art/Character/Texture/final.png` + /// - `MyProject/Art/Character/README.md` + /// + /// With output_name = "MyExport", the resulting package will contain: + /// - `MyExport/Model/final.fbx` + /// - `MyExport/Texture/final.png` + /// - `MyExport/README.md` + /// + /// # Arguments + /// * `output_name` - Name of the output package (will be used as the root directory) + /// * `paths` - List of sheet paths to include in the package + /// + /// # Returns + /// Returns an InputPackage containing the exported files with optimized paths, + /// or an error if paths are empty or files are not found in the sheet mapping + pub fn output_mappings( + &self, + output_name: InputName, + paths: &[SheetPathBuf], + ) -> Result { + let output_name = sanitize_file_path(output_name); + + // Return error for empty paths since there's no need to generate an empty package + if paths.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Cannot generate output package with empty paths", + )); + } + + // Find the longest common prefix among all paths + let common_prefix = Self::find_longest_common_prefix(paths); + + // Create output files with optimized relative paths + let files = paths + .iter() + .map(|path| { + let relative_path = path.strip_prefix(&common_prefix).unwrap_or(path); + let output_path = PathBuf::from(&output_name).join(relative_path); + + self.data + .mapping + .get(path) + .map(|vfid| (output_path, vfid.clone())) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("File not found: {:?}", path), + ) + }) + }) + .collect::, _>>()?; + + Ok(InputPackage { + name: output_name, + from: self.name.clone(), + files, + }) + } + + /// Helper function to find the longest common prefix among all paths + fn find_longest_common_prefix(paths: &[SheetPathBuf]) -> PathBuf { + if paths.is_empty() { + return PathBuf::new(); + } + + let first_path = &paths[0]; + let mut common_components = Vec::new(); + + for (component_idx, first_component) in first_path.components().enumerate() { + for path in paths.iter().skip(1) { + if let Some(component) = path.components().nth(component_idx) { + if component != first_component { + return common_components.into_iter().collect(); + } + } else { + return common_components.into_iter().collect(); + } + } + common_components.push(first_component); + } + + common_components.into_iter().collect() + } +} diff --git a/crates/vcs_data/src/data/user.rs b/crates/vcs_data/src/data/user.rs new file mode 100644 index 0000000..0abd098 --- /dev/null +++ b/crates/vcs_data/src/data/user.rs @@ -0,0 +1,28 @@ +use crate::current::current_doc_dir; +use std::path::PathBuf; + +pub mod accounts; + +pub struct UserDirectory { + local_path: PathBuf, +} + +impl UserDirectory { + /// Create a user ditectory struct from the current system's document directory + pub fn current_doc_dir() -> Option { + Some(UserDirectory { + local_path: current_doc_dir()?, + }) + } + + /// Create a user directory struct from a specified directory path + /// Returns None if the directory does not exist + pub fn from_path>(path: P) -> Option { + let local_path = path.into(); + if local_path.exists() { + Some(UserDirectory { local_path }) + } else { + None + } + } +} diff --git a/crates/vcs_data/src/data/user/accounts.rs b/crates/vcs_data/src/data/user/accounts.rs new file mode 100644 index 0000000..d77bc02 --- /dev/null +++ b/crates/vcs_data/src/data/user/accounts.rs @@ -0,0 +1,164 @@ +use std::{ + fs, + io::{Error, ErrorKind}, + path::PathBuf, +}; + +use cfg_file::config::ConfigFile; + +use crate::{ + constants::{USER_FILE_ACCOUNTS, USER_FILE_KEY, USER_FILE_MEMBER}, + data::{ + member::{Member, MemberId}, + user::UserDirectory, + }, +}; + +const SELF_ID: &str = "{self_id}"; + +/// Account Management +impl UserDirectory { + /// Read account from configuration file + pub async fn account(&self, id: &MemberId) -> Result { + if let Some(cfg_file) = self.account_cfg(id) { + let member = Member::read_from(cfg_file).await?; + return Ok(member); + } + + Err(Error::new(ErrorKind::NotFound, "Account not found!")) + } + + /// List all account IDs in the user directory + pub fn account_ids(&self) -> Result, std::io::Error> { + let accounts_path = self + .local_path + .join(USER_FILE_ACCOUNTS.replace(SELF_ID, "")); + + if !accounts_path.exists() { + return Ok(Vec::new()); + } + + let mut account_ids = Vec::new(); + + for entry in fs::read_dir(accounts_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() + && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) + && path.extension().and_then(|s| s.to_str()) == Some("toml") + { + // Remove the "_private" suffix from key files if present + let account_id = file_name.replace("_private", ""); + account_ids.push(account_id); + } + } + + Ok(account_ids) + } + + /// Get all accounts + /// This method will read and deserialize account information, please pay attention to performance issues + pub async fn accounts(&self) -> Result, std::io::Error> { + let mut accounts = Vec::new(); + + for account_id in self.account_ids()? { + if let Ok(account) = self.account(&account_id).await { + accounts.push(account); + } + } + + Ok(accounts) + } + + /// Update account info + pub async fn update_account(&self, member: Member) -> Result<(), std::io::Error> { + // Ensure account exist + if self.account_cfg(&member.id()).is_some() { + let account_cfg_path = self.account_cfg_path(&member.id()); + Member::write_to(&member, account_cfg_path).await?; + return Ok(()); + } + + Err(Error::new(ErrorKind::NotFound, "Account not found!")) + } + + /// Register an account to user directory + pub async fn register_account(&self, member: Member) -> Result<(), std::io::Error> { + // Ensure account not exist + if self.account_cfg(&member.id()).is_some() { + return Err(Error::new( + ErrorKind::DirectoryNotEmpty, + format!("Account `{}` already registered!", member.id()), + )); + } + + // Ensure accounts directory exists + let accounts_dir = self + .local_path + .join(USER_FILE_ACCOUNTS.replace(SELF_ID, "")); + if !accounts_dir.exists() { + fs::create_dir_all(&accounts_dir)?; + } + + // Write config file to accounts dir + let account_cfg_path = self.account_cfg_path(&member.id()); + Member::write_to(&member, account_cfg_path).await?; + + Ok(()) + } + + /// Remove account from user directory + pub fn remove_account(&self, id: &MemberId) -> Result<(), std::io::Error> { + // Remove config file if exists + if let Some(account_cfg_path) = self.account_cfg(id) { + fs::remove_file(account_cfg_path)?; + } + + // Remove private key file if exists + if let Some(private_key_path) = self.account_private_key(id) + && private_key_path.exists() + { + fs::remove_file(private_key_path)?; + } + + Ok(()) + } + + /// Try to get the account's configuration file to determine if the account exists + pub fn account_cfg(&self, id: &MemberId) -> Option { + let cfg_file = self.account_cfg_path(id); + if cfg_file.exists() { + Some(cfg_file) + } else { + None + } + } + + /// Try to get the account's private key file to determine if the account has a private key + pub fn account_private_key(&self, id: &MemberId) -> Option { + let key_file = self.account_private_key_path(id); + if key_file.exists() { + Some(key_file) + } else { + None + } + } + + /// Check if account has private key + pub fn has_private_key(&self, id: &MemberId) -> bool { + self.account_private_key(id).is_some() + } + + /// Get the account's configuration file path, but do not check if the file exists + pub fn account_cfg_path(&self, id: &MemberId) -> PathBuf { + self.local_path + .join(USER_FILE_MEMBER.replace(SELF_ID, id.to_string().as_str())) + } + + /// Get the account's private key file path, but do not check if the file exists + pub fn account_private_key_path(&self, id: &MemberId) -> PathBuf { + self.local_path + .join(USER_FILE_KEY.replace(SELF_ID, id.to_string().as_str())) + } +} diff --git a/crates/vcs_data/src/data/vault.rs b/crates/vcs_data/src/data/vault.rs new file mode 100644 index 0000000..5d17a81 --- /dev/null +++ b/crates/vcs_data/src/data/vault.rs @@ -0,0 +1,146 @@ +use std::{ + env::current_dir, + fs::{self, create_dir_all}, + path::PathBuf, +}; + +use cfg_file::config::ConfigFile; + +use crate::{ + constants::{ + SERVER_FILE_README, SERVER_FILE_VAULT, SERVER_PATH_MEMBER_PUB, SERVER_PATH_MEMBERS, + SERVER_PATH_SHEETS, SERVER_PATH_VF_ROOT, VAULT_HOST_NAME, + }, + current::{current_vault_path, find_vault_path}, + data::{member::Member, vault::config::VaultConfig}, +}; + +pub mod config; +pub mod member; +pub mod sheets; +pub mod virtual_file; + +pub struct Vault { + config: VaultConfig, + vault_path: PathBuf, +} + +impl Vault { + /// Get vault path + pub fn vault_path(&self) -> &PathBuf { + &self.vault_path + } + + /// Initialize vault + pub fn init(config: VaultConfig, vault_path: impl Into) -> Option { + let vault_path = find_vault_path(vault_path)?; + Some(Self { config, vault_path }) + } + + /// Initialize vault + pub fn init_current_dir(config: VaultConfig) -> Option { + let vault_path = current_vault_path()?; + Some(Self { config, vault_path }) + } + + /// Setup vault + pub async fn setup_vault(vault_path: impl Into) -> Result<(), std::io::Error> { + let vault_path: PathBuf = vault_path.into(); + + // Ensure directory is empty + if vault_path.exists() && vault_path.read_dir()?.next().is_some() { + return Err(std::io::Error::new( + std::io::ErrorKind::DirectoryNotEmpty, + "DirectoryNotEmpty", + )); + } + + // 1. Setup main config + let config = VaultConfig::default(); + VaultConfig::write_to(&config, vault_path.join(SERVER_FILE_VAULT)).await?; + + // 2. Setup sheets directory + create_dir_all(vault_path.join(SERVER_PATH_SHEETS))?; + + // 3. Setup key directory + create_dir_all(vault_path.join(SERVER_PATH_MEMBER_PUB))?; + + // 4. Setup member directory + create_dir_all(vault_path.join(SERVER_PATH_MEMBERS))?; + + // 5. Setup storage directory + create_dir_all(vault_path.join(SERVER_PATH_VF_ROOT))?; + + let Some(vault) = Vault::init(config, &vault_path) else { + return Err(std::io::Error::other("Failed to initialize vault")); + }; + + // 6. Create host member + vault + .register_member_to_vault(Member::new(VAULT_HOST_NAME)) + .await?; + + // 7. Setup reference sheet + vault + .create_sheet(&"ref".to_string(), &VAULT_HOST_NAME.to_string()) + .await?; + + // Final, generate README.md + let readme_content = format!( + "\ +# JustEnoughVCS Server Setup + +This directory contains the server configuration and data for `JustEnoughVCS`. + +## User Authentication +To allow users to connect to this server, place their public keys in the `{}` directory. +Each public key file should be named `{{member_id}}.pem` (e.g., `juliet.pem`), and contain the user's public key in PEM format. + +**ECDSA:** +```bash +openssl genpkey -algorithm ed25519 -out your_name_private.pem +openssl pkey -in your_name_private.pem -pubout -out your_name.pem +``` + +**RSA:** +```bash +openssl genpkey -algorithm RSA -out your_name_private.pem -pkeyopt rsa_keygen_bits:2048 +openssl pkey -in your_name_private.pem -pubout -out your_name.pem +``` + +**DSA:** +```bash +openssl genpkey -algorithm DSA -out your_name_private.pem -pkeyopt dsa_paramgen_bits:2048 +openssl pkey -in your_name_private.pem -pubout -out your_name.pem +``` + +Place only the `your_name.pem` file in the server's `./key/` directory, renamed to match the user's member ID. + +## File Storage +All version-controlled files (Virtual File) are stored in the `{}` directory. + +## License +This software is distributed under the MIT License. For complete license details, please see the main repository homepage. + +## Support +Repository: `https://github.com/JustEnoughVCS/VersionControl` +Please report any issues or questions on the GitHub issue tracker. + +## Thanks :) +Thank you for using `JustEnoughVCS!` + ", + SERVER_PATH_MEMBER_PUB, SERVER_PATH_VF_ROOT + ) + .trim() + .to_string(); + fs::write(vault_path.join(SERVER_FILE_README), readme_content)?; + + Ok(()) + } + + /// Setup vault in current directory + pub async fn setup_vault_current_dir() -> Result<(), std::io::Error> { + Self::setup_vault(current_dir()?).await?; + Ok(()) + } +} diff --git a/crates/vcs_data/src/data/vault/config.rs b/crates/vcs_data/src/data/vault/config.rs new file mode 100644 index 0000000..6eea25a --- /dev/null +++ b/crates/vcs_data/src/data/vault/config.rs @@ -0,0 +1,77 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use cfg_file::ConfigFile; +use serde::{Deserialize, Serialize}; + +use crate::constants::{PORT, SERVER_FILE_VAULT}; +use crate::data::member::{Member, MemberId}; + +#[derive(Serialize, Deserialize, ConfigFile)] +#[cfg_file(path = SERVER_FILE_VAULT)] +pub struct VaultConfig { + /// Vault name, which can be used as the project name and generally serves as a hint + vault_name: String, + + /// Vault admin id, a list of member id representing administrator identities + vault_admin_list: Vec, + + /// Vault server configuration, which will be loaded when connecting to the server + server_config: VaultServerConfig, +} + +#[derive(Serialize, Deserialize)] +pub struct VaultServerConfig { + /// Local IP address to bind to when the server starts + local_bind: IpAddr, + + /// TCP port to bind to when the server starts + port: u16, + + /// Whether to enable LAN discovery, allowing members on the same LAN to more easily find the upstream server + lan_discovery: bool, // TODO + + /// Authentication strength level + /// 0: Weakest - Anyone can claim any identity, fastest speed + /// 1: Basic - Any device can claim any registered identity, slightly faster + /// 2: Advanced - Uses asymmetric encryption, multiple devices can use key authentication to log in simultaneously, slightly slower + /// 3: Secure - Uses asymmetric encryption, only one device can use key for authentication at a time, much slower + /// Default is "Advanced", if using a lower security policy, ensure your server is only accessible by trusted devices + auth_strength: u8, // TODO +} + +impl Default for VaultConfig { + fn default() -> Self { + Self { + vault_name: "JustEnoughVault".to_string(), + vault_admin_list: Vec::new(), + server_config: VaultServerConfig { + local_bind: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + port: PORT, + lan_discovery: false, + auth_strength: 2, + }, + } + } +} + +/// Vault Management +impl VaultConfig { + // Change name of the vault. + pub fn change_name(&mut self, name: impl Into) { + self.vault_name = name.into() + } + + // Add admin + pub fn add_admin(&mut self, member: &Member) { + let uuid = member.id(); + if !self.vault_admin_list.contains(&uuid) { + self.vault_admin_list.push(uuid); + } + } + + // Remove admin + pub fn remove_admin(&mut self, member: &Member) { + let id = member.id(); + self.vault_admin_list.retain(|x| x != &id); + } +} diff --git a/crates/vcs_data/src/data/vault/member.rs b/crates/vcs_data/src/data/vault/member.rs new file mode 100644 index 0000000..aebd92d --- /dev/null +++ b/crates/vcs_data/src/data/vault/member.rs @@ -0,0 +1,140 @@ +use std::{ + fs, + io::{Error, ErrorKind}, + path::PathBuf, +}; + +use cfg_file::config::ConfigFile; + +use crate::{ + constants::{SERVER_FILE_MEMBER_INFO, SERVER_FILE_MEMBER_PUB, SERVER_PATH_MEMBERS}, + data::{ + member::{Member, MemberId}, + vault::Vault, + }, +}; + +const ID_PARAM: &str = "{member_id}"; + +/// Member Manage +impl Vault { + /// Read member from configuration file + pub async fn member(&self, id: &MemberId) -> Result { + if let Some(cfg_file) = self.member_cfg(id) { + let member = Member::read_from(cfg_file).await?; + return Ok(member); + } + + Err(Error::new(ErrorKind::NotFound, "Member not found!")) + } + + /// List all member IDs in the vault + pub fn member_ids(&self) -> Result, std::io::Error> { + let members_path = self.vault_path.join(SERVER_PATH_MEMBERS); + + if !members_path.exists() { + return Ok(Vec::new()); + } + + let mut member_ids = Vec::new(); + + for entry in fs::read_dir(members_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() + && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) + && path.extension().and_then(|s| s.to_str()) == Some("toml") + { + member_ids.push(file_name.to_string()); + } + } + + Ok(member_ids) + } + + /// Get all members + /// This method will read and deserialize member information, please pay attention to performance issues + pub async fn members(&self) -> Result, std::io::Error> { + let mut members = Vec::new(); + + for member_id in self.member_ids()? { + if let Ok(member) = self.member(&member_id).await { + members.push(member); + } + } + + Ok(members) + } + + /// Update member info + pub async fn update_member(&self, member: Member) -> Result<(), std::io::Error> { + // Ensure member exist + if self.member_cfg(&member.id()).is_some() { + let member_cfg_path = self.member_cfg_path(&member.id()); + Member::write_to(&member, member_cfg_path).await?; + return Ok(()); + } + + Err(Error::new(ErrorKind::NotFound, "Member not found!")) + } + + /// Register a member to vault + pub async fn register_member_to_vault(&self, member: Member) -> Result<(), std::io::Error> { + // Ensure member not exist + if self.member_cfg(&member.id()).is_some() { + return Err(Error::new( + ErrorKind::DirectoryNotEmpty, + format!("Member `{}` already registered!", member.id()), + )); + } + + // Wrtie config file to member dir + let member_cfg_path = self.member_cfg_path(&member.id()); + Member::write_to(&member, member_cfg_path).await?; + + Ok(()) + } + + /// Remove member from vault + pub fn remove_member_from_vault(&self, id: &MemberId) -> Result<(), std::io::Error> { + // Ensure member exist + if let Some(member_cfg_path) = self.member_cfg(id) { + fs::remove_file(member_cfg_path)?; + } + + Ok(()) + } + + /// Try to get the member's configuration file to determine if the member exists + pub fn member_cfg(&self, id: &MemberId) -> Option { + let cfg_file = self.member_cfg_path(id); + if cfg_file.exists() { + Some(cfg_file) + } else { + None + } + } + + /// Try to get the member's public key file to determine if the member has login permission + pub fn member_key(&self, id: &MemberId) -> Option { + let key_file = self.member_key_path(id); + if key_file.exists() { + Some(key_file) + } else { + None + } + } + + /// Get the member's configuration file path, but do not check if the file exists + pub fn member_cfg_path(&self, id: &MemberId) -> PathBuf { + self.vault_path + .join(SERVER_FILE_MEMBER_INFO.replace(ID_PARAM, id.to_string().as_str())) + } + + /// Get the member's public key file path, but do not check if the file exists + pub fn member_key_path(&self, id: &MemberId) -> PathBuf { + self.vault_path + .join(SERVER_FILE_MEMBER_PUB.replace(ID_PARAM, id.to_string().as_str())) + } +} diff --git a/crates/vcs_data/src/data/vault/sheets.rs b/crates/vcs_data/src/data/vault/sheets.rs new file mode 100644 index 0000000..0bba4f5 --- /dev/null +++ b/crates/vcs_data/src/data/vault/sheets.rs @@ -0,0 +1,268 @@ +use std::{collections::HashMap, io::Error}; + +use cfg_file::config::ConfigFile; +use string_proc::snake_case; +use tokio::fs; + +use crate::{ + constants::SERVER_PATH_SHEETS, + data::{ + member::MemberId, + sheet::{Sheet, SheetData, SheetName}, + vault::Vault, + }, +}; + +/// Vault Sheets Management +impl Vault { + /// Load all sheets in the vault + /// + /// It is generally not recommended to call this function frequently. + /// Although a vault typically won't contain too many sheets, + /// if individual sheet contents are large, this operation may cause + /// significant performance bottlenecks. + pub async fn sheets<'a>(&'a self) -> Result>, std::io::Error> { + let sheet_names = self.sheet_names()?; + let mut sheets = Vec::new(); + + for sheet_name in sheet_names { + let sheet = self.sheet(&sheet_name).await?; + sheets.push(sheet); + } + + Ok(sheets) + } + + /// Search for all sheet names in the vault + /// + /// The complexity of this operation is proportional to the number of sheets, + /// but generally there won't be too many sheets in a Vault + pub fn sheet_names(&self) -> Result, std::io::Error> { + // Get the sheets directory path + let sheets_dir = self.vault_path.join(SERVER_PATH_SHEETS); + + // If the directory doesn't exist, return an empty list + if !sheets_dir.exists() { + return Ok(vec![]); + } + + let mut sheet_names = Vec::new(); + + // Iterate through all files in the sheets directory + for entry in std::fs::read_dir(sheets_dir)? { + let entry = entry?; + let path = entry.path(); + + // Check if it's a YAML file + if path.is_file() + && path.extension().is_some_and(|ext| ext == "yaml") + && let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) + { + // Create a new SheetName and add it to the result list + sheet_names.push(file_stem.to_string()); + } + } + + Ok(sheet_names) + } + + /// Read a sheet from its name + /// + /// If the sheet information is successfully found in the vault, + /// it will be deserialized and read as a sheet. + /// This is the only correct way to obtain a sheet instance. + pub async fn sheet<'a>(&'a self, sheet_name: &SheetName) -> Result, std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Get the path to the sheet file + let sheet_path = Sheet::sheet_path_with_name(self, &sheet_name); + + // Ensure the sheet file exists + if !sheet_path.exists() { + // If the sheet does not exist, try to restore it from the trash + if self.restore_sheet(&sheet_name).await.is_err() { + // If restoration fails, return an error + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Sheet `{}` not found!", sheet_name), + )); + } + } + + // Read the sheet data from the file + let data = SheetData::read_from(sheet_path).await?; + + Ok(Sheet { + name: sheet_name.clone(), + data, + vault_reference: self, + }) + } + + /// Create a sheet locally and return the sheet instance + /// + /// This method creates a new sheet in the vault with the given name and holder. + /// It will verify that the member exists and that the sheet doesn't already exist + /// before creating the sheet file with default empty data. + pub async fn create_sheet<'a>( + &'a self, + sheet_name: &SheetName, + holder: &MemberId, + ) -> Result, std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Ensure member exists + if !self.member_cfg_path(holder).exists() { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Member `{}` not found!", &holder), + )); + } + + // Ensure sheet does not already exist + let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); + if sheet_file_path.exists() { + return Err(Error::new( + std::io::ErrorKind::AlreadyExists, + format!("Sheet `{}` already exists!", &sheet_name), + )); + } + + // Create the sheet file + let sheet_data = SheetData { + holder: holder.clone(), + inputs: Vec::new(), + mapping: HashMap::new(), + }; + SheetData::write_to(&sheet_data, sheet_file_path).await?; + + Ok(Sheet { + name: sheet_name, + data: sheet_data, + vault_reference: self, + }) + } + + /// Delete the sheet file from local disk by name + /// + /// This method will remove the sheet file with the given name from the vault. + /// It will verify that the sheet exists before attempting to delete it. + /// If the sheet is successfully deleted, it will return Ok(()). + /// + /// Warning: This operation is dangerous. Deleting a sheet will cause local workspaces + /// using this sheet to become invalid. Please ensure the sheet is not currently in use + /// and will not be used in the future. + /// + /// For a safer deletion method, consider using `delete_sheet_safety`. + /// + /// Note: This function is intended for server-side use only and should not be + /// arbitrarily called by other members to prevent unauthorized data deletion. + pub async fn delete_sheet(&self, sheet_name: &SheetName) -> Result<(), std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Ensure sheet exists + let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); + if !sheet_file_path.exists() { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Sheet `{}` not found!", &sheet_name), + )); + } + + // Delete the sheet file + fs::remove_file(sheet_file_path).await?; + + Ok(()) + } + + /// Safely delete the sheet + /// + /// The sheet will be moved to the trash directory, ensuring it does not appear in the + /// results of `sheets` and `sheet_names` methods. + /// However, if the sheet's holder attempts to access the sheet through the `sheet` method, + /// the system will automatically restore it from the trash directory. + /// This means: the sheet will only permanently remain in the trash directory, + /// waiting for manual cleanup by an administrator, when it is truly no longer in use. + /// + /// This is a safer deletion method because it provides the possibility of recovery, + /// avoiding irreversible data loss caused by accidental deletion. + /// + /// Note: This function is intended for server-side use only and should not be + /// arbitrarily called by other members to prevent unauthorized data deletion. + pub async fn delete_sheet_safely(&self, sheet_name: &SheetName) -> Result<(), std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Ensure the sheet exists + let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); + if !sheet_file_path.exists() { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Sheet `{}` not found!", &sheet_name), + )); + } + + // Create the trash directory + let trash_dir = self.vault_path.join(".trash"); + if !trash_dir.exists() { + fs::create_dir_all(&trash_dir).await?; + } + + // Generate a unique filename in the trash + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + let trash_file_name = format!("{}_{}.yaml", sheet_name, timestamp); + let trash_path = trash_dir.join(trash_file_name); + + // Move the sheet file to the trash + fs::rename(&sheet_file_path, &trash_path).await?; + + Ok(()) + } + + /// Restore the sheet from the trash + /// + /// Restore the specified sheet from the trash to its original location, making it accessible normally. + pub async fn restore_sheet(&self, sheet_name: &SheetName) -> Result<(), std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Search for matching files in the trash + let trash_dir = self.vault_path.join(".trash"); + if !trash_dir.exists() { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "Trash directory does not exist!".to_string(), + )); + } + + let mut found_path = None; + for entry in std::fs::read_dir(&trash_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() + && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) + { + // Check if the filename starts with the sheet name + if file_name.starts_with(&sheet_name) { + found_path = Some(path); + break; + } + } + } + + let trash_path = found_path.ok_or_else(|| { + Error::new( + std::io::ErrorKind::NotFound, + format!("Sheet `{}` not found in trash!", &sheet_name), + ) + })?; + + // Restore the sheet to its original location + let original_path = Sheet::sheet_path_with_name(self, &sheet_name); + fs::rename(&trash_path, &original_path).await?; + + Ok(()) + } +} diff --git a/crates/vcs_data/src/data/vault/virtual_file.rs b/crates/vcs_data/src/data/vault/virtual_file.rs new file mode 100644 index 0000000..fe83594 --- /dev/null +++ b/crates/vcs_data/src/data/vault/virtual_file.rs @@ -0,0 +1,473 @@ +use std::{ + collections::HashMap, + io::{Error, ErrorKind}, + path::PathBuf, +}; + +use cfg_file::{ConfigFile, config::ConfigFile}; +use serde::{Deserialize, Serialize}; +use string_proc::snake_case; +use tcp_connection::instance::ConnectionInstance; +use tokio::fs; +use uuid::Uuid; + +use crate::{ + constants::{ + SERVER_FILE_VF_META, SERVER_FILE_VF_VERSION_INSTANCE, SERVER_PATH_VF_ROOT, + SERVER_PATH_VF_STORAGE, SERVER_PATH_VF_TEMP, + }, + data::{member::MemberId, vault::Vault}, +}; + +pub type VirtualFileId = String; +pub type VirtualFileVersion = String; + +const VF_PREFIX: &str = "vf_"; +const ID_PARAM: &str = "{vf_id}"; +const ID_INDEX: &str = "{vf_index}"; +const VERSION_PARAM: &str = "{vf_version}"; +const TEMP_NAME: &str = "{temp_name}"; + +pub struct VirtualFile<'a> { + /// Unique identifier for the virtual file + id: VirtualFileId, + + /// Reference of Vault + current_vault: &'a Vault, +} + +#[derive(Default, Clone, Serialize, Deserialize, ConfigFile)] +pub struct VirtualFileMeta { + /// Current version of the virtual file + current_version: VirtualFileVersion, + + /// The member who holds the edit right of the file + hold_member: MemberId, + + /// Description of each version + version_description: HashMap, + + /// Histories + histories: Vec, +} + +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct VirtualFileVersionDescription { + /// The member who created this version + pub creator: MemberId, + + /// The description of this version + pub description: String, +} + +impl VirtualFileVersionDescription { + /// Create a new version description + pub fn new(creator: MemberId, description: String) -> Self { + Self { + creator, + description, + } + } +} + +/// Virtual File Operations +impl Vault { + /// Generate a temporary path for receiving + pub fn virtual_file_temp_path(&self) -> PathBuf { + let random_receive_name = format!("{}", uuid::Uuid::new_v4()); + self.vault_path + .join(SERVER_PATH_VF_TEMP.replace(TEMP_NAME, &random_receive_name)) + } + + /// Get the directory where virtual files are stored + pub fn virtual_file_storage_dir(&self) -> PathBuf { + self.vault_path().join(SERVER_PATH_VF_ROOT) + } + + /// Get the directory where a specific virtual file is stored + pub fn virtual_file_dir(&self, id: &VirtualFileId) -> Result { + Ok(self.vault_path().join( + SERVER_PATH_VF_STORAGE + .replace(ID_PARAM, &id.to_string()) + .replace(ID_INDEX, &Self::vf_index(id)?), + )) + } + + // Generate index path of virtual file + fn vf_index(id: &VirtualFileId) -> Result { + // Remove VF_PREFIX if present + let id_str = if let Some(stripped) = id.strip_prefix(VF_PREFIX) { + stripped + } else { + id + }; + + // Extract the first part before the first hyphen + let first_part = id_str.split('-').next().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid virtual file ID format: no hyphen found", + ) + })?; + + // Ensure the first part has exactly 8 characters + if first_part.len() != 8 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid virtual file ID format: first part must be 8 characters", + ))?; + } + + // Split into 2-character chunks and join with path separator + let mut path = String::new(); + for i in (0..first_part.len()).step_by(2) { + if i > 0 { + path.push('/'); + } + path.push_str(&first_part[i..i + 2]); + } + + Ok(path) + } + + /// Get the directory where a specific virtual file's metadata is stored + pub fn virtual_file_real_path( + &self, + id: &VirtualFileId, + version: &VirtualFileVersion, + ) -> PathBuf { + self.vault_path().join( + SERVER_FILE_VF_VERSION_INSTANCE + .replace(ID_PARAM, &id.to_string()) + .replace(ID_INDEX, &Self::vf_index(id).unwrap_or_default()) + .replace(VERSION_PARAM, &version.to_string()), + ) + } + + /// Get the directory where a specific virtual file's metadata is stored + pub fn virtual_file_meta_path(&self, id: &VirtualFileId) -> PathBuf { + self.vault_path().join( + SERVER_FILE_VF_META + .replace(ID_PARAM, &id.to_string()) + .replace(ID_INDEX, &Self::vf_index(id).unwrap_or_default()), + ) + } + + /// Get the virtual file with the given ID + pub fn virtual_file(&self, id: &VirtualFileId) -> Result, std::io::Error> { + let dir = self.virtual_file_dir(id); + if dir?.exists() { + Ok(VirtualFile { + id: id.clone(), + current_vault: self, + }) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Cannot found virtual file!", + )) + } + } + + /// Get the meta data of the virtual file with the given ID + pub async fn virtual_file_meta( + &self, + id: &VirtualFileId, + ) -> Result { + let dir = self.virtual_file_meta_path(id); + let metadata = VirtualFileMeta::read_from(dir).await?; + Ok(metadata) + } + + /// Write the meta data of the virtual file with the given ID + pub async fn write_virtual_file_meta( + &self, + id: &VirtualFileId, + meta: &VirtualFileMeta, + ) -> Result<(), std::io::Error> { + let dir = self.virtual_file_meta_path(id); + VirtualFileMeta::write_to(meta, dir).await?; + Ok(()) + } + + /// Create a virtual file from a connection instance + /// + /// It's the only way to create virtual files! + /// + /// When the target machine executes `write_file`, use this function instead of `read_file`, + /// and provide the member ID of the transmitting member. + /// + /// The system will automatically receive the file and + /// create the virtual file. + pub async fn create_virtual_file_from_connection( + &self, + instance: &mut ConnectionInstance, + member_id: &MemberId, + ) -> Result { + const FIRST_VERSION: &str = "0"; + let receive_path = self.virtual_file_temp_path(); + let new_id = format!("{}{}", VF_PREFIX, Uuid::new_v4()); + let move_path = self.virtual_file_real_path(&new_id, &FIRST_VERSION.to_string()); + + match instance.read_file(receive_path.clone()).await { + Ok(_) => { + // Read successful, create virtual file + // Create default version description + let mut version_description = + HashMap::::new(); + version_description.insert( + FIRST_VERSION.to_string(), + VirtualFileVersionDescription { + creator: member_id.clone(), + description: "Track".to_string(), + }, + ); + // Create metadata + let mut meta = VirtualFileMeta { + current_version: FIRST_VERSION.to_string(), + hold_member: member_id.clone(), // The holder of the newly created virtual file is the creator by default + version_description, + histories: Vec::default(), + }; + + // Add first version + meta.histories.push(FIRST_VERSION.to_string()); + + // Write metadata to file + VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(&new_id)).await?; + + // Move temp file to virtual file directory + if let Some(parent) = move_path.parent() + && !parent.exists() + { + fs::create_dir_all(parent).await?; + } + fs::rename(receive_path, move_path).await?; + + // + + Ok(new_id) + } + Err(e) => { + // Read failed, remove temp file. + if receive_path.exists() { + fs::remove_file(receive_path).await?; + } + + Err(Error::other(e)) + } + } + } + + /// Update a virtual file from a connection instance + /// + /// It's the only way to update virtual files! + /// When the target machine executes `write_file`, use this function instead of `read_file`, + /// and provide the member ID of the transmitting member. + /// + /// The system will automatically receive the file and + /// update the virtual file. + /// + /// Note: The specified member must hold the edit right of the file, + /// otherwise the file reception will not be allowed. + /// + /// Make sure to obtain the edit right of the file before calling this function. + pub async fn update_virtual_file_from_connection( + &self, + instance: &mut ConnectionInstance, + member: &MemberId, + virtual_file_id: &VirtualFileId, + new_version: &VirtualFileVersion, + description: VirtualFileVersionDescription, + ) -> Result<(), std::io::Error> { + let new_version = snake_case!(new_version.clone()); + let mut meta = self.virtual_file_meta(virtual_file_id).await?; + + // Check if the member has edit right + self.check_virtual_file_edit_right(member, virtual_file_id) + .await?; + + // Check if the new version already exists + if meta.version_description.contains_key(&new_version) { + return Err(Error::new( + ErrorKind::AlreadyExists, + format!( + "Version `{}` already exists for virtual file `{}`", + new_version, virtual_file_id + ), + )); + } + + // Verify success + let receive_path = self.virtual_file_temp_path(); + let move_path = self.virtual_file_real_path(virtual_file_id, &new_version); + + match instance.read_file(receive_path.clone()).await { + Ok(_) => { + // Read success, move temp file to real path. + fs::rename(receive_path, move_path).await?; + + // Update metadata + meta.current_version = new_version.clone(); + meta.version_description + .insert(new_version.clone(), description); + meta.histories.push(new_version); + VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(virtual_file_id)) + .await?; + + Ok(()) + } + Err(e) => { + // Read failed, remove temp file. + if receive_path.exists() { + fs::remove_file(receive_path).await?; + } + + Err(Error::other(e)) + } + } + } + + /// Update virtual file from existing version + /// + /// This operation creates a new version based on the specified old version file instance. + /// The new version will retain the same version name as the old version, but use a different version number. + /// After the update, this version will be considered newer than the original version when comparing versions. + pub async fn update_virtual_file_from_exist_version( + &self, + member: &MemberId, + virtual_file_id: &VirtualFileId, + old_version: &VirtualFileVersion, + ) -> Result<(), std::io::Error> { + let old_version = snake_case!(old_version.clone()); + let mut meta = self.virtual_file_meta(virtual_file_id).await?; + + // Check if the member has edit right + self.check_virtual_file_edit_right(member, virtual_file_id) + .await?; + + // Ensure virtual file exist + let Ok(_) = self.virtual_file(virtual_file_id) else { + return Err(Error::new( + ErrorKind::NotFound, + format!("Virtual file `{}` not found!", virtual_file_id), + )); + }; + + // Ensure version exist + if !meta.version_exists(&old_version) { + return Err(Error::new( + ErrorKind::NotFound, + format!("Version `{}` not found!", old_version), + )); + } + + // Ok, Create new version + meta.current_version = old_version.clone(); + meta.histories.push(old_version); + VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(virtual_file_id)).await?; + + Ok(()) + } + + /// Grant a member the edit right for a virtual file + /// This operation takes effect immediately upon success + pub async fn grant_virtual_file_edit_right( + &self, + member_id: &MemberId, + virtual_file_id: &VirtualFileId, + ) -> Result<(), std::io::Error> { + let mut meta = self.virtual_file_meta(virtual_file_id).await?; + meta.hold_member = member_id.clone(); + self.write_virtual_file_meta(virtual_file_id, &meta).await + } + + /// Check if a member has the edit right for a virtual file + pub async fn has_virtual_file_edit_right( + &self, + member_id: &MemberId, + virtual_file_id: &VirtualFileId, + ) -> Result { + let meta = self.virtual_file_meta(virtual_file_id).await?; + Ok(meta.hold_member.eq(member_id)) + } + + /// Check if a member has the edit right for a virtual file and return Result + /// Returns Ok(()) if the member has edit right, otherwise returns PermissionDenied error + pub async fn check_virtual_file_edit_right( + &self, + member_id: &MemberId, + virtual_file_id: &VirtualFileId, + ) -> Result<(), std::io::Error> { + if !self + .has_virtual_file_edit_right(member_id, virtual_file_id) + .await? + { + return Err(Error::new( + ErrorKind::PermissionDenied, + format!( + "Member `{}` not allowed to update virtual file `{}`", + member_id, virtual_file_id + ), + )); + } + Ok(()) + } + + /// Revoke the edit right for a virtual file from the current holder + /// This operation takes effect immediately upon success + pub async fn revoke_virtual_file_edit_right( + &self, + virtual_file_id: &VirtualFileId, + ) -> Result<(), std::io::Error> { + let mut meta = self.virtual_file_meta(virtual_file_id).await?; + meta.hold_member = String::default(); + self.write_virtual_file_meta(virtual_file_id, &meta).await + } +} + +impl<'a> VirtualFile<'a> { + /// Get id of VirtualFile + pub fn id(&self) -> VirtualFileId { + self.id.clone() + } + + /// Read metadata of VirtualFile + pub async fn read_meta(&self) -> Result { + self.current_vault.virtual_file_meta(&self.id).await + } +} + +impl VirtualFileMeta { + /// Get all versions of the virtual file + pub fn versions(&self) -> &Vec { + &self.histories + } + + /// Get the total number of versions for this virtual file + pub fn version_len(&self) -> i32 { + self.histories.len() as i32 + } + + /// Check if a specific version exists + /// Returns true if the version exists, false otherwise + pub fn version_exists(&self, version: &VirtualFileVersion) -> bool { + self.versions().iter().any(|v| v == version) + } + + /// Get the version number (index) for a given version name + /// Returns None if the version doesn't exist + pub fn version_num(&self, version: &VirtualFileVersion) -> Option { + self.histories + .iter() + .rev() + .position(|v| v == version) + .map(|pos| (self.histories.len() - 1 - pos) as i32) + } + + /// Get the version name for a given version number (index) + /// Returns None if the version number is out of range + pub fn version_name(&self, version_num: i32) -> Option { + self.histories.get(version_num as usize).cloned() + } +} diff --git a/crates/vcs_data/src/lib.rs b/crates/vcs_data/src/lib.rs new file mode 100644 index 0000000..1b41391 --- /dev/null +++ b/crates/vcs_data/src/lib.rs @@ -0,0 +1,5 @@ +pub mod constants; +pub mod current; + +#[allow(dead_code)] +pub mod data; diff --git a/crates/vcs_data/todo.txt b/crates/vcs_data/todo.txt new file mode 100644 index 0000000..65c94ef --- /dev/null +++ b/crates/vcs_data/todo.txt @@ -0,0 +1,36 @@ +本地文件操作 +设置上游服务器(仅设置,不会连接和修改染色标识) +验证连接、权限,并为当前工作区染色(若已染色,则无法连接不同标识的服务器) +进入表 (否则无法做任何操作) +退出表 (文件将会从当前目录移出,等待下次进入时还原) +去色 - 断开与上游服务器的关联 +跟踪本地文件的移动、重命名,立刻同步至表 +扫描本地文件结构,标记变化 +通过本地暂存的表索引搜索文件 +查询本地某个文件的状态 +查询当前目录的状态 +查询工作区状态 +将本地所有文件更新到最新状态 +提交所有产生变化的自身所属文件 + + +表操作(必须指定成员和表) +表查看 - 指定表并查看结构 +从参照表拉入文件项目 +将文件项目(或多个)导出到指定表 +查看导入请求 +在某个本地地址同意并导入文件 +拒绝某个、某些或所有导入请求 +删除表中的映射,但要确保实际文件已被移除 (忽略文件) +放弃表,所有者消失,下一个切换至表的人获得(放弃需要确保表中没有任何文件是所有者持有的)(替代目前的安全删除) + + +虚拟文件操作 +跟踪本地某些文件,并将其创建为虚拟文件,然后添加到自己的表 +根据本地文件的目录查找虚拟文件,并为自己获得所有权(需要确保版本和上游同步才可) +根据本地文件的目录查找虚拟文件,并放弃所有权(需要确保和上游同步才可) +根据本地文件的目录查找虚拟文件,并定向到指定的存在的老版本 + + +?为什么虚拟文件不能删除:虚拟文件的唯一删除方式就是,没有人再用他 +?为什么没有删除表:同理,表权限可以转移,但是删除只能等待定期清除无主人的表 diff --git a/crates/vcs_data/vcs_data_test/Cargo.toml b/crates/vcs_data/vcs_data_test/Cargo.toml new file mode 100644 index 0000000..9dcbd4a --- /dev/null +++ b/crates/vcs_data/vcs_data_test/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "vcs_data_test" +edition = "2024" +version.workspace = true + +[dependencies] +tcp_connection = { path = "../../utils/tcp_connection" } +tcp_connection_test = { path = "../../utils/tcp_connection/tcp_connection_test" } +cfg_file = { path = "../../utils/cfg_file", features = ["default"] } +vcs_data = { path = "../../vcs_data" } + +# Async & Networking +tokio = { version = "1.46.1", features = ["full"] } diff --git a/crates/vcs_data/vcs_data_test/lib.rs b/crates/vcs_data/vcs_data_test/lib.rs new file mode 100644 index 0000000..5b65941 --- /dev/null +++ b/crates/vcs_data/vcs_data_test/lib.rs @@ -0,0 +1,11 @@ +use vcs_service::{action::Action, action_pool::ActionPool}; + +use crate::actions::test::FindMemberInServer; + +pub mod constants; +pub mod current; + +#[allow(dead_code)] +pub mod data; + +pub mod actions; diff --git a/crates/vcs_data/vcs_data_test/src/lib.rs b/crates/vcs_data/vcs_data_test/src/lib.rs new file mode 100644 index 0000000..8ad03e1 --- /dev/null +++ b/crates/vcs_data/vcs_data_test/src/lib.rs @@ -0,0 +1,27 @@ +use std::{env::current_dir, path::PathBuf}; + +use tokio::fs; + +#[cfg(test)] +pub mod test_vault_setup_and_member_register; + +#[cfg(test)] +pub mod test_virtual_file_creation_and_update; + +#[cfg(test)] +pub mod test_local_workspace_setup_and_account_management; + +#[cfg(test)] +pub mod test_sheet_creation_management_and_persistence; + +pub async fn get_test_dir(area: &str) -> Result { + let dir = current_dir()?.join(".temp").join("test").join(area); + if !dir.exists() { + std::fs::create_dir_all(&dir)?; + } else { + // Regenerate existing directory + fs::remove_dir_all(&dir).await?; + fs::create_dir_all(&dir).await?; + } + Ok(dir) +} diff --git a/crates/vcs_data/vcs_data_test/src/test_local_workspace_setup_and_account_management.rs b/crates/vcs_data/vcs_data_test/src/test_local_workspace_setup_and_account_management.rs new file mode 100644 index 0000000..2718d01 --- /dev/null +++ b/crates/vcs_data/vcs_data_test/src/test_local_workspace_setup_and_account_management.rs @@ -0,0 +1,248 @@ +use std::io::Error; + +use cfg_file::config::ConfigFile; +use vcs_data::{ + constants::{CLIENT_FILE_README, CLIENT_FILE_WORKSPACE, USER_FILE_KEY, USER_FILE_MEMBER}, + data::{ + local::{LocalWorkspace, config::LocalConfig}, + member::Member, + user::UserDirectory, + }, +}; + +use crate::get_test_dir; + +#[tokio::test] +async fn test_local_workspace_setup_and_account_management() -> Result<(), std::io::Error> { + let dir = get_test_dir("local_workspace_account_management").await?; + + // Setup local workspace + LocalWorkspace::setup_local_workspace(dir.clone()).await?; + + // Check if the following files are created in `dir`: + // Files: CLIENT_FILE_WORKSPACE, CLIENT_FILE_README + assert!(dir.join(CLIENT_FILE_WORKSPACE).exists()); + assert!(dir.join(CLIENT_FILE_README).exists()); + + // Get local workspace + let config = LocalConfig::read_from(dir.join(CLIENT_FILE_WORKSPACE)).await?; + let Some(_local_workspace) = LocalWorkspace::init(config, &dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "Local workspace not found!", + )); + }; + + // Create user directory from workspace path + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Test account registration + let member_id = "test_account"; + let member = Member::new(member_id); + + // Register account + user_directory.register_account(member.clone()).await?; + + // Check if the account config file exists + assert!( + dir.join(USER_FILE_MEMBER.replace("{self_id}", member_id)) + .exists() + ); + + // Test account retrieval + let retrieved_member = user_directory.account(&member_id.to_string()).await?; + assert_eq!(retrieved_member.id(), member.id()); + + // Test account IDs listing + let account_ids = user_directory.account_ids()?; + assert!(account_ids.contains(&member_id.to_string())); + + // Test accounts listing + let accounts = user_directory.accounts().await?; + assert_eq!(accounts.len(), 1); + assert_eq!(accounts[0].id(), member.id()); + + // Test account existence check + assert!(user_directory.account_cfg(&member_id.to_string()).is_some()); + + // Test private key check (should be false initially) + assert!(!user_directory.has_private_key(&member_id.to_string())); + + // Test account update + let mut updated_member = member.clone(); + updated_member.set_metadata("email", "test@example.com"); + user_directory + .update_account(updated_member.clone()) + .await?; + + // Verify update + let updated_retrieved = user_directory.account(&member_id.to_string()).await?; + assert_eq!( + updated_retrieved.metadata("email"), + Some(&"test@example.com".to_string()) + ); + + // Test account removal + user_directory.remove_account(&member_id.to_string())?; + + // Check if the account config file no longer exists + assert!( + !dir.join(USER_FILE_MEMBER.replace("{self_id}", member_id)) + .exists() + ); + + // Check if account is no longer in the list + let account_ids_after_removal = user_directory.account_ids()?; + assert!(!account_ids_after_removal.contains(&member_id.to_string())); + + Ok(()) +} + +#[tokio::test] +async fn test_account_private_key_management() -> Result<(), std::io::Error> { + let dir = get_test_dir("account_private_key_management").await?; + + // Create user directory + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Register account + let member_id = "test_account_with_key"; + let member = Member::new(member_id); + user_directory.register_account(member).await?; + + // Create a dummy private key file for testing + let private_key_path = dir.join(USER_FILE_KEY.replace("{self_id}", member_id)); + std::fs::create_dir_all(private_key_path.parent().unwrap())?; + std::fs::write(&private_key_path, "dummy_private_key_content")?; + + // Test private key existence check + assert!(user_directory.has_private_key(&member_id.to_string())); + + // Test private key path retrieval + assert!( + user_directory + .account_private_key(&member_id.to_string()) + .is_some() + ); + + // Remove account (should also remove private key) + user_directory.remove_account(&member_id.to_string())?; + + // Check if private key file is also removed + assert!(!private_key_path.exists()); + + Ok(()) +} + +#[tokio::test] +async fn test_multiple_account_management() -> Result<(), std::io::Error> { + let dir = get_test_dir("multiple_account_management").await?; + + // Create user directory + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Register multiple accounts + let account_names = vec!["alice", "bob", "charlie"]; + + for name in &account_names { + user_directory.register_account(Member::new(*name)).await?; + } + + // Test account IDs listing + let account_ids = user_directory.account_ids()?; + assert_eq!(account_ids.len(), 3); + + for name in &account_names { + assert!(account_ids.contains(&name.to_string())); + } + + // Test accounts listing + let accounts = user_directory.accounts().await?; + assert_eq!(accounts.len(), 3); + + // Remove one account + user_directory.remove_account(&"bob".to_string())?; + + // Verify removal + let account_ids_after_removal = user_directory.account_ids()?; + assert_eq!(account_ids_after_removal.len(), 2); + assert!(!account_ids_after_removal.contains(&"bob".to_string())); + assert!(account_ids_after_removal.contains(&"alice".to_string())); + assert!(account_ids_after_removal.contains(&"charlie".to_string())); + + Ok(()) +} + +#[tokio::test] +async fn test_account_registration_duplicate_prevention() -> Result<(), std::io::Error> { + let dir = get_test_dir("account_duplicate_prevention").await?; + + // Create user directory + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Register account + let member_id = "duplicate_test"; + user_directory + .register_account(Member::new(member_id)) + .await?; + + // Try to register same account again - should fail + let result = user_directory + .register_account(Member::new(member_id)) + .await; + assert!(result.is_err()); + + Ok(()) +} + +#[tokio::test] +async fn test_nonexistent_account_operations() -> Result<(), std::io::Error> { + let dir = get_test_dir("nonexistent_account_operations").await?; + + // Create user directory + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Try to read non-existent account - should fail + let result = user_directory.account(&"nonexistent".to_string()).await; + assert!(result.is_err()); + + // Try to update non-existent account - should fail + let result = user_directory + .update_account(Member::new("nonexistent")) + .await; + assert!(result.is_err()); + + // Try to remove non-existent account - should succeed (idempotent) + let result = user_directory.remove_account(&"nonexistent".to_string()); + assert!(result.is_ok()); + + // Check private key for non-existent account - should be false + assert!(!user_directory.has_private_key(&"nonexistent".to_string())); + + Ok(()) +} diff --git a/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs b/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs new file mode 100644 index 0000000..461d465 --- /dev/null +++ b/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs @@ -0,0 +1,307 @@ +use std::io::Error; + +use cfg_file::config::ConfigFile; +use vcs_data::{ + constants::{SERVER_FILE_SHEET, SERVER_FILE_VAULT}, + data::{ + member::{Member, MemberId}, + sheet::{InputRelativePathBuf, SheetName}, + vault::{Vault, config::VaultConfig, virtual_file::VirtualFileId}, + }, +}; + +use crate::get_test_dir; + +#[tokio::test] +async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io::Error> { + let dir = get_test_dir("sheet_management").await?; + + // Setup vault + Vault::setup_vault(dir.clone()).await?; + + // Get vault + let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?; + let Some(vault) = Vault::init(config, &dir) else { + return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!")); + }; + + // Add a member to use as sheet holder + let member_id: MemberId = "test_member".to_string(); + vault + .register_member_to_vault(Member::new(&member_id)) + .await?; + + // Test 1: Create a new sheet + let sheet_name: SheetName = "test_sheet".to_string(); + let sheet = vault.create_sheet(&sheet_name, &member_id).await?; + + // Verify sheet properties + assert_eq!(sheet.holder(), &member_id); + assert_eq!(sheet.holder(), &member_id); + assert!(sheet.inputs().is_empty()); + assert!(sheet.mapping().is_empty()); + + // Verify sheet file was created + const SHEET_NAME_PARAM: &str = "{sheet-name}"; + let sheet_path = dir.join(SERVER_FILE_SHEET.replace(SHEET_NAME_PARAM, &sheet_name)); + assert!(sheet_path.exists()); + + // Test 2: Add input packages to the sheet + let input_name = "source_files".to_string(); + + // First add mapping entries that will be used to generate the input package + let mut sheet = vault.sheet(&sheet_name).await?; + + // Add mapping entries for the files + let main_rs_path = vcs_data::data::sheet::SheetPathBuf::from("src/main.rs"); + let lib_rs_path = vcs_data::data::sheet::SheetPathBuf::from("src/lib.rs"); + let main_rs_id = VirtualFileId::new(); + let lib_rs_id = VirtualFileId::new(); + + sheet + .add_mapping(main_rs_path.clone(), main_rs_id.clone()) + .await?; + sheet + .add_mapping(lib_rs_path.clone(), lib_rs_id.clone()) + .await?; + + // Use output_mappings to generate the InputPackage + let paths = vec![main_rs_path, lib_rs_path]; + let input_package = sheet.output_mappings(input_name.clone(), &paths)?; + sheet.add_input(input_package)?; + + // Verify input was added + assert_eq!(sheet.inputs().len(), 1); + let added_input = &sheet.inputs()[0]; + assert_eq!(added_input.name, input_name); + assert_eq!(added_input.files.len(), 2); + assert_eq!( + added_input.files[0].0, + InputRelativePathBuf::from("source_files/main.rs") + ); + assert_eq!( + added_input.files[1].0, + InputRelativePathBuf::from("source_files/lib.rs") + ); + + // Test 3: Add mapping entries + let mapping_path = vcs_data::data::sheet::SheetPathBuf::from("output/build.exe"); + let virtual_file_id = VirtualFileId::new(); + + sheet + .add_mapping(mapping_path.clone(), virtual_file_id.clone()) + .await?; + + // Verify mapping was added + assert_eq!(sheet.mapping().len(), 3); + assert_eq!(sheet.mapping().get(&mapping_path), Some(&virtual_file_id)); + + // Test 4: Persist sheet to disk + sheet.persist().await?; + + // Verify persistence by reloading the sheet + let reloaded_sheet = vault.sheet(&sheet_name).await?; + assert_eq!(reloaded_sheet.holder(), &member_id); + assert_eq!(reloaded_sheet.inputs().len(), 1); + assert_eq!(reloaded_sheet.mapping().len(), 3); + + // Test 5: Remove input package + let mut sheet_for_removal = vault.sheet(&sheet_name).await?; + let removed_input = sheet_for_removal.deny_input(&input_name); + assert!(removed_input.is_some()); + let removed_input = removed_input.unwrap(); + assert_eq!(removed_input.name, input_name); + assert_eq!(removed_input.files.len(), 2); + assert_eq!(sheet_for_removal.inputs().len(), 0); + + // Test 6: Remove mapping entry + let _removed_virtual_file_id = sheet_for_removal.remove_mapping(&mapping_path).await; + // Don't check the return value since it depends on virtual file existence + assert_eq!(sheet_for_removal.mapping().len(), 2); + + // Test 7: List all sheets in vault + let sheet_names = vault.sheet_names()?; + assert_eq!(sheet_names.len(), 2); + assert!(sheet_names.contains(&sheet_name)); + assert!(sheet_names.contains(&"ref".to_string())); + + let all_sheets = vault.sheets().await?; + assert_eq!(all_sheets.len(), 2); + // One sheet should be the test sheet, the other should be the ref sheet with host as holder + let test_sheet_holder = all_sheets + .iter() + .find(|s| s.holder() == &member_id) + .map(|s| s.holder()) + .unwrap(); + let ref_sheet_holder = all_sheets + .iter() + .find(|s| s.holder() == &"host".to_string()) + .map(|s| s.holder()) + .unwrap(); + assert_eq!(test_sheet_holder, &member_id); + assert_eq!(ref_sheet_holder, &"host".to_string()); + + // Test 8: Safe deletion (move to trash) + vault.delete_sheet_safely(&sheet_name).await?; + + // Verify sheet is not in normal listing but can be restored + let sheet_names_after_deletion = vault.sheet_names()?; + assert_eq!(sheet_names_after_deletion.len(), 1); + assert_eq!(sheet_names_after_deletion[0], "ref"); + + // Test 9: Restore sheet from trash + let restored_sheet = vault.sheet(&sheet_name).await?; + assert_eq!(restored_sheet.holder(), &member_id); + assert_eq!(restored_sheet.holder(), &member_id); + + // Verify sheet is back in normal listing + let sheet_names_after_restore = vault.sheet_names()?; + assert_eq!(sheet_names_after_restore.len(), 2); + assert!(sheet_names_after_restore.contains(&sheet_name)); + assert!(sheet_names_after_restore.contains(&"ref".to_string())); + + // Test 10: Permanent deletion + vault.delete_sheet(&sheet_name).await?; + + // Verify sheet is permanently gone + let sheet_names_final = vault.sheet_names()?; + assert_eq!(sheet_names_final.len(), 1); + assert_eq!(sheet_names_final[0], "ref"); + + // Attempt to access deleted sheet should fail + let result = vault.sheet(&sheet_name).await; + assert!(result.is_err()); + + // Clean up: Remove member + vault.remove_member_from_vault(&member_id)?; + + Ok(()) +} + +#[tokio::test] +async fn test_sheet_error_conditions() -> Result<(), std::io::Error> { + let dir = get_test_dir("sheet_error_conditions").await?; + + // Setup vault + Vault::setup_vault(dir.clone()).await?; + + // Get vault + let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?; + let Some(vault) = Vault::init(config, &dir) else { + return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!")); + }; + + // Test 1: Create sheet with non-existent member should fail + let non_existent_member: MemberId = "non_existent_member".to_string(); + let sheet_name: SheetName = "test_sheet".to_string(); + + let result = vault.create_sheet(&sheet_name, &non_existent_member).await; + assert!(result.is_err()); + + // Add a member first + let member_id: MemberId = "test_member".to_string(); + vault + .register_member_to_vault(Member::new(&member_id)) + .await?; + + // Test 2: Create duplicate sheet should fail + vault.create_sheet(&sheet_name, &member_id).await?; + let result = vault.create_sheet(&sheet_name, &member_id).await; + assert!(result.is_err()); + + // Test 3: Delete non-existent sheet should fail + let non_existent_sheet: SheetName = "non_existent_sheet".to_string(); + let result = vault.delete_sheet(&non_existent_sheet).await; + assert!(result.is_err()); + + // Test 4: Safe delete non-existent sheet should fail + let result = vault.delete_sheet_safely(&non_existent_sheet).await; + assert!(result.is_err()); + + // Test 5: Restore non-existent sheet from trash should fail + let result = vault.restore_sheet(&non_existent_sheet).await; + assert!(result.is_err()); + + // Clean up + vault.remove_member_from_vault(&member_id)?; + + Ok(()) +} + +#[tokio::test] +async fn test_sheet_data_serialization() -> Result<(), std::io::Error> { + let dir = get_test_dir("sheet_serialization").await?; + + // Test serialization by creating a sheet through the vault + // Setup vault + Vault::setup_vault(dir.clone()).await?; + + // Get vault + let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?; + let Some(vault) = Vault::init(config, &dir) else { + return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!")); + }; + + // Add a member + let member_id: MemberId = "test_member".to_string(); + vault + .register_member_to_vault(Member::new(&member_id)) + .await?; + + // Create a sheet + let sheet_name: SheetName = "test_serialization_sheet".to_string(); + let mut sheet = vault.create_sheet(&sheet_name, &member_id).await?; + + // Add some inputs + let input_name = "source_files".to_string(); + let _files = vec![ + ( + InputRelativePathBuf::from("src/main.rs"), + VirtualFileId::new(), + ), + ( + InputRelativePathBuf::from("src/lib.rs"), + VirtualFileId::new(), + ), + ]; + // First add mapping entries + let main_rs_path = vcs_data::data::sheet::SheetPathBuf::from("src/main.rs"); + let lib_rs_path = vcs_data::data::sheet::SheetPathBuf::from("src/lib.rs"); + let main_rs_id = VirtualFileId::new(); + let lib_rs_id = VirtualFileId::new(); + + sheet + .add_mapping(main_rs_path.clone(), main_rs_id.clone()) + .await?; + sheet + .add_mapping(lib_rs_path.clone(), lib_rs_id.clone()) + .await?; + + // Use output_mappings to generate the InputPackage + let paths = vec![main_rs_path, lib_rs_path]; + let input_package = sheet.output_mappings(input_name.clone(), &paths)?; + sheet.add_input(input_package)?; + + // Add some mappings + let build_exe_id = VirtualFileId::new(); + + sheet + .add_mapping( + vcs_data::data::sheet::SheetPathBuf::from("output/build.exe"), + build_exe_id, + ) + .await?; + + // Persist the sheet + sheet.persist().await?; + + // Verify the sheet file was created + const SHEET_NAME_PARAM: &str = "{sheet-name}"; + let sheet_path = dir.join(SERVER_FILE_SHEET.replace(SHEET_NAME_PARAM, &sheet_name)); + assert!(sheet_path.exists()); + + // Clean up + vault.remove_member_from_vault(&member_id)?; + + Ok(()) +} diff --git a/crates/vcs_data/vcs_data_test/src/test_vault_setup_and_member_register.rs b/crates/vcs_data/vcs_data_test/src/test_vault_setup_and_member_register.rs new file mode 100644 index 0000000..80ae39e --- /dev/null +++ b/crates/vcs_data/vcs_data_test/src/test_vault_setup_and_member_register.rs @@ -0,0 +1,67 @@ +use std::io::Error; + +use cfg_file::config::ConfigFile; +use vcs_data::{ + constants::{ + SERVER_FILE_MEMBER_INFO, SERVER_FILE_README, SERVER_FILE_VAULT, SERVER_PATH_MEMBER_PUB, + SERVER_PATH_MEMBERS, SERVER_PATH_SHEETS, SERVER_PATH_VF_ROOT, + }, + data::{ + member::Member, + vault::{Vault, config::VaultConfig}, + }, +}; + +use crate::get_test_dir; + +#[tokio::test] +async fn test_vault_setup_and_member_register() -> Result<(), std::io::Error> { + let dir = get_test_dir("member_register").await?; + + // Setup vault + Vault::setup_vault(dir.clone()).await?; + + // Check if the following files and directories are created in `dir`: + // Files: SERVER_FILE_VAULT, SERVER_FILE_README + // Directories: SERVER_PATH_SHEETS, + // SERVER_PATH_MEMBERS, + // SERVER_PATH_MEMBER_PUB, + // SERVER_PATH_VIRTUAL_FILE_ROOT + assert!(dir.join(SERVER_FILE_VAULT).exists()); + assert!(dir.join(SERVER_FILE_README).exists()); + assert!(dir.join(SERVER_PATH_SHEETS).exists()); + assert!(dir.join(SERVER_PATH_MEMBERS).exists()); + assert!(dir.join(SERVER_PATH_MEMBER_PUB).exists()); + assert!(dir.join(SERVER_PATH_VF_ROOT).exists()); + + // Get vault + let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?; + let Some(vault) = Vault::init(config, &dir) else { + return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!")); + }; + + // Add member + let member_id = "test_member"; + vault + .register_member_to_vault(Member::new(member_id)) + .await?; + + const ID_PARAM: &str = "{member_id}"; + + // Check if the member info file exists + assert!( + dir.join(SERVER_FILE_MEMBER_INFO.replace(ID_PARAM, member_id)) + .exists() + ); + + // Remove member + vault.remove_member_from_vault(&member_id.to_string())?; + + // Check if the member info file not exists + assert!( + !dir.join(SERVER_FILE_MEMBER_INFO.replace(ID_PARAM, member_id)) + .exists() + ); + + Ok(()) +} diff --git a/crates/vcs_data/vcs_data_test/src/test_virtual_file_creation_and_update.rs b/crates/vcs_data/vcs_data_test/src/test_virtual_file_creation_and_update.rs new file mode 100644 index 0000000..7e30dad --- /dev/null +++ b/crates/vcs_data/vcs_data_test/src/test_virtual_file_creation_and_update.rs @@ -0,0 +1,162 @@ +use std::time::Duration; + +use cfg_file::config::ConfigFile; +use tcp_connection_test::{ + handle::{ClientHandle, ServerHandle}, + target::TcpServerTarget, + target_configure::ServerTargetConfig, +}; +use tokio::{ + join, + time::{sleep, timeout}, +}; +use vcs_data::{ + constants::SERVER_FILE_VAULT, + data::{ + member::Member, + vault::{Vault, config::VaultConfig, virtual_file::VirtualFileVersionDescription}, + }, +}; + +use crate::get_test_dir; + +struct VirtualFileCreateClientHandle; +struct VirtualFileCreateServerHandle; + +impl ClientHandle for VirtualFileCreateClientHandle { + async fn process(mut instance: tcp_connection::instance::ConnectionInstance) { + let dir = get_test_dir("virtual_file_creation_and_update_2") + .await + .unwrap(); + // Create first test file for virtual file creation + let test_content_1 = b"Test file content for virtual file creation"; + let temp_file_path_1 = dir.join("test_virtual_file_1.txt"); + + tokio::fs::write(&temp_file_path_1, test_content_1) + .await + .unwrap(); + + // Send the first file to server for virtual file creation + instance.write_file(&temp_file_path_1).await.unwrap(); + + // Create second test file for virtual file update + let test_content_2 = b"Updated test file content for virtual file"; + let temp_file_path_2 = dir.join("test_virtual_file_2.txt"); + + tokio::fs::write(&temp_file_path_2, test_content_2) + .await + .unwrap(); + + // Send the second file to server for virtual file update + instance.write_file(&temp_file_path_2).await.unwrap(); + } +} + +impl ServerHandle for VirtualFileCreateServerHandle { + async fn process(mut instance: tcp_connection::instance::ConnectionInstance) { + let dir = get_test_dir("virtual_file_creation_and_update") + .await + .unwrap(); + + // Setup vault + Vault::setup_vault(dir.clone()).await.unwrap(); + + // Read vault + let Some(vault) = Vault::init( + VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)) + .await + .unwrap(), + &dir, + ) else { + panic!("No vault found!"); + }; + + // Register member + let member_id = "test_member"; + vault + .register_member_to_vault(Member::new(member_id)) + .await + .unwrap(); + + // Create visual file + let virtual_file_id = vault + .create_virtual_file_from_connection(&mut instance, &member_id.to_string()) + .await + .unwrap(); + + // Grant edit right to member + vault + .grant_virtual_file_edit_right(&member_id.to_string(), &virtual_file_id) + .await + .unwrap(); + + // Update visual file + vault + .update_virtual_file_from_connection( + &mut instance, + &member_id.to_string(), + &virtual_file_id, + &"2".to_string(), + VirtualFileVersionDescription { + creator: member_id.to_string(), + description: "Update".to_string(), + }, + ) + .await + .unwrap(); + } +} + +#[tokio::test] +async fn test_virtual_file_creation_and_update() -> Result<(), std::io::Error> { + let host = "localhost:5009"; + + // Server setup + let Ok(server_target) = TcpServerTarget::< + VirtualFileCreateClientHandle, + VirtualFileCreateServerHandle, + >::from_domain(host) + .await + else { + panic!("Test target built failed from a domain named `{}`", host); + }; + + // Client setup + let Ok(client_target) = TcpServerTarget::< + VirtualFileCreateClientHandle, + VirtualFileCreateServerHandle, + >::from_domain(host) + .await + else { + panic!("Test target built failed from a domain named `{}`", host); + }; + + let future_server = async move { + // Only process once + let configured_server = server_target.server_cfg(ServerTargetConfig::default().once()); + + // Listen here + let _ = configured_server.listen().await; + }; + + let future_client = async move { + // Wait for server start + let _ = sleep(Duration::from_secs_f32(1.5)).await; + + // Connect here + let _ = client_target.connect().await; + }; + + let test_timeout = Duration::from_secs(15); + + timeout(test_timeout, async { join!(future_client, future_server) }) + .await + .map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::TimedOut, + format!("Test timed out after {:?}", test_timeout), + ) + })?; + + Ok(()) +} -- cgit From 03a5ff8a1629cde933120faf47963fcb59118261 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 6 Oct 2025 04:11:43 +0800 Subject: Create vcs_actions crate for client-server interaction logic - Add new crate to combine action_system and vcs_data functionality - Define dependencies on both action_system and vcs_data crates - Prepare structure for implementing client-server communication logic --- crates/vcs_actions/Cargo.toml | 15 +++++++++++++++ crates/vcs_actions/src/actions.rs | 5 +++++ crates/vcs_actions/src/actions/local_actions.rs | 0 crates/vcs_actions/src/actions/sheet_actions.rs | 0 crates/vcs_actions/src/actions/user_actions.rs | 0 crates/vcs_actions/src/actions/vault_actions.rs | 0 crates/vcs_actions/src/actions/virtual_file_actions.rs | 0 crates/vcs_actions/src/lib.rs | 2 ++ crates/vcs_actions/src/registry.rs | 2 ++ crates/vcs_actions/src/registry/client_registry.rs | 0 crates/vcs_actions/src/registry/server_registry.rs | 0 11 files changed, 24 insertions(+) create mode 100644 crates/vcs_actions/Cargo.toml create mode 100644 crates/vcs_actions/src/actions.rs create mode 100644 crates/vcs_actions/src/actions/local_actions.rs create mode 100644 crates/vcs_actions/src/actions/sheet_actions.rs create mode 100644 crates/vcs_actions/src/actions/user_actions.rs create mode 100644 crates/vcs_actions/src/actions/vault_actions.rs create mode 100644 crates/vcs_actions/src/actions/virtual_file_actions.rs create mode 100644 crates/vcs_actions/src/lib.rs create mode 100644 crates/vcs_actions/src/registry.rs create mode 100644 crates/vcs_actions/src/registry/client_registry.rs create mode 100644 crates/vcs_actions/src/registry/server_registry.rs diff --git a/crates/vcs_actions/Cargo.toml b/crates/vcs_actions/Cargo.toml new file mode 100644 index 0000000..e5a07f6 --- /dev/null +++ b/crates/vcs_actions/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "vcs_actions" +edition = "2024" +version.workspace = true + +[dependencies] + +# Utils +tcp_connection = { path = "../utils/tcp_connection" } +cfg_file = { path = "../utils/cfg_file", features = ["default"] } +string_proc = { path = "../utils/string_proc" } + +# Core dependencies +action_system = { path = "../system_action" } +vcs_data = { path = "../vcs_data" } diff --git a/crates/vcs_actions/src/actions.rs b/crates/vcs_actions/src/actions.rs new file mode 100644 index 0000000..20bd037 --- /dev/null +++ b/crates/vcs_actions/src/actions.rs @@ -0,0 +1,5 @@ +pub mod local_actions; +pub mod sheet_actions; +pub mod user_actions; +pub mod vault_actions; +pub mod virtual_file_actions; diff --git a/crates/vcs_actions/src/actions/local_actions.rs b/crates/vcs_actions/src/actions/local_actions.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/vcs_actions/src/actions/sheet_actions.rs b/crates/vcs_actions/src/actions/sheet_actions.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/vcs_actions/src/actions/user_actions.rs b/crates/vcs_actions/src/actions/user_actions.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/vcs_actions/src/actions/vault_actions.rs b/crates/vcs_actions/src/actions/vault_actions.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/vcs_actions/src/actions/virtual_file_actions.rs b/crates/vcs_actions/src/actions/virtual_file_actions.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/vcs_actions/src/lib.rs b/crates/vcs_actions/src/lib.rs new file mode 100644 index 0000000..92de35f --- /dev/null +++ b/crates/vcs_actions/src/lib.rs @@ -0,0 +1,2 @@ +pub mod actions; +pub mod registry; diff --git a/crates/vcs_actions/src/registry.rs b/crates/vcs_actions/src/registry.rs new file mode 100644 index 0000000..ceec1a1 --- /dev/null +++ b/crates/vcs_actions/src/registry.rs @@ -0,0 +1,2 @@ +pub mod client_registry; +pub mod server_registry; diff --git a/crates/vcs_actions/src/registry/client_registry.rs b/crates/vcs_actions/src/registry/client_registry.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/vcs_actions/src/registry/server_registry.rs b/crates/vcs_actions/src/registry/server_registry.rs new file mode 100644 index 0000000..e69de29 -- cgit From ee7cba690582b9c47e8c856bf0bd331eedda7908 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 6 Oct 2025 04:11:58 +0800 Subject: Remove old vcs directory after migration to vcs_data - Delete entire crates/vcs directory and its contents - Remove test files and configuration from old structure - Complete transition to new vcs_data and vcs_actions architecture --- crates/vcs/Cargo.toml | 22 - crates/vcs/src/constants.rs | 54 --- crates/vcs/src/current.rs | 78 ---- crates/vcs/src/data.rs | 5 - crates/vcs/src/data/local.rs | 100 ----- crates/vcs/src/data/local/config.rs | 53 --- crates/vcs/src/data/member.rs | 69 --- crates/vcs/src/data/sheet.rs | 347 --------------- crates/vcs/src/data/user.rs | 28 -- crates/vcs/src/data/user/accounts.rs | 164 ------- crates/vcs/src/data/vault.rs | 146 ------- crates/vcs/src/data/vault/config.rs | 77 ---- crates/vcs/src/data/vault/member.rs | 140 ------ crates/vcs/src/data/vault/sheets.rs | 268 ------------ crates/vcs/src/data/vault/virtual_file.rs | 473 --------------------- crates/vcs/src/lib.rs | 5 - crates/vcs/todo.txt | 36 -- crates/vcs/vcs_test/Cargo.toml | 13 - crates/vcs/vcs_test/lib.rs | 11 - crates/vcs/vcs_test/src/lib.rs | 27 -- ...local_workspace_setup_and_account_management.rs | 248 ----------- ...st_sheet_creation_management_and_persistence.rs | 307 ------------- .../src/test_vault_setup_and_member_register.rs | 67 --- .../src/test_virtual_file_creation_and_update.rs | 162 ------- 24 files changed, 2900 deletions(-) delete mode 100644 crates/vcs/Cargo.toml delete mode 100644 crates/vcs/src/constants.rs delete mode 100644 crates/vcs/src/current.rs delete mode 100644 crates/vcs/src/data.rs delete mode 100644 crates/vcs/src/data/local.rs delete mode 100644 crates/vcs/src/data/local/config.rs delete mode 100644 crates/vcs/src/data/member.rs delete mode 100644 crates/vcs/src/data/sheet.rs delete mode 100644 crates/vcs/src/data/user.rs delete mode 100644 crates/vcs/src/data/user/accounts.rs delete mode 100644 crates/vcs/src/data/vault.rs delete mode 100644 crates/vcs/src/data/vault/config.rs delete mode 100644 crates/vcs/src/data/vault/member.rs delete mode 100644 crates/vcs/src/data/vault/sheets.rs delete mode 100644 crates/vcs/src/data/vault/virtual_file.rs delete mode 100644 crates/vcs/src/lib.rs delete mode 100644 crates/vcs/todo.txt delete mode 100644 crates/vcs/vcs_test/Cargo.toml delete mode 100644 crates/vcs/vcs_test/lib.rs delete mode 100644 crates/vcs/vcs_test/src/lib.rs delete mode 100644 crates/vcs/vcs_test/src/test_local_workspace_setup_and_account_management.rs delete mode 100644 crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs delete mode 100644 crates/vcs/vcs_test/src/test_vault_setup_and_member_register.rs delete mode 100644 crates/vcs/vcs_test/src/test_virtual_file_creation_and_update.rs diff --git a/crates/vcs/Cargo.toml b/crates/vcs/Cargo.toml deleted file mode 100644 index 888e18d..0000000 --- a/crates/vcs/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "vcs" -edition = "2024" -version.workspace = true - -[dependencies] -tcp_connection = { path = "../utils/tcp_connection" } -cfg_file = { path = "../utils/cfg_file", features = ["default"] } -string_proc = { path = "../utils/string_proc" } -action_system = { path = "../system_action" } - -# Identity -uuid = { version = "1.18.1", features = ["v4", "serde"] } - -# Serialization -serde = { version = "1.0.219", features = ["derive"] } - -# Async & Networking -tokio = { version = "1.46.1", features = ["full"] } - -# Filesystem -dirs = "6.0.0" diff --git a/crates/vcs/src/constants.rs b/crates/vcs/src/constants.rs deleted file mode 100644 index 5e147c4..0000000 --- a/crates/vcs/src/constants.rs +++ /dev/null @@ -1,54 +0,0 @@ -// ------------------------------------------------------------------------------------- -// - -// Project -pub const PATH_TEMP: &str = "./.temp/"; - -// Default Port -pub const PORT: u16 = 25331; - -// Vault Host Name -pub const VAULT_HOST_NAME: &str = "host"; - -// Server -// Server - Vault (Main) -pub const SERVER_FILE_VAULT: &str = "./vault.toml"; // crates::env::vault::vault_config - -// Server - Sheets -pub const REF_SHEET_NAME: &str = "ref"; -pub const SERVER_PATH_SHEETS: &str = "./sheets/"; -pub const SERVER_FILE_SHEET: &str = "./sheets/{sheet-name}.yaml"; - -// Server - Members -pub const SERVER_PATH_MEMBERS: &str = "./members/"; -pub const SERVER_PATH_MEMBER_PUB: &str = "./key/"; -pub const SERVER_FILE_MEMBER_INFO: &str = "./members/{member_id}.toml"; // crates::env::member::manager -pub const SERVER_FILE_MEMBER_PUB: &str = "./key/{member_id}.pem"; // crates::utils::tcp_connection::instance - -// Server - Virtual File Storage -pub const SERVER_PATH_VF_TEMP: &str = "./.temp/{temp_name}"; -pub const SERVER_PATH_VF_ROOT: &str = "./storage/"; -pub const SERVER_PATH_VF_STORAGE: &str = "./storage/{vf_index}/{vf_id}/"; -pub const SERVER_FILE_VF_VERSION_INSTANCE: &str = "./storage/{vf_index}/{vf_id}/{vf_version}.rf"; -pub const SERVER_FILE_VF_META: &str = "./storage/{vf_index}/{vf_id}/meta.yaml"; - -pub const SERVER_FILE_README: &str = "./README.md"; - -// ------------------------------------------------------------------------------------- - -// Client -pub const CLIENT_PATH_WORKSPACE_ROOT: &str = "./.jv/"; - -// Client - Workspace (Main) -pub const CLIENT_FILE_WORKSPACE: &str = "./.jv/workspace.toml"; // crates::env::local::local_config - -// Client - Other -pub const CLIENT_FILE_IGNOREFILES: &str = ".jgnore .gitignore"; // Support gitignore file. -pub const CLIENT_FILE_README: &str = "./README.md"; - -// ------------------------------------------------------------------------------------- - -// User - Verify (Documents path) -pub const USER_FILE_ACCOUNTS: &str = "./accounts/"; -pub const USER_FILE_KEY: &str = "./accounts/{self_id}_private.pem"; -pub const USER_FILE_MEMBER: &str = "./accounts/{self_id}.toml"; diff --git a/crates/vcs/src/current.rs b/crates/vcs/src/current.rs deleted file mode 100644 index 97b5058..0000000 --- a/crates/vcs/src/current.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::constants::*; -use std::io::{self, Error}; -use std::{env::set_current_dir, path::PathBuf}; - -/// Find the nearest vault or local workspace and correct the `current_dir` to it -pub fn correct_current_dir() -> Result<(), io::Error> { - if let Some(local_workspace) = current_local_path() { - set_current_dir(local_workspace)?; - return Ok(()); - } - if let Some(vault) = current_vault_path() { - set_current_dir(vault)?; - return Ok(()); - } - Err(Error::new( - io::ErrorKind::NotFound, - "Could not find any vault or local workspace!", - )) -} - -/// Get the nearest Vault directory from `current_dir` -pub fn current_vault_path() -> Option { - let current_dir = std::env::current_dir().ok()?; - find_vault_path(current_dir) -} - -/// Get the nearest local workspace from `current_dir` -pub fn current_local_path() -> Option { - let current_dir = std::env::current_dir().ok()?; - find_local_path(current_dir) -} - -/// Get the nearest Vault directory from the specified path -pub fn find_vault_path(path: impl Into) -> Option { - let mut current_path = path.into(); - let vault_file = SERVER_FILE_VAULT; - - loop { - let vault_toml_path = current_path.join(vault_file); - if vault_toml_path.exists() { - return Some(current_path); - } - - if let Some(parent) = current_path.parent() { - current_path = parent.to_path_buf(); - } else { - break; - } - } - - None -} - -/// Get the nearest local workspace from the specified path -pub fn find_local_path(path: impl Into) -> Option { - let mut current_path = path.into(); - let workspace_dir = CLIENT_PATH_WORKSPACE_ROOT; - - loop { - let jvc_path = current_path.join(workspace_dir); - if jvc_path.exists() { - return Some(current_path); - } - - if let Some(parent) = current_path.parent() { - current_path = parent.to_path_buf(); - } else { - break; - } - } - - None -} - -/// Get the system's document directory and join with .just_enough_vcs -pub fn current_doc_dir() -> Option { - dirs::document_dir().map(|path| path.join(".just_enough_vcs")) -} diff --git a/crates/vcs/src/data.rs b/crates/vcs/src/data.rs deleted file mode 100644 index ed9383a..0000000 --- a/crates/vcs/src/data.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod local; -pub mod member; -pub mod sheet; -pub mod user; -pub mod vault; diff --git a/crates/vcs/src/data/local.rs b/crates/vcs/src/data/local.rs deleted file mode 100644 index 1c99832..0000000 --- a/crates/vcs/src/data/local.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::{env::current_dir, path::PathBuf}; - -use cfg_file::config::ConfigFile; -use tokio::fs; - -use crate::{ - constants::{CLIENT_FILE_README, CLIENT_FILE_WORKSPACE}, - current::{current_local_path, find_local_path}, - data::local::config::LocalConfig, -}; - -pub mod config; - -pub struct LocalWorkspace { - config: LocalConfig, - local_path: PathBuf, -} - -impl LocalWorkspace { - /// Get the path of the local workspace. - pub fn local_path(&self) -> &PathBuf { - &self.local_path - } - - /// Initialize local workspace. - pub fn init(config: LocalConfig, local_path: impl Into) -> Option { - let local_path = find_local_path(local_path)?; - Some(Self { config, local_path }) - } - - /// Initialize local workspace in the current directory. - pub fn init_current_dir(config: LocalConfig) -> Option { - let local_path = current_local_path()?; - Some(Self { config, local_path }) - } - - /// Setup local workspace - pub async fn setup_local_workspace( - local_path: impl Into, - ) -> Result<(), std::io::Error> { - let local_path: PathBuf = local_path.into(); - - // Ensure directory is empty - if local_path.exists() && local_path.read_dir()?.next().is_some() { - return Err(std::io::Error::new( - std::io::ErrorKind::DirectoryNotEmpty, - "DirectoryNotEmpty", - )); - } - - // 1. Setup config - let config = LocalConfig::default(); - LocalConfig::write_to(&config, local_path.join(CLIENT_FILE_WORKSPACE)).await?; - - // 2. Setup README.md - let readme_content = "\ -# JustEnoughVCS Local Workspace - -This directory is a **Local Workspace** managed by `JustEnoughVCS`. All files and subdirectories within this scope can be version-controlled using the `JustEnoughVCS` CLI or GUI tools, with the following exceptions: - -- The `.jv` directory -- Any files or directories excluded via `.jgnore` or `.gitignore` - -> ⚠️ **Warning** -> -> Files in this workspace will be uploaded to the upstream server. Please ensure you fully trust this server before proceeding. - -## Access Requirements - -To use `JustEnoughVCS` with this workspace, you must have: - -- **A registered user ID** with the upstream server -- **Your private key** properly configured locally -- **Your public key** stored in the server's public key directory - -Without these credentials, the server will reject all access requests. - -## Support - -- **Permission or access issues?** → Contact your server administrator -- **Tooling problems or bugs?** → Reach out to the development team via [GitHub Issues](https://github.com/JustEnoughVCS/VersionControl/issues) -- **Documentation**: Visit our repository for full documentation - ------- - -*Thank you for using JustEnoughVCS!* -".to_string() - .trim() - .to_string(); - fs::write(local_path.join(CLIENT_FILE_README), readme_content).await?; - - Ok(()) - } - - /// Setup local workspace in current directory - pub async fn setup_local_workspacecurrent_dir() -> Result<(), std::io::Error> { - Self::setup_local_workspace(current_dir()?).await?; - Ok(()) - } -} diff --git a/crates/vcs/src/data/local/config.rs b/crates/vcs/src/data/local/config.rs deleted file mode 100644 index 5444047..0000000 --- a/crates/vcs/src/data/local/config.rs +++ /dev/null @@ -1,53 +0,0 @@ -use cfg_file::ConfigFile; -use serde::{Deserialize, Serialize}; -use std::net::SocketAddr; - -use crate::constants::CLIENT_FILE_WORKSPACE; -use crate::constants::PORT; -use crate::data::member::MemberId; - -#[derive(Serialize, Deserialize, ConfigFile)] -#[cfg_file(path = CLIENT_FILE_WORKSPACE)] -pub struct LocalConfig { - /// The upstream address, representing the upstream address of the local workspace, - /// to facilitate timely retrieval of new updates from the upstream source. - upstream_addr: SocketAddr, - - /// The member ID used by the current local workspace. - /// This ID will be used to verify access permissions when connecting to the upstream server. - using_account: MemberId, -} - -impl Default for LocalConfig { - fn default() -> Self { - Self { - upstream_addr: SocketAddr::V4(std::net::SocketAddrV4::new( - std::net::Ipv4Addr::new(127, 0, 0, 1), - PORT, - )), - using_account: "unknown".to_string(), - } - } -} - -impl LocalConfig { - /// Set the vault address. - pub fn set_vault_addr(&mut self, addr: SocketAddr) { - self.upstream_addr = addr; - } - - /// Get the vault address. - pub fn vault_addr(&self) -> SocketAddr { - self.upstream_addr - } - - /// Set the currently used account - pub fn set_current_account(&mut self, account: MemberId) { - self.using_account = account; - } - - /// Get the currently used account - pub fn current_account(&self) -> MemberId { - self.using_account.clone() - } -} diff --git a/crates/vcs/src/data/member.rs b/crates/vcs/src/data/member.rs deleted file mode 100644 index b5136a1..0000000 --- a/crates/vcs/src/data/member.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::collections::HashMap; - -use cfg_file::ConfigFile; -use serde::{Deserialize, Serialize}; -use string_proc::snake_case; - -pub type MemberId = String; - -#[derive(Debug, Eq, Clone, ConfigFile, Serialize, Deserialize)] -pub struct Member { - /// Member ID, the unique identifier of the member - id: String, - - /// Member metadata - metadata: HashMap, -} - -impl Default for Member { - fn default() -> Self { - Self::new("default_user") - } -} - -impl PartialEq for Member { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl std::fmt::Display for Member { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.id) - } -} - -impl std::convert::AsRef for Member { - fn as_ref(&self) -> &str { - &self.id - } -} - -impl Member { - /// Create member struct by id - pub fn new(new_id: impl Into) -> Self { - Self { - id: snake_case!(new_id.into()), - metadata: HashMap::new(), - } - } - - /// Get member id - pub fn id(&self) -> String { - self.id.clone() - } - - /// Get metadata - pub fn metadata(&self, key: impl Into) -> Option<&String> { - self.metadata.get(&key.into()) - } - - /// Set metadata - pub fn set_metadata( - &mut self, - key: impl AsRef, - value: impl Into, - ) -> Option { - self.metadata.insert(key.as_ref().to_string(), value.into()) - } -} diff --git a/crates/vcs/src/data/sheet.rs b/crates/vcs/src/data/sheet.rs deleted file mode 100644 index a6220c9..0000000 --- a/crates/vcs/src/data/sheet.rs +++ /dev/null @@ -1,347 +0,0 @@ -use std::{collections::HashMap, path::PathBuf}; - -use cfg_file::{ConfigFile, config::ConfigFile}; -use serde::{Deserialize, Serialize}; -use string_proc::simple_processer::sanitize_file_path; - -use crate::{ - constants::SERVER_FILE_SHEET, - data::{ - member::MemberId, - vault::{Vault, virtual_file::VirtualFileId}, - }, -}; - -pub type SheetName = String; -pub type SheetPathBuf = PathBuf; -pub type InputName = String; -pub type InputRelativePathBuf = PathBuf; - -#[derive(Debug, Clone, Serialize, Deserialize, Eq)] -pub struct InputPackage { - /// Name of the input package - pub name: InputName, - - /// The sheet from which this input package was created - pub from: SheetName, - - /// Files in this input package with their relative paths and virtual file IDs - pub files: Vec<(InputRelativePathBuf, VirtualFileId)>, -} - -impl PartialEq for InputPackage { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - } -} - -const SHEET_NAME: &str = "{sheet-name}"; - -pub struct Sheet<'a> { - /// The name of the current sheet - pub(crate) name: SheetName, - - /// Sheet data - pub(crate) data: SheetData, - - /// Sheet path - pub(crate) vault_reference: &'a Vault, -} - -#[derive(Default, Serialize, Deserialize, ConfigFile)] -pub struct SheetData { - /// The holder of the current sheet, who has full operation rights to the sheet mapping - pub(crate) holder: MemberId, - - /// Inputs - pub(crate) inputs: Vec, - - /// Mapping of sheet paths to virtual file IDs - pub(crate) mapping: HashMap, -} - -impl<'a> Sheet<'a> { - /// Get the holder of this sheet - pub fn holder(&self) -> &MemberId { - &self.data.holder - } - - /// Get the inputs of this sheet - pub fn inputs(&self) -> &Vec { - &self.data.inputs - } - - /// Get the names of the inputs of this sheet - pub fn input_names(&self) -> Vec { - self.data - .inputs - .iter() - .map(|input| input.name.clone()) - .collect() - } - - /// Get the mapping of this sheet - pub fn mapping(&self) -> &HashMap { - &self.data.mapping - } - - /// Add an input package to the sheet - pub fn add_input(&mut self, input_package: InputPackage) -> Result<(), std::io::Error> { - if self.data.inputs.iter().any(|input| input == &input_package) { - return Err(std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - format!("Input package '{}' already exists", input_package.name), - )); - } - self.data.inputs.push(input_package); - Ok(()) - } - - /// Deny and remove an input package from the sheet - pub fn deny_input(&mut self, input_name: &InputName) -> Option { - self.data - .inputs - .iter() - .position(|input| input.name == *input_name) - .map(|pos| self.data.inputs.remove(pos)) - } - - /// Accept an input package and insert to the sheet - pub fn accept_import( - &mut self, - input_name: &InputName, - insert_to: &SheetPathBuf, - ) -> Result<(), std::io::Error> { - // Remove inputs - let input = self - .inputs() - .iter() - .position(|input| input.name == *input_name) - .map(|pos| self.data.inputs.remove(pos)); - - // Ensure input is not empty - let Some(input) = input else { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Empty inputs.", - )); - }; - - // Insert to sheet - for (relative_path, virtual_file_id) in input.files { - let _ = self.add_mapping(insert_to.join(relative_path), virtual_file_id); - } - - Ok(()) - } - - /// Add (or Edit) a mapping entry to the sheet - /// - /// This operation performs safety checks to ensure the member has the right to add the mapping: - /// 1. If the virtual file ID doesn't exist in the vault, the mapping is added directly - /// 2. If the virtual file exists, check if the member has edit rights to the virtual file - /// 3. If member has edit rights, the mapping is not allowed to be modified and returns an error - /// 4. If member doesn't have edit rights, the mapping is allowed (member is giving up the file) - /// - /// Note: Full validation adds overhead - avoid frequent calls - pub async fn add_mapping( - &mut self, - sheet_path: SheetPathBuf, - virtual_file_id: VirtualFileId, - ) -> Result<(), std::io::Error> { - // Check if the virtual file exists in the vault - if self.vault_reference.virtual_file(&virtual_file_id).is_err() { - // Virtual file doesn't exist, add the mapping directly - self.data.mapping.insert(sheet_path, virtual_file_id); - return Ok(()); - } - - // Check if the holder has edit rights to the virtual file - match self - .vault_reference - .has_virtual_file_edit_right(self.holder(), &virtual_file_id) - .await - { - Ok(false) => { - // Holder doesn't have rights, add the mapping (member is giving up the file) - self.data.mapping.insert(sheet_path, virtual_file_id); - Ok(()) - } - Ok(true) => { - // Holder has edit rights, don't allow modifying the mapping - Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "Member has edit rights to the virtual file, cannot modify mapping", - )) - } - Err(_) => { - // Error checking rights, don't allow modifying the mapping - Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to check virtual file edit rights", - )) - } - } - } - - /// Remove a mapping entry from the sheet - /// - /// This operation performs safety checks to ensure the member has the right to remove the mapping: - /// 1. Member must NOT have edit rights to the virtual file to release it (ensuring clear ownership) - /// 2. If the virtual file doesn't exist, the mapping is removed but no ID is returned - /// 3. If member has no edit rights and the file exists, returns the removed virtual file ID - /// - /// Note: Full validation adds overhead - avoid frequent calls - pub async fn remove_mapping(&mut self, sheet_path: &SheetPathBuf) -> Option { - let virtual_file_id = match self.data.mapping.get(sheet_path) { - Some(id) => id, - None => { - // The mapping entry doesn't exist, nothing to remove - return None; - } - }; - - // Check if the virtual file exists in the vault - if self.vault_reference.virtual_file(virtual_file_id).is_err() { - // Virtual file doesn't exist, remove the mapping and return None - self.data.mapping.remove(sheet_path); - return None; - } - - // Check if the holder has edit rights to the virtual file - match self - .vault_reference - .has_virtual_file_edit_right(self.holder(), virtual_file_id) - .await - { - Ok(false) => { - // Holder doesn't have rights, remove and return the virtual file ID - self.data.mapping.remove(sheet_path) - } - Ok(true) => { - // Holder has edit rights, don't remove the mapping - None - } - Err(_) => { - // Error checking rights, don't remove the mapping - None - } - } - } - - /// Persist the sheet to disk - /// - /// Why not use a reference? - /// Because I don't want a second instance of the sheet to be kept in memory. - /// If needed, please deserialize and reload it. - pub async fn persist(self) -> Result<(), std::io::Error> { - SheetData::write_to(&self.data, self.sheet_path()).await - } - - /// Get the path to the sheet file - pub fn sheet_path(&self) -> PathBuf { - Sheet::sheet_path_with_name(self.vault_reference, &self.name) - } - - /// Get the path to the sheet file with the given name - pub fn sheet_path_with_name(vault: &Vault, name: impl AsRef) -> PathBuf { - vault - .vault_path() - .join(SERVER_FILE_SHEET.replace(SHEET_NAME, name.as_ref())) - } - - /// Export files from the current sheet as an InputPackage for importing into other sheets - /// - /// This is the recommended way to create InputPackages. It takes a list of sheet paths - /// and generates an InputPackage with optimized relative paths by removing the longest - /// common prefix from all provided paths, then placing the files under a directory - /// named with the output_name. - /// - /// # Example - /// Given paths: - /// - `MyProject/Art/Character/Model/final.fbx` - /// - `MyProject/Art/Character/Texture/final.png` - /// - `MyProject/Art/Character/README.md` - /// - /// With output_name = "MyExport", the resulting package will contain: - /// - `MyExport/Model/final.fbx` - /// - `MyExport/Texture/final.png` - /// - `MyExport/README.md` - /// - /// # Arguments - /// * `output_name` - Name of the output package (will be used as the root directory) - /// * `paths` - List of sheet paths to include in the package - /// - /// # Returns - /// Returns an InputPackage containing the exported files with optimized paths, - /// or an error if paths are empty or files are not found in the sheet mapping - pub fn output_mappings( - &self, - output_name: InputName, - paths: &[SheetPathBuf], - ) -> Result { - let output_name = sanitize_file_path(output_name); - - // Return error for empty paths since there's no need to generate an empty package - if paths.is_empty() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "Cannot generate output package with empty paths", - )); - } - - // Find the longest common prefix among all paths - let common_prefix = Self::find_longest_common_prefix(paths); - - // Create output files with optimized relative paths - let files = paths - .iter() - .map(|path| { - let relative_path = path.strip_prefix(&common_prefix).unwrap_or(path); - let output_path = PathBuf::from(&output_name).join(relative_path); - - self.data - .mapping - .get(path) - .map(|vfid| (output_path, vfid.clone())) - .ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("File not found: {:?}", path), - ) - }) - }) - .collect::, _>>()?; - - Ok(InputPackage { - name: output_name, - from: self.name.clone(), - files, - }) - } - - /// Helper function to find the longest common prefix among all paths - fn find_longest_common_prefix(paths: &[SheetPathBuf]) -> PathBuf { - if paths.is_empty() { - return PathBuf::new(); - } - - let first_path = &paths[0]; - let mut common_components = Vec::new(); - - for (component_idx, first_component) in first_path.components().enumerate() { - for path in paths.iter().skip(1) { - if let Some(component) = path.components().nth(component_idx) { - if component != first_component { - return common_components.into_iter().collect(); - } - } else { - return common_components.into_iter().collect(); - } - } - common_components.push(first_component); - } - - common_components.into_iter().collect() - } -} diff --git a/crates/vcs/src/data/user.rs b/crates/vcs/src/data/user.rs deleted file mode 100644 index 0abd098..0000000 --- a/crates/vcs/src/data/user.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::current::current_doc_dir; -use std::path::PathBuf; - -pub mod accounts; - -pub struct UserDirectory { - local_path: PathBuf, -} - -impl UserDirectory { - /// Create a user ditectory struct from the current system's document directory - pub fn current_doc_dir() -> Option { - Some(UserDirectory { - local_path: current_doc_dir()?, - }) - } - - /// Create a user directory struct from a specified directory path - /// Returns None if the directory does not exist - pub fn from_path>(path: P) -> Option { - let local_path = path.into(); - if local_path.exists() { - Some(UserDirectory { local_path }) - } else { - None - } - } -} diff --git a/crates/vcs/src/data/user/accounts.rs b/crates/vcs/src/data/user/accounts.rs deleted file mode 100644 index d77bc02..0000000 --- a/crates/vcs/src/data/user/accounts.rs +++ /dev/null @@ -1,164 +0,0 @@ -use std::{ - fs, - io::{Error, ErrorKind}, - path::PathBuf, -}; - -use cfg_file::config::ConfigFile; - -use crate::{ - constants::{USER_FILE_ACCOUNTS, USER_FILE_KEY, USER_FILE_MEMBER}, - data::{ - member::{Member, MemberId}, - user::UserDirectory, - }, -}; - -const SELF_ID: &str = "{self_id}"; - -/// Account Management -impl UserDirectory { - /// Read account from configuration file - pub async fn account(&self, id: &MemberId) -> Result { - if let Some(cfg_file) = self.account_cfg(id) { - let member = Member::read_from(cfg_file).await?; - return Ok(member); - } - - Err(Error::new(ErrorKind::NotFound, "Account not found!")) - } - - /// List all account IDs in the user directory - pub fn account_ids(&self) -> Result, std::io::Error> { - let accounts_path = self - .local_path - .join(USER_FILE_ACCOUNTS.replace(SELF_ID, "")); - - if !accounts_path.exists() { - return Ok(Vec::new()); - } - - let mut account_ids = Vec::new(); - - for entry in fs::read_dir(accounts_path)? { - let entry = entry?; - let path = entry.path(); - - if path.is_file() - && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) - && path.extension().and_then(|s| s.to_str()) == Some("toml") - { - // Remove the "_private" suffix from key files if present - let account_id = file_name.replace("_private", ""); - account_ids.push(account_id); - } - } - - Ok(account_ids) - } - - /// Get all accounts - /// This method will read and deserialize account information, please pay attention to performance issues - pub async fn accounts(&self) -> Result, std::io::Error> { - let mut accounts = Vec::new(); - - for account_id in self.account_ids()? { - if let Ok(account) = self.account(&account_id).await { - accounts.push(account); - } - } - - Ok(accounts) - } - - /// Update account info - pub async fn update_account(&self, member: Member) -> Result<(), std::io::Error> { - // Ensure account exist - if self.account_cfg(&member.id()).is_some() { - let account_cfg_path = self.account_cfg_path(&member.id()); - Member::write_to(&member, account_cfg_path).await?; - return Ok(()); - } - - Err(Error::new(ErrorKind::NotFound, "Account not found!")) - } - - /// Register an account to user directory - pub async fn register_account(&self, member: Member) -> Result<(), std::io::Error> { - // Ensure account not exist - if self.account_cfg(&member.id()).is_some() { - return Err(Error::new( - ErrorKind::DirectoryNotEmpty, - format!("Account `{}` already registered!", member.id()), - )); - } - - // Ensure accounts directory exists - let accounts_dir = self - .local_path - .join(USER_FILE_ACCOUNTS.replace(SELF_ID, "")); - if !accounts_dir.exists() { - fs::create_dir_all(&accounts_dir)?; - } - - // Write config file to accounts dir - let account_cfg_path = self.account_cfg_path(&member.id()); - Member::write_to(&member, account_cfg_path).await?; - - Ok(()) - } - - /// Remove account from user directory - pub fn remove_account(&self, id: &MemberId) -> Result<(), std::io::Error> { - // Remove config file if exists - if let Some(account_cfg_path) = self.account_cfg(id) { - fs::remove_file(account_cfg_path)?; - } - - // Remove private key file if exists - if let Some(private_key_path) = self.account_private_key(id) - && private_key_path.exists() - { - fs::remove_file(private_key_path)?; - } - - Ok(()) - } - - /// Try to get the account's configuration file to determine if the account exists - pub fn account_cfg(&self, id: &MemberId) -> Option { - let cfg_file = self.account_cfg_path(id); - if cfg_file.exists() { - Some(cfg_file) - } else { - None - } - } - - /// Try to get the account's private key file to determine if the account has a private key - pub fn account_private_key(&self, id: &MemberId) -> Option { - let key_file = self.account_private_key_path(id); - if key_file.exists() { - Some(key_file) - } else { - None - } - } - - /// Check if account has private key - pub fn has_private_key(&self, id: &MemberId) -> bool { - self.account_private_key(id).is_some() - } - - /// Get the account's configuration file path, but do not check if the file exists - pub fn account_cfg_path(&self, id: &MemberId) -> PathBuf { - self.local_path - .join(USER_FILE_MEMBER.replace(SELF_ID, id.to_string().as_str())) - } - - /// Get the account's private key file path, but do not check if the file exists - pub fn account_private_key_path(&self, id: &MemberId) -> PathBuf { - self.local_path - .join(USER_FILE_KEY.replace(SELF_ID, id.to_string().as_str())) - } -} diff --git a/crates/vcs/src/data/vault.rs b/crates/vcs/src/data/vault.rs deleted file mode 100644 index 5d17a81..0000000 --- a/crates/vcs/src/data/vault.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::{ - env::current_dir, - fs::{self, create_dir_all}, - path::PathBuf, -}; - -use cfg_file::config::ConfigFile; - -use crate::{ - constants::{ - SERVER_FILE_README, SERVER_FILE_VAULT, SERVER_PATH_MEMBER_PUB, SERVER_PATH_MEMBERS, - SERVER_PATH_SHEETS, SERVER_PATH_VF_ROOT, VAULT_HOST_NAME, - }, - current::{current_vault_path, find_vault_path}, - data::{member::Member, vault::config::VaultConfig}, -}; - -pub mod config; -pub mod member; -pub mod sheets; -pub mod virtual_file; - -pub struct Vault { - config: VaultConfig, - vault_path: PathBuf, -} - -impl Vault { - /// Get vault path - pub fn vault_path(&self) -> &PathBuf { - &self.vault_path - } - - /// Initialize vault - pub fn init(config: VaultConfig, vault_path: impl Into) -> Option { - let vault_path = find_vault_path(vault_path)?; - Some(Self { config, vault_path }) - } - - /// Initialize vault - pub fn init_current_dir(config: VaultConfig) -> Option { - let vault_path = current_vault_path()?; - Some(Self { config, vault_path }) - } - - /// Setup vault - pub async fn setup_vault(vault_path: impl Into) -> Result<(), std::io::Error> { - let vault_path: PathBuf = vault_path.into(); - - // Ensure directory is empty - if vault_path.exists() && vault_path.read_dir()?.next().is_some() { - return Err(std::io::Error::new( - std::io::ErrorKind::DirectoryNotEmpty, - "DirectoryNotEmpty", - )); - } - - // 1. Setup main config - let config = VaultConfig::default(); - VaultConfig::write_to(&config, vault_path.join(SERVER_FILE_VAULT)).await?; - - // 2. Setup sheets directory - create_dir_all(vault_path.join(SERVER_PATH_SHEETS))?; - - // 3. Setup key directory - create_dir_all(vault_path.join(SERVER_PATH_MEMBER_PUB))?; - - // 4. Setup member directory - create_dir_all(vault_path.join(SERVER_PATH_MEMBERS))?; - - // 5. Setup storage directory - create_dir_all(vault_path.join(SERVER_PATH_VF_ROOT))?; - - let Some(vault) = Vault::init(config, &vault_path) else { - return Err(std::io::Error::other("Failed to initialize vault")); - }; - - // 6. Create host member - vault - .register_member_to_vault(Member::new(VAULT_HOST_NAME)) - .await?; - - // 7. Setup reference sheet - vault - .create_sheet(&"ref".to_string(), &VAULT_HOST_NAME.to_string()) - .await?; - - // Final, generate README.md - let readme_content = format!( - "\ -# JustEnoughVCS Server Setup - -This directory contains the server configuration and data for `JustEnoughVCS`. - -## User Authentication -To allow users to connect to this server, place their public keys in the `{}` directory. -Each public key file should be named `{{member_id}}.pem` (e.g., `juliet.pem`), and contain the user's public key in PEM format. - -**ECDSA:** -```bash -openssl genpkey -algorithm ed25519 -out your_name_private.pem -openssl pkey -in your_name_private.pem -pubout -out your_name.pem -``` - -**RSA:** -```bash -openssl genpkey -algorithm RSA -out your_name_private.pem -pkeyopt rsa_keygen_bits:2048 -openssl pkey -in your_name_private.pem -pubout -out your_name.pem -``` - -**DSA:** -```bash -openssl genpkey -algorithm DSA -out your_name_private.pem -pkeyopt dsa_paramgen_bits:2048 -openssl pkey -in your_name_private.pem -pubout -out your_name.pem -``` - -Place only the `your_name.pem` file in the server's `./key/` directory, renamed to match the user's member ID. - -## File Storage -All version-controlled files (Virtual File) are stored in the `{}` directory. - -## License -This software is distributed under the MIT License. For complete license details, please see the main repository homepage. - -## Support -Repository: `https://github.com/JustEnoughVCS/VersionControl` -Please report any issues or questions on the GitHub issue tracker. - -## Thanks :) -Thank you for using `JustEnoughVCS!` - ", - SERVER_PATH_MEMBER_PUB, SERVER_PATH_VF_ROOT - ) - .trim() - .to_string(); - fs::write(vault_path.join(SERVER_FILE_README), readme_content)?; - - Ok(()) - } - - /// Setup vault in current directory - pub async fn setup_vault_current_dir() -> Result<(), std::io::Error> { - Self::setup_vault(current_dir()?).await?; - Ok(()) - } -} diff --git a/crates/vcs/src/data/vault/config.rs b/crates/vcs/src/data/vault/config.rs deleted file mode 100644 index 1cfc8ef..0000000 --- a/crates/vcs/src/data/vault/config.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr}; - -use cfg_file::ConfigFile; -use serde::{Deserialize, Serialize}; - -use crate::constants::{PORT, SERVER_FILE_VAULT}; -use crate::data::member::{Member, MemberId}; - -#[derive(Serialize, Deserialize, ConfigFile)] -#[cfg_file(path = SERVER_FILE_VAULT)] -pub struct VaultConfig { - /// Vault name, which can be used as the project name and generally serves as a hint - vault_name: String, - - /// Vault admin id, a list of member id representing administrator identities - vault_admin_list: Vec, - - /// Vault server configuration, which will be loaded when connecting to the server - server_config: VaultServerConfig, -} - -#[derive(Serialize, Deserialize)] -pub struct VaultServerConfig { - /// Local IP address to bind to when the server starts - local_bind: IpAddr, - - /// TCP port to bind to when the server starts - port: u16, - - /// Whether to enable LAN discovery, allowing members on the same LAN to more easily find the upstream server - lan_discovery: bool, - - /// Authentication strength level - /// 0: Weakest - Anyone can claim any identity, fastest speed - /// 1: Basic - Any device can claim any registered identity, slightly faster - /// 2: Advanced - Uses asymmetric encryption, multiple devices can use key authentication to log in simultaneously, slightly slower - /// 3: Secure - Uses asymmetric encryption, only one device can use key for authentication at a time, much slower - /// Default is "Advanced", if using a lower security policy, ensure your server is only accessible by trusted devices - auth_strength: u8, -} - -impl Default for VaultConfig { - fn default() -> Self { - Self { - vault_name: "JustEnoughVault".to_string(), - vault_admin_list: Vec::new(), - server_config: VaultServerConfig { - local_bind: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - port: PORT, - lan_discovery: false, - auth_strength: 2, - }, - } - } -} - -/// Vault Management -impl VaultConfig { - // Change name of the vault. - pub fn change_name(&mut self, name: impl Into) { - self.vault_name = name.into() - } - - // Add admin - pub fn add_admin(&mut self, member: &Member) { - let uuid = member.id(); - if !self.vault_admin_list.contains(&uuid) { - self.vault_admin_list.push(uuid); - } - } - - // Remove admin - pub fn remove_admin(&mut self, member: &Member) { - let id = member.id(); - self.vault_admin_list.retain(|x| x != &id); - } -} diff --git a/crates/vcs/src/data/vault/member.rs b/crates/vcs/src/data/vault/member.rs deleted file mode 100644 index aebd92d..0000000 --- a/crates/vcs/src/data/vault/member.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::{ - fs, - io::{Error, ErrorKind}, - path::PathBuf, -}; - -use cfg_file::config::ConfigFile; - -use crate::{ - constants::{SERVER_FILE_MEMBER_INFO, SERVER_FILE_MEMBER_PUB, SERVER_PATH_MEMBERS}, - data::{ - member::{Member, MemberId}, - vault::Vault, - }, -}; - -const ID_PARAM: &str = "{member_id}"; - -/// Member Manage -impl Vault { - /// Read member from configuration file - pub async fn member(&self, id: &MemberId) -> Result { - if let Some(cfg_file) = self.member_cfg(id) { - let member = Member::read_from(cfg_file).await?; - return Ok(member); - } - - Err(Error::new(ErrorKind::NotFound, "Member not found!")) - } - - /// List all member IDs in the vault - pub fn member_ids(&self) -> Result, std::io::Error> { - let members_path = self.vault_path.join(SERVER_PATH_MEMBERS); - - if !members_path.exists() { - return Ok(Vec::new()); - } - - let mut member_ids = Vec::new(); - - for entry in fs::read_dir(members_path)? { - let entry = entry?; - let path = entry.path(); - - if path.is_file() - && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) - && path.extension().and_then(|s| s.to_str()) == Some("toml") - { - member_ids.push(file_name.to_string()); - } - } - - Ok(member_ids) - } - - /// Get all members - /// This method will read and deserialize member information, please pay attention to performance issues - pub async fn members(&self) -> Result, std::io::Error> { - let mut members = Vec::new(); - - for member_id in self.member_ids()? { - if let Ok(member) = self.member(&member_id).await { - members.push(member); - } - } - - Ok(members) - } - - /// Update member info - pub async fn update_member(&self, member: Member) -> Result<(), std::io::Error> { - // Ensure member exist - if self.member_cfg(&member.id()).is_some() { - let member_cfg_path = self.member_cfg_path(&member.id()); - Member::write_to(&member, member_cfg_path).await?; - return Ok(()); - } - - Err(Error::new(ErrorKind::NotFound, "Member not found!")) - } - - /// Register a member to vault - pub async fn register_member_to_vault(&self, member: Member) -> Result<(), std::io::Error> { - // Ensure member not exist - if self.member_cfg(&member.id()).is_some() { - return Err(Error::new( - ErrorKind::DirectoryNotEmpty, - format!("Member `{}` already registered!", member.id()), - )); - } - - // Wrtie config file to member dir - let member_cfg_path = self.member_cfg_path(&member.id()); - Member::write_to(&member, member_cfg_path).await?; - - Ok(()) - } - - /// Remove member from vault - pub fn remove_member_from_vault(&self, id: &MemberId) -> Result<(), std::io::Error> { - // Ensure member exist - if let Some(member_cfg_path) = self.member_cfg(id) { - fs::remove_file(member_cfg_path)?; - } - - Ok(()) - } - - /// Try to get the member's configuration file to determine if the member exists - pub fn member_cfg(&self, id: &MemberId) -> Option { - let cfg_file = self.member_cfg_path(id); - if cfg_file.exists() { - Some(cfg_file) - } else { - None - } - } - - /// Try to get the member's public key file to determine if the member has login permission - pub fn member_key(&self, id: &MemberId) -> Option { - let key_file = self.member_key_path(id); - if key_file.exists() { - Some(key_file) - } else { - None - } - } - - /// Get the member's configuration file path, but do not check if the file exists - pub fn member_cfg_path(&self, id: &MemberId) -> PathBuf { - self.vault_path - .join(SERVER_FILE_MEMBER_INFO.replace(ID_PARAM, id.to_string().as_str())) - } - - /// Get the member's public key file path, but do not check if the file exists - pub fn member_key_path(&self, id: &MemberId) -> PathBuf { - self.vault_path - .join(SERVER_FILE_MEMBER_PUB.replace(ID_PARAM, id.to_string().as_str())) - } -} diff --git a/crates/vcs/src/data/vault/sheets.rs b/crates/vcs/src/data/vault/sheets.rs deleted file mode 100644 index 0bba4f5..0000000 --- a/crates/vcs/src/data/vault/sheets.rs +++ /dev/null @@ -1,268 +0,0 @@ -use std::{collections::HashMap, io::Error}; - -use cfg_file::config::ConfigFile; -use string_proc::snake_case; -use tokio::fs; - -use crate::{ - constants::SERVER_PATH_SHEETS, - data::{ - member::MemberId, - sheet::{Sheet, SheetData, SheetName}, - vault::Vault, - }, -}; - -/// Vault Sheets Management -impl Vault { - /// Load all sheets in the vault - /// - /// It is generally not recommended to call this function frequently. - /// Although a vault typically won't contain too many sheets, - /// if individual sheet contents are large, this operation may cause - /// significant performance bottlenecks. - pub async fn sheets<'a>(&'a self) -> Result>, std::io::Error> { - let sheet_names = self.sheet_names()?; - let mut sheets = Vec::new(); - - for sheet_name in sheet_names { - let sheet = self.sheet(&sheet_name).await?; - sheets.push(sheet); - } - - Ok(sheets) - } - - /// Search for all sheet names in the vault - /// - /// The complexity of this operation is proportional to the number of sheets, - /// but generally there won't be too many sheets in a Vault - pub fn sheet_names(&self) -> Result, std::io::Error> { - // Get the sheets directory path - let sheets_dir = self.vault_path.join(SERVER_PATH_SHEETS); - - // If the directory doesn't exist, return an empty list - if !sheets_dir.exists() { - return Ok(vec![]); - } - - let mut sheet_names = Vec::new(); - - // Iterate through all files in the sheets directory - for entry in std::fs::read_dir(sheets_dir)? { - let entry = entry?; - let path = entry.path(); - - // Check if it's a YAML file - if path.is_file() - && path.extension().is_some_and(|ext| ext == "yaml") - && let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) - { - // Create a new SheetName and add it to the result list - sheet_names.push(file_stem.to_string()); - } - } - - Ok(sheet_names) - } - - /// Read a sheet from its name - /// - /// If the sheet information is successfully found in the vault, - /// it will be deserialized and read as a sheet. - /// This is the only correct way to obtain a sheet instance. - pub async fn sheet<'a>(&'a self, sheet_name: &SheetName) -> Result, std::io::Error> { - let sheet_name = snake_case!(sheet_name.clone()); - - // Get the path to the sheet file - let sheet_path = Sheet::sheet_path_with_name(self, &sheet_name); - - // Ensure the sheet file exists - if !sheet_path.exists() { - // If the sheet does not exist, try to restore it from the trash - if self.restore_sheet(&sheet_name).await.is_err() { - // If restoration fails, return an error - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Sheet `{}` not found!", sheet_name), - )); - } - } - - // Read the sheet data from the file - let data = SheetData::read_from(sheet_path).await?; - - Ok(Sheet { - name: sheet_name.clone(), - data, - vault_reference: self, - }) - } - - /// Create a sheet locally and return the sheet instance - /// - /// This method creates a new sheet in the vault with the given name and holder. - /// It will verify that the member exists and that the sheet doesn't already exist - /// before creating the sheet file with default empty data. - pub async fn create_sheet<'a>( - &'a self, - sheet_name: &SheetName, - holder: &MemberId, - ) -> Result, std::io::Error> { - let sheet_name = snake_case!(sheet_name.clone()); - - // Ensure member exists - if !self.member_cfg_path(holder).exists() { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Member `{}` not found!", &holder), - )); - } - - // Ensure sheet does not already exist - let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); - if sheet_file_path.exists() { - return Err(Error::new( - std::io::ErrorKind::AlreadyExists, - format!("Sheet `{}` already exists!", &sheet_name), - )); - } - - // Create the sheet file - let sheet_data = SheetData { - holder: holder.clone(), - inputs: Vec::new(), - mapping: HashMap::new(), - }; - SheetData::write_to(&sheet_data, sheet_file_path).await?; - - Ok(Sheet { - name: sheet_name, - data: sheet_data, - vault_reference: self, - }) - } - - /// Delete the sheet file from local disk by name - /// - /// This method will remove the sheet file with the given name from the vault. - /// It will verify that the sheet exists before attempting to delete it. - /// If the sheet is successfully deleted, it will return Ok(()). - /// - /// Warning: This operation is dangerous. Deleting a sheet will cause local workspaces - /// using this sheet to become invalid. Please ensure the sheet is not currently in use - /// and will not be used in the future. - /// - /// For a safer deletion method, consider using `delete_sheet_safety`. - /// - /// Note: This function is intended for server-side use only and should not be - /// arbitrarily called by other members to prevent unauthorized data deletion. - pub async fn delete_sheet(&self, sheet_name: &SheetName) -> Result<(), std::io::Error> { - let sheet_name = snake_case!(sheet_name.clone()); - - // Ensure sheet exists - let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); - if !sheet_file_path.exists() { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Sheet `{}` not found!", &sheet_name), - )); - } - - // Delete the sheet file - fs::remove_file(sheet_file_path).await?; - - Ok(()) - } - - /// Safely delete the sheet - /// - /// The sheet will be moved to the trash directory, ensuring it does not appear in the - /// results of `sheets` and `sheet_names` methods. - /// However, if the sheet's holder attempts to access the sheet through the `sheet` method, - /// the system will automatically restore it from the trash directory. - /// This means: the sheet will only permanently remain in the trash directory, - /// waiting for manual cleanup by an administrator, when it is truly no longer in use. - /// - /// This is a safer deletion method because it provides the possibility of recovery, - /// avoiding irreversible data loss caused by accidental deletion. - /// - /// Note: This function is intended for server-side use only and should not be - /// arbitrarily called by other members to prevent unauthorized data deletion. - pub async fn delete_sheet_safely(&self, sheet_name: &SheetName) -> Result<(), std::io::Error> { - let sheet_name = snake_case!(sheet_name.clone()); - - // Ensure the sheet exists - let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); - if !sheet_file_path.exists() { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Sheet `{}` not found!", &sheet_name), - )); - } - - // Create the trash directory - let trash_dir = self.vault_path.join(".trash"); - if !trash_dir.exists() { - fs::create_dir_all(&trash_dir).await?; - } - - // Generate a unique filename in the trash - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis(); - let trash_file_name = format!("{}_{}.yaml", sheet_name, timestamp); - let trash_path = trash_dir.join(trash_file_name); - - // Move the sheet file to the trash - fs::rename(&sheet_file_path, &trash_path).await?; - - Ok(()) - } - - /// Restore the sheet from the trash - /// - /// Restore the specified sheet from the trash to its original location, making it accessible normally. - pub async fn restore_sheet(&self, sheet_name: &SheetName) -> Result<(), std::io::Error> { - let sheet_name = snake_case!(sheet_name.clone()); - - // Search for matching files in the trash - let trash_dir = self.vault_path.join(".trash"); - if !trash_dir.exists() { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "Trash directory does not exist!".to_string(), - )); - } - - let mut found_path = None; - for entry in std::fs::read_dir(&trash_dir)? { - let entry = entry?; - let path = entry.path(); - - if path.is_file() - && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) - { - // Check if the filename starts with the sheet name - if file_name.starts_with(&sheet_name) { - found_path = Some(path); - break; - } - } - } - - let trash_path = found_path.ok_or_else(|| { - Error::new( - std::io::ErrorKind::NotFound, - format!("Sheet `{}` not found in trash!", &sheet_name), - ) - })?; - - // Restore the sheet to its original location - let original_path = Sheet::sheet_path_with_name(self, &sheet_name); - fs::rename(&trash_path, &original_path).await?; - - Ok(()) - } -} diff --git a/crates/vcs/src/data/vault/virtual_file.rs b/crates/vcs/src/data/vault/virtual_file.rs deleted file mode 100644 index fe83594..0000000 --- a/crates/vcs/src/data/vault/virtual_file.rs +++ /dev/null @@ -1,473 +0,0 @@ -use std::{ - collections::HashMap, - io::{Error, ErrorKind}, - path::PathBuf, -}; - -use cfg_file::{ConfigFile, config::ConfigFile}; -use serde::{Deserialize, Serialize}; -use string_proc::snake_case; -use tcp_connection::instance::ConnectionInstance; -use tokio::fs; -use uuid::Uuid; - -use crate::{ - constants::{ - SERVER_FILE_VF_META, SERVER_FILE_VF_VERSION_INSTANCE, SERVER_PATH_VF_ROOT, - SERVER_PATH_VF_STORAGE, SERVER_PATH_VF_TEMP, - }, - data::{member::MemberId, vault::Vault}, -}; - -pub type VirtualFileId = String; -pub type VirtualFileVersion = String; - -const VF_PREFIX: &str = "vf_"; -const ID_PARAM: &str = "{vf_id}"; -const ID_INDEX: &str = "{vf_index}"; -const VERSION_PARAM: &str = "{vf_version}"; -const TEMP_NAME: &str = "{temp_name}"; - -pub struct VirtualFile<'a> { - /// Unique identifier for the virtual file - id: VirtualFileId, - - /// Reference of Vault - current_vault: &'a Vault, -} - -#[derive(Default, Clone, Serialize, Deserialize, ConfigFile)] -pub struct VirtualFileMeta { - /// Current version of the virtual file - current_version: VirtualFileVersion, - - /// The member who holds the edit right of the file - hold_member: MemberId, - - /// Description of each version - version_description: HashMap, - - /// Histories - histories: Vec, -} - -#[derive(Default, Clone, Serialize, Deserialize)] -pub struct VirtualFileVersionDescription { - /// The member who created this version - pub creator: MemberId, - - /// The description of this version - pub description: String, -} - -impl VirtualFileVersionDescription { - /// Create a new version description - pub fn new(creator: MemberId, description: String) -> Self { - Self { - creator, - description, - } - } -} - -/// Virtual File Operations -impl Vault { - /// Generate a temporary path for receiving - pub fn virtual_file_temp_path(&self) -> PathBuf { - let random_receive_name = format!("{}", uuid::Uuid::new_v4()); - self.vault_path - .join(SERVER_PATH_VF_TEMP.replace(TEMP_NAME, &random_receive_name)) - } - - /// Get the directory where virtual files are stored - pub fn virtual_file_storage_dir(&self) -> PathBuf { - self.vault_path().join(SERVER_PATH_VF_ROOT) - } - - /// Get the directory where a specific virtual file is stored - pub fn virtual_file_dir(&self, id: &VirtualFileId) -> Result { - Ok(self.vault_path().join( - SERVER_PATH_VF_STORAGE - .replace(ID_PARAM, &id.to_string()) - .replace(ID_INDEX, &Self::vf_index(id)?), - )) - } - - // Generate index path of virtual file - fn vf_index(id: &VirtualFileId) -> Result { - // Remove VF_PREFIX if present - let id_str = if let Some(stripped) = id.strip_prefix(VF_PREFIX) { - stripped - } else { - id - }; - - // Extract the first part before the first hyphen - let first_part = id_str.split('-').next().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "Invalid virtual file ID format: no hyphen found", - ) - })?; - - // Ensure the first part has exactly 8 characters - if first_part.len() != 8 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "Invalid virtual file ID format: first part must be 8 characters", - ))?; - } - - // Split into 2-character chunks and join with path separator - let mut path = String::new(); - for i in (0..first_part.len()).step_by(2) { - if i > 0 { - path.push('/'); - } - path.push_str(&first_part[i..i + 2]); - } - - Ok(path) - } - - /// Get the directory where a specific virtual file's metadata is stored - pub fn virtual_file_real_path( - &self, - id: &VirtualFileId, - version: &VirtualFileVersion, - ) -> PathBuf { - self.vault_path().join( - SERVER_FILE_VF_VERSION_INSTANCE - .replace(ID_PARAM, &id.to_string()) - .replace(ID_INDEX, &Self::vf_index(id).unwrap_or_default()) - .replace(VERSION_PARAM, &version.to_string()), - ) - } - - /// Get the directory where a specific virtual file's metadata is stored - pub fn virtual_file_meta_path(&self, id: &VirtualFileId) -> PathBuf { - self.vault_path().join( - SERVER_FILE_VF_META - .replace(ID_PARAM, &id.to_string()) - .replace(ID_INDEX, &Self::vf_index(id).unwrap_or_default()), - ) - } - - /// Get the virtual file with the given ID - pub fn virtual_file(&self, id: &VirtualFileId) -> Result, std::io::Error> { - let dir = self.virtual_file_dir(id); - if dir?.exists() { - Ok(VirtualFile { - id: id.clone(), - current_vault: self, - }) - } else { - Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Cannot found virtual file!", - )) - } - } - - /// Get the meta data of the virtual file with the given ID - pub async fn virtual_file_meta( - &self, - id: &VirtualFileId, - ) -> Result { - let dir = self.virtual_file_meta_path(id); - let metadata = VirtualFileMeta::read_from(dir).await?; - Ok(metadata) - } - - /// Write the meta data of the virtual file with the given ID - pub async fn write_virtual_file_meta( - &self, - id: &VirtualFileId, - meta: &VirtualFileMeta, - ) -> Result<(), std::io::Error> { - let dir = self.virtual_file_meta_path(id); - VirtualFileMeta::write_to(meta, dir).await?; - Ok(()) - } - - /// Create a virtual file from a connection instance - /// - /// It's the only way to create virtual files! - /// - /// When the target machine executes `write_file`, use this function instead of `read_file`, - /// and provide the member ID of the transmitting member. - /// - /// The system will automatically receive the file and - /// create the virtual file. - pub async fn create_virtual_file_from_connection( - &self, - instance: &mut ConnectionInstance, - member_id: &MemberId, - ) -> Result { - const FIRST_VERSION: &str = "0"; - let receive_path = self.virtual_file_temp_path(); - let new_id = format!("{}{}", VF_PREFIX, Uuid::new_v4()); - let move_path = self.virtual_file_real_path(&new_id, &FIRST_VERSION.to_string()); - - match instance.read_file(receive_path.clone()).await { - Ok(_) => { - // Read successful, create virtual file - // Create default version description - let mut version_description = - HashMap::::new(); - version_description.insert( - FIRST_VERSION.to_string(), - VirtualFileVersionDescription { - creator: member_id.clone(), - description: "Track".to_string(), - }, - ); - // Create metadata - let mut meta = VirtualFileMeta { - current_version: FIRST_VERSION.to_string(), - hold_member: member_id.clone(), // The holder of the newly created virtual file is the creator by default - version_description, - histories: Vec::default(), - }; - - // Add first version - meta.histories.push(FIRST_VERSION.to_string()); - - // Write metadata to file - VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(&new_id)).await?; - - // Move temp file to virtual file directory - if let Some(parent) = move_path.parent() - && !parent.exists() - { - fs::create_dir_all(parent).await?; - } - fs::rename(receive_path, move_path).await?; - - // - - Ok(new_id) - } - Err(e) => { - // Read failed, remove temp file. - if receive_path.exists() { - fs::remove_file(receive_path).await?; - } - - Err(Error::other(e)) - } - } - } - - /// Update a virtual file from a connection instance - /// - /// It's the only way to update virtual files! - /// When the target machine executes `write_file`, use this function instead of `read_file`, - /// and provide the member ID of the transmitting member. - /// - /// The system will automatically receive the file and - /// update the virtual file. - /// - /// Note: The specified member must hold the edit right of the file, - /// otherwise the file reception will not be allowed. - /// - /// Make sure to obtain the edit right of the file before calling this function. - pub async fn update_virtual_file_from_connection( - &self, - instance: &mut ConnectionInstance, - member: &MemberId, - virtual_file_id: &VirtualFileId, - new_version: &VirtualFileVersion, - description: VirtualFileVersionDescription, - ) -> Result<(), std::io::Error> { - let new_version = snake_case!(new_version.clone()); - let mut meta = self.virtual_file_meta(virtual_file_id).await?; - - // Check if the member has edit right - self.check_virtual_file_edit_right(member, virtual_file_id) - .await?; - - // Check if the new version already exists - if meta.version_description.contains_key(&new_version) { - return Err(Error::new( - ErrorKind::AlreadyExists, - format!( - "Version `{}` already exists for virtual file `{}`", - new_version, virtual_file_id - ), - )); - } - - // Verify success - let receive_path = self.virtual_file_temp_path(); - let move_path = self.virtual_file_real_path(virtual_file_id, &new_version); - - match instance.read_file(receive_path.clone()).await { - Ok(_) => { - // Read success, move temp file to real path. - fs::rename(receive_path, move_path).await?; - - // Update metadata - meta.current_version = new_version.clone(); - meta.version_description - .insert(new_version.clone(), description); - meta.histories.push(new_version); - VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(virtual_file_id)) - .await?; - - Ok(()) - } - Err(e) => { - // Read failed, remove temp file. - if receive_path.exists() { - fs::remove_file(receive_path).await?; - } - - Err(Error::other(e)) - } - } - } - - /// Update virtual file from existing version - /// - /// This operation creates a new version based on the specified old version file instance. - /// The new version will retain the same version name as the old version, but use a different version number. - /// After the update, this version will be considered newer than the original version when comparing versions. - pub async fn update_virtual_file_from_exist_version( - &self, - member: &MemberId, - virtual_file_id: &VirtualFileId, - old_version: &VirtualFileVersion, - ) -> Result<(), std::io::Error> { - let old_version = snake_case!(old_version.clone()); - let mut meta = self.virtual_file_meta(virtual_file_id).await?; - - // Check if the member has edit right - self.check_virtual_file_edit_right(member, virtual_file_id) - .await?; - - // Ensure virtual file exist - let Ok(_) = self.virtual_file(virtual_file_id) else { - return Err(Error::new( - ErrorKind::NotFound, - format!("Virtual file `{}` not found!", virtual_file_id), - )); - }; - - // Ensure version exist - if !meta.version_exists(&old_version) { - return Err(Error::new( - ErrorKind::NotFound, - format!("Version `{}` not found!", old_version), - )); - } - - // Ok, Create new version - meta.current_version = old_version.clone(); - meta.histories.push(old_version); - VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(virtual_file_id)).await?; - - Ok(()) - } - - /// Grant a member the edit right for a virtual file - /// This operation takes effect immediately upon success - pub async fn grant_virtual_file_edit_right( - &self, - member_id: &MemberId, - virtual_file_id: &VirtualFileId, - ) -> Result<(), std::io::Error> { - let mut meta = self.virtual_file_meta(virtual_file_id).await?; - meta.hold_member = member_id.clone(); - self.write_virtual_file_meta(virtual_file_id, &meta).await - } - - /// Check if a member has the edit right for a virtual file - pub async fn has_virtual_file_edit_right( - &self, - member_id: &MemberId, - virtual_file_id: &VirtualFileId, - ) -> Result { - let meta = self.virtual_file_meta(virtual_file_id).await?; - Ok(meta.hold_member.eq(member_id)) - } - - /// Check if a member has the edit right for a virtual file and return Result - /// Returns Ok(()) if the member has edit right, otherwise returns PermissionDenied error - pub async fn check_virtual_file_edit_right( - &self, - member_id: &MemberId, - virtual_file_id: &VirtualFileId, - ) -> Result<(), std::io::Error> { - if !self - .has_virtual_file_edit_right(member_id, virtual_file_id) - .await? - { - return Err(Error::new( - ErrorKind::PermissionDenied, - format!( - "Member `{}` not allowed to update virtual file `{}`", - member_id, virtual_file_id - ), - )); - } - Ok(()) - } - - /// Revoke the edit right for a virtual file from the current holder - /// This operation takes effect immediately upon success - pub async fn revoke_virtual_file_edit_right( - &self, - virtual_file_id: &VirtualFileId, - ) -> Result<(), std::io::Error> { - let mut meta = self.virtual_file_meta(virtual_file_id).await?; - meta.hold_member = String::default(); - self.write_virtual_file_meta(virtual_file_id, &meta).await - } -} - -impl<'a> VirtualFile<'a> { - /// Get id of VirtualFile - pub fn id(&self) -> VirtualFileId { - self.id.clone() - } - - /// Read metadata of VirtualFile - pub async fn read_meta(&self) -> Result { - self.current_vault.virtual_file_meta(&self.id).await - } -} - -impl VirtualFileMeta { - /// Get all versions of the virtual file - pub fn versions(&self) -> &Vec { - &self.histories - } - - /// Get the total number of versions for this virtual file - pub fn version_len(&self) -> i32 { - self.histories.len() as i32 - } - - /// Check if a specific version exists - /// Returns true if the version exists, false otherwise - pub fn version_exists(&self, version: &VirtualFileVersion) -> bool { - self.versions().iter().any(|v| v == version) - } - - /// Get the version number (index) for a given version name - /// Returns None if the version doesn't exist - pub fn version_num(&self, version: &VirtualFileVersion) -> Option { - self.histories - .iter() - .rev() - .position(|v| v == version) - .map(|pos| (self.histories.len() - 1 - pos) as i32) - } - - /// Get the version name for a given version number (index) - /// Returns None if the version number is out of range - pub fn version_name(&self, version_num: i32) -> Option { - self.histories.get(version_num as usize).cloned() - } -} diff --git a/crates/vcs/src/lib.rs b/crates/vcs/src/lib.rs deleted file mode 100644 index 1b41391..0000000 --- a/crates/vcs/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod constants; -pub mod current; - -#[allow(dead_code)] -pub mod data; diff --git a/crates/vcs/todo.txt b/crates/vcs/todo.txt deleted file mode 100644 index 65c94ef..0000000 --- a/crates/vcs/todo.txt +++ /dev/null @@ -1,36 +0,0 @@ -本地文件操作 -设置上游服务器(仅设置,不会连接和修改染色标识) -验证连接、权限,并为当前工作区染色(若已染色,则无法连接不同标识的服务器) -进入表 (否则无法做任何操作) -退出表 (文件将会从当前目录移出,等待下次进入时还原) -去色 - 断开与上游服务器的关联 -跟踪本地文件的移动、重命名,立刻同步至表 -扫描本地文件结构,标记变化 -通过本地暂存的表索引搜索文件 -查询本地某个文件的状态 -查询当前目录的状态 -查询工作区状态 -将本地所有文件更新到最新状态 -提交所有产生变化的自身所属文件 - - -表操作(必须指定成员和表) -表查看 - 指定表并查看结构 -从参照表拉入文件项目 -将文件项目(或多个)导出到指定表 -查看导入请求 -在某个本地地址同意并导入文件 -拒绝某个、某些或所有导入请求 -删除表中的映射,但要确保实际文件已被移除 (忽略文件) -放弃表,所有者消失,下一个切换至表的人获得(放弃需要确保表中没有任何文件是所有者持有的)(替代目前的安全删除) - - -虚拟文件操作 -跟踪本地某些文件,并将其创建为虚拟文件,然后添加到自己的表 -根据本地文件的目录查找虚拟文件,并为自己获得所有权(需要确保版本和上游同步才可) -根据本地文件的目录查找虚拟文件,并放弃所有权(需要确保和上游同步才可) -根据本地文件的目录查找虚拟文件,并定向到指定的存在的老版本 - - -?为什么虚拟文件不能删除:虚拟文件的唯一删除方式就是,没有人再用他 -?为什么没有删除表:同理,表权限可以转移,但是删除只能等待定期清除无主人的表 diff --git a/crates/vcs/vcs_test/Cargo.toml b/crates/vcs/vcs_test/Cargo.toml deleted file mode 100644 index 1cc43ac..0000000 --- a/crates/vcs/vcs_test/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "vcs_test" -edition = "2024" -version.workspace = true - -[dependencies] -tcp_connection = { path = "../../utils/tcp_connection" } -tcp_connection_test = { path = "../../utils/tcp_connection/tcp_connection_test" } -cfg_file = { path = "../../utils/cfg_file", features = ["default"] } -vcs = { path = "../../vcs" } - -# Async & Networking -tokio = { version = "1.46.1", features = ["full"] } diff --git a/crates/vcs/vcs_test/lib.rs b/crates/vcs/vcs_test/lib.rs deleted file mode 100644 index 5b65941..0000000 --- a/crates/vcs/vcs_test/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -use vcs_service::{action::Action, action_pool::ActionPool}; - -use crate::actions::test::FindMemberInServer; - -pub mod constants; -pub mod current; - -#[allow(dead_code)] -pub mod data; - -pub mod actions; diff --git a/crates/vcs/vcs_test/src/lib.rs b/crates/vcs/vcs_test/src/lib.rs deleted file mode 100644 index 8ad03e1..0000000 --- a/crates/vcs/vcs_test/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::{env::current_dir, path::PathBuf}; - -use tokio::fs; - -#[cfg(test)] -pub mod test_vault_setup_and_member_register; - -#[cfg(test)] -pub mod test_virtual_file_creation_and_update; - -#[cfg(test)] -pub mod test_local_workspace_setup_and_account_management; - -#[cfg(test)] -pub mod test_sheet_creation_management_and_persistence; - -pub async fn get_test_dir(area: &str) -> Result { - let dir = current_dir()?.join(".temp").join("test").join(area); - if !dir.exists() { - std::fs::create_dir_all(&dir)?; - } else { - // Regenerate existing directory - fs::remove_dir_all(&dir).await?; - fs::create_dir_all(&dir).await?; - } - Ok(dir) -} diff --git a/crates/vcs/vcs_test/src/test_local_workspace_setup_and_account_management.rs b/crates/vcs/vcs_test/src/test_local_workspace_setup_and_account_management.rs deleted file mode 100644 index df766f7..0000000 --- a/crates/vcs/vcs_test/src/test_local_workspace_setup_and_account_management.rs +++ /dev/null @@ -1,248 +0,0 @@ -use std::io::Error; - -use cfg_file::config::ConfigFile; -use vcs::{ - constants::{CLIENT_FILE_README, CLIENT_FILE_WORKSPACE, USER_FILE_KEY, USER_FILE_MEMBER}, - data::{ - local::{LocalWorkspace, config::LocalConfig}, - member::Member, - user::UserDirectory, - }, -}; - -use crate::get_test_dir; - -#[tokio::test] -async fn test_local_workspace_setup_and_account_management() -> Result<(), std::io::Error> { - let dir = get_test_dir("local_workspace_account_management").await?; - - // Setup local workspace - LocalWorkspace::setup_local_workspace(dir.clone()).await?; - - // Check if the following files are created in `dir`: - // Files: CLIENT_FILE_WORKSPACE, CLIENT_FILE_README - assert!(dir.join(CLIENT_FILE_WORKSPACE).exists()); - assert!(dir.join(CLIENT_FILE_README).exists()); - - // Get local workspace - let config = LocalConfig::read_from(dir.join(CLIENT_FILE_WORKSPACE)).await?; - let Some(_local_workspace) = LocalWorkspace::init(config, &dir) else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "Local workspace not found!", - )); - }; - - // Create user directory from workspace path - let Some(user_directory) = UserDirectory::from_path(&dir) else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "User directory not found!", - )); - }; - - // Test account registration - let member_id = "test_account"; - let member = Member::new(member_id); - - // Register account - user_directory.register_account(member.clone()).await?; - - // Check if the account config file exists - assert!( - dir.join(USER_FILE_MEMBER.replace("{self_id}", member_id)) - .exists() - ); - - // Test account retrieval - let retrieved_member = user_directory.account(&member_id.to_string()).await?; - assert_eq!(retrieved_member.id(), member.id()); - - // Test account IDs listing - let account_ids = user_directory.account_ids()?; - assert!(account_ids.contains(&member_id.to_string())); - - // Test accounts listing - let accounts = user_directory.accounts().await?; - assert_eq!(accounts.len(), 1); - assert_eq!(accounts[0].id(), member.id()); - - // Test account existence check - assert!(user_directory.account_cfg(&member_id.to_string()).is_some()); - - // Test private key check (should be false initially) - assert!(!user_directory.has_private_key(&member_id.to_string())); - - // Test account update - let mut updated_member = member.clone(); - updated_member.set_metadata("email", "test@example.com"); - user_directory - .update_account(updated_member.clone()) - .await?; - - // Verify update - let updated_retrieved = user_directory.account(&member_id.to_string()).await?; - assert_eq!( - updated_retrieved.metadata("email"), - Some(&"test@example.com".to_string()) - ); - - // Test account removal - user_directory.remove_account(&member_id.to_string())?; - - // Check if the account config file no longer exists - assert!( - !dir.join(USER_FILE_MEMBER.replace("{self_id}", member_id)) - .exists() - ); - - // Check if account is no longer in the list - let account_ids_after_removal = user_directory.account_ids()?; - assert!(!account_ids_after_removal.contains(&member_id.to_string())); - - Ok(()) -} - -#[tokio::test] -async fn test_account_private_key_management() -> Result<(), std::io::Error> { - let dir = get_test_dir("account_private_key_management").await?; - - // Create user directory - let Some(user_directory) = UserDirectory::from_path(&dir) else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "User directory not found!", - )); - }; - - // Register account - let member_id = "test_account_with_key"; - let member = Member::new(member_id); - user_directory.register_account(member).await?; - - // Create a dummy private key file for testing - let private_key_path = dir.join(USER_FILE_KEY.replace("{self_id}", member_id)); - std::fs::create_dir_all(private_key_path.parent().unwrap())?; - std::fs::write(&private_key_path, "dummy_private_key_content")?; - - // Test private key existence check - assert!(user_directory.has_private_key(&member_id.to_string())); - - // Test private key path retrieval - assert!( - user_directory - .account_private_key(&member_id.to_string()) - .is_some() - ); - - // Remove account (should also remove private key) - user_directory.remove_account(&member_id.to_string())?; - - // Check if private key file is also removed - assert!(!private_key_path.exists()); - - Ok(()) -} - -#[tokio::test] -async fn test_multiple_account_management() -> Result<(), std::io::Error> { - let dir = get_test_dir("multiple_account_management").await?; - - // Create user directory - let Some(user_directory) = UserDirectory::from_path(&dir) else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "User directory not found!", - )); - }; - - // Register multiple accounts - let account_names = vec!["alice", "bob", "charlie"]; - - for name in &account_names { - user_directory.register_account(Member::new(*name)).await?; - } - - // Test account IDs listing - let account_ids = user_directory.account_ids()?; - assert_eq!(account_ids.len(), 3); - - for name in &account_names { - assert!(account_ids.contains(&name.to_string())); - } - - // Test accounts listing - let accounts = user_directory.accounts().await?; - assert_eq!(accounts.len(), 3); - - // Remove one account - user_directory.remove_account(&"bob".to_string())?; - - // Verify removal - let account_ids_after_removal = user_directory.account_ids()?; - assert_eq!(account_ids_after_removal.len(), 2); - assert!(!account_ids_after_removal.contains(&"bob".to_string())); - assert!(account_ids_after_removal.contains(&"alice".to_string())); - assert!(account_ids_after_removal.contains(&"charlie".to_string())); - - Ok(()) -} - -#[tokio::test] -async fn test_account_registration_duplicate_prevention() -> Result<(), std::io::Error> { - let dir = get_test_dir("account_duplicate_prevention").await?; - - // Create user directory - let Some(user_directory) = UserDirectory::from_path(&dir) else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "User directory not found!", - )); - }; - - // Register account - let member_id = "duplicate_test"; - user_directory - .register_account(Member::new(member_id)) - .await?; - - // Try to register same account again - should fail - let result = user_directory - .register_account(Member::new(member_id)) - .await; - assert!(result.is_err()); - - Ok(()) -} - -#[tokio::test] -async fn test_nonexistent_account_operations() -> Result<(), std::io::Error> { - let dir = get_test_dir("nonexistent_account_operations").await?; - - // Create user directory - let Some(user_directory) = UserDirectory::from_path(&dir) else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "User directory not found!", - )); - }; - - // Try to read non-existent account - should fail - let result = user_directory.account(&"nonexistent".to_string()).await; - assert!(result.is_err()); - - // Try to update non-existent account - should fail - let result = user_directory - .update_account(Member::new("nonexistent")) - .await; - assert!(result.is_err()); - - // Try to remove non-existent account - should succeed (idempotent) - let result = user_directory.remove_account(&"nonexistent".to_string()); - assert!(result.is_ok()); - - // Check private key for non-existent account - should be false - assert!(!user_directory.has_private_key(&"nonexistent".to_string())); - - Ok(()) -} diff --git a/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs b/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs deleted file mode 100644 index 3b038a0..0000000 --- a/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs +++ /dev/null @@ -1,307 +0,0 @@ -use std::io::Error; - -use cfg_file::config::ConfigFile; -use vcs::{ - constants::{SERVER_FILE_SHEET, SERVER_FILE_VAULT}, - data::{ - member::{Member, MemberId}, - sheet::{InputRelativePathBuf, SheetName}, - vault::{Vault, config::VaultConfig, virtual_file::VirtualFileId}, - }, -}; - -use crate::get_test_dir; - -#[tokio::test] -async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io::Error> { - let dir = get_test_dir("sheet_management").await?; - - // Setup vault - Vault::setup_vault(dir.clone()).await?; - - // Get vault - let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?; - let Some(vault) = Vault::init(config, &dir) else { - return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!")); - }; - - // Add a member to use as sheet holder - let member_id: MemberId = "test_member".to_string(); - vault - .register_member_to_vault(Member::new(&member_id)) - .await?; - - // Test 1: Create a new sheet - let sheet_name: SheetName = "test_sheet".to_string(); - let sheet = vault.create_sheet(&sheet_name, &member_id).await?; - - // Verify sheet properties - assert_eq!(sheet.holder(), &member_id); - assert_eq!(sheet.holder(), &member_id); - assert!(sheet.inputs().is_empty()); - assert!(sheet.mapping().is_empty()); - - // Verify sheet file was created - const SHEET_NAME_PARAM: &str = "{sheet-name}"; - let sheet_path = dir.join(SERVER_FILE_SHEET.replace(SHEET_NAME_PARAM, &sheet_name)); - assert!(sheet_path.exists()); - - // Test 2: Add input packages to the sheet - let input_name = "source_files".to_string(); - - // First add mapping entries that will be used to generate the input package - let mut sheet = vault.sheet(&sheet_name).await?; - - // Add mapping entries for the files - let main_rs_path = vcs::data::sheet::SheetPathBuf::from("src/main.rs"); - let lib_rs_path = vcs::data::sheet::SheetPathBuf::from("src/lib.rs"); - let main_rs_id = VirtualFileId::new(); - let lib_rs_id = VirtualFileId::new(); - - sheet - .add_mapping(main_rs_path.clone(), main_rs_id.clone()) - .await?; - sheet - .add_mapping(lib_rs_path.clone(), lib_rs_id.clone()) - .await?; - - // Use output_mappings to generate the InputPackage - let paths = vec![main_rs_path, lib_rs_path]; - let input_package = sheet.output_mappings(input_name.clone(), &paths)?; - sheet.add_input(input_package)?; - - // Verify input was added - assert_eq!(sheet.inputs().len(), 1); - let added_input = &sheet.inputs()[0]; - assert_eq!(added_input.name, input_name); - assert_eq!(added_input.files.len(), 2); - assert_eq!( - added_input.files[0].0, - InputRelativePathBuf::from("source_files/main.rs") - ); - assert_eq!( - added_input.files[1].0, - InputRelativePathBuf::from("source_files/lib.rs") - ); - - // Test 3: Add mapping entries - let mapping_path = vcs::data::sheet::SheetPathBuf::from("output/build.exe"); - let virtual_file_id = VirtualFileId::new(); - - sheet - .add_mapping(mapping_path.clone(), virtual_file_id.clone()) - .await?; - - // Verify mapping was added - assert_eq!(sheet.mapping().len(), 3); - assert_eq!(sheet.mapping().get(&mapping_path), Some(&virtual_file_id)); - - // Test 4: Persist sheet to disk - sheet.persist().await?; - - // Verify persistence by reloading the sheet - let reloaded_sheet = vault.sheet(&sheet_name).await?; - assert_eq!(reloaded_sheet.holder(), &member_id); - assert_eq!(reloaded_sheet.inputs().len(), 1); - assert_eq!(reloaded_sheet.mapping().len(), 3); - - // Test 5: Remove input package - let mut sheet_for_removal = vault.sheet(&sheet_name).await?; - let removed_input = sheet_for_removal.deny_input(&input_name); - assert!(removed_input.is_some()); - let removed_input = removed_input.unwrap(); - assert_eq!(removed_input.name, input_name); - assert_eq!(removed_input.files.len(), 2); - assert_eq!(sheet_for_removal.inputs().len(), 0); - - // Test 6: Remove mapping entry - let _removed_virtual_file_id = sheet_for_removal.remove_mapping(&mapping_path).await; - // Don't check the return value since it depends on virtual file existence - assert_eq!(sheet_for_removal.mapping().len(), 2); - - // Test 7: List all sheets in vault - let sheet_names = vault.sheet_names()?; - assert_eq!(sheet_names.len(), 2); - assert!(sheet_names.contains(&sheet_name)); - assert!(sheet_names.contains(&"ref".to_string())); - - let all_sheets = vault.sheets().await?; - assert_eq!(all_sheets.len(), 2); - // One sheet should be the test sheet, the other should be the ref sheet with host as holder - let test_sheet_holder = all_sheets - .iter() - .find(|s| s.holder() == &member_id) - .map(|s| s.holder()) - .unwrap(); - let ref_sheet_holder = all_sheets - .iter() - .find(|s| s.holder() == &"host".to_string()) - .map(|s| s.holder()) - .unwrap(); - assert_eq!(test_sheet_holder, &member_id); - assert_eq!(ref_sheet_holder, &"host".to_string()); - - // Test 8: Safe deletion (move to trash) - vault.delete_sheet_safely(&sheet_name).await?; - - // Verify sheet is not in normal listing but can be restored - let sheet_names_after_deletion = vault.sheet_names()?; - assert_eq!(sheet_names_after_deletion.len(), 1); - assert_eq!(sheet_names_after_deletion[0], "ref"); - - // Test 9: Restore sheet from trash - let restored_sheet = vault.sheet(&sheet_name).await?; - assert_eq!(restored_sheet.holder(), &member_id); - assert_eq!(restored_sheet.holder(), &member_id); - - // Verify sheet is back in normal listing - let sheet_names_after_restore = vault.sheet_names()?; - assert_eq!(sheet_names_after_restore.len(), 2); - assert!(sheet_names_after_restore.contains(&sheet_name)); - assert!(sheet_names_after_restore.contains(&"ref".to_string())); - - // Test 10: Permanent deletion - vault.delete_sheet(&sheet_name).await?; - - // Verify sheet is permanently gone - let sheet_names_final = vault.sheet_names()?; - assert_eq!(sheet_names_final.len(), 1); - assert_eq!(sheet_names_final[0], "ref"); - - // Attempt to access deleted sheet should fail - let result = vault.sheet(&sheet_name).await; - assert!(result.is_err()); - - // Clean up: Remove member - vault.remove_member_from_vault(&member_id)?; - - Ok(()) -} - -#[tokio::test] -async fn test_sheet_error_conditions() -> Result<(), std::io::Error> { - let dir = get_test_dir("sheet_error_conditions").await?; - - // Setup vault - Vault::setup_vault(dir.clone()).await?; - - // Get vault - let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?; - let Some(vault) = Vault::init(config, &dir) else { - return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!")); - }; - - // Test 1: Create sheet with non-existent member should fail - let non_existent_member: MemberId = "non_existent_member".to_string(); - let sheet_name: SheetName = "test_sheet".to_string(); - - let result = vault.create_sheet(&sheet_name, &non_existent_member).await; - assert!(result.is_err()); - - // Add a member first - let member_id: MemberId = "test_member".to_string(); - vault - .register_member_to_vault(Member::new(&member_id)) - .await?; - - // Test 2: Create duplicate sheet should fail - vault.create_sheet(&sheet_name, &member_id).await?; - let result = vault.create_sheet(&sheet_name, &member_id).await; - assert!(result.is_err()); - - // Test 3: Delete non-existent sheet should fail - let non_existent_sheet: SheetName = "non_existent_sheet".to_string(); - let result = vault.delete_sheet(&non_existent_sheet).await; - assert!(result.is_err()); - - // Test 4: Safe delete non-existent sheet should fail - let result = vault.delete_sheet_safely(&non_existent_sheet).await; - assert!(result.is_err()); - - // Test 5: Restore non-existent sheet from trash should fail - let result = vault.restore_sheet(&non_existent_sheet).await; - assert!(result.is_err()); - - // Clean up - vault.remove_member_from_vault(&member_id)?; - - Ok(()) -} - -#[tokio::test] -async fn test_sheet_data_serialization() -> Result<(), std::io::Error> { - let dir = get_test_dir("sheet_serialization").await?; - - // Test serialization by creating a sheet through the vault - // Setup vault - Vault::setup_vault(dir.clone()).await?; - - // Get vault - let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?; - let Some(vault) = Vault::init(config, &dir) else { - return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!")); - }; - - // Add a member - let member_id: MemberId = "test_member".to_string(); - vault - .register_member_to_vault(Member::new(&member_id)) - .await?; - - // Create a sheet - let sheet_name: SheetName = "test_serialization_sheet".to_string(); - let mut sheet = vault.create_sheet(&sheet_name, &member_id).await?; - - // Add some inputs - let input_name = "source_files".to_string(); - let _files = vec![ - ( - InputRelativePathBuf::from("src/main.rs"), - VirtualFileId::new(), - ), - ( - InputRelativePathBuf::from("src/lib.rs"), - VirtualFileId::new(), - ), - ]; - // First add mapping entries - let main_rs_path = vcs::data::sheet::SheetPathBuf::from("src/main.rs"); - let lib_rs_path = vcs::data::sheet::SheetPathBuf::from("src/lib.rs"); - let main_rs_id = VirtualFileId::new(); - let lib_rs_id = VirtualFileId::new(); - - sheet - .add_mapping(main_rs_path.clone(), main_rs_id.clone()) - .await?; - sheet - .add_mapping(lib_rs_path.clone(), lib_rs_id.clone()) - .await?; - - // Use output_mappings to generate the InputPackage - let paths = vec![main_rs_path, lib_rs_path]; - let input_package = sheet.output_mappings(input_name.clone(), &paths)?; - sheet.add_input(input_package)?; - - // Add some mappings - let build_exe_id = VirtualFileId::new(); - - sheet - .add_mapping( - vcs::data::sheet::SheetPathBuf::from("output/build.exe"), - build_exe_id, - ) - .await?; - - // Persist the sheet - sheet.persist().await?; - - // Verify the sheet file was created - const SHEET_NAME_PARAM: &str = "{sheet-name}"; - let sheet_path = dir.join(SERVER_FILE_SHEET.replace(SHEET_NAME_PARAM, &sheet_name)); - assert!(sheet_path.exists()); - - // Clean up - vault.remove_member_from_vault(&member_id)?; - - Ok(()) -} diff --git a/crates/vcs/vcs_test/src/test_vault_setup_and_member_register.rs b/crates/vcs/vcs_test/src/test_vault_setup_and_member_register.rs deleted file mode 100644 index 6a30cf7..0000000 --- a/crates/vcs/vcs_test/src/test_vault_setup_and_member_register.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::io::Error; - -use cfg_file::config::ConfigFile; -use vcs::{ - constants::{ - SERVER_FILE_MEMBER_INFO, SERVER_FILE_README, SERVER_FILE_VAULT, SERVER_PATH_MEMBER_PUB, - SERVER_PATH_MEMBERS, SERVER_PATH_SHEETS, SERVER_PATH_VF_ROOT, - }, - data::{ - member::Member, - vault::{Vault, config::VaultConfig}, - }, -}; - -use crate::get_test_dir; - -#[tokio::test] -async fn test_vault_setup_and_member_register() -> Result<(), std::io::Error> { - let dir = get_test_dir("member_register").await?; - - // Setup vault - Vault::setup_vault(dir.clone()).await?; - - // Check if the following files and directories are created in `dir`: - // Files: SERVER_FILE_VAULT, SERVER_FILE_README - // Directories: SERVER_PATH_SHEETS, - // SERVER_PATH_MEMBERS, - // SERVER_PATH_MEMBER_PUB, - // SERVER_PATH_VIRTUAL_FILE_ROOT - assert!(dir.join(SERVER_FILE_VAULT).exists()); - assert!(dir.join(SERVER_FILE_README).exists()); - assert!(dir.join(SERVER_PATH_SHEETS).exists()); - assert!(dir.join(SERVER_PATH_MEMBERS).exists()); - assert!(dir.join(SERVER_PATH_MEMBER_PUB).exists()); - assert!(dir.join(SERVER_PATH_VF_ROOT).exists()); - - // Get vault - let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?; - let Some(vault) = Vault::init(config, &dir) else { - return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!")); - }; - - // Add member - let member_id = "test_member"; - vault - .register_member_to_vault(Member::new(member_id)) - .await?; - - const ID_PARAM: &str = "{member_id}"; - - // Check if the member info file exists - assert!( - dir.join(SERVER_FILE_MEMBER_INFO.replace(ID_PARAM, member_id)) - .exists() - ); - - // Remove member - vault.remove_member_from_vault(&member_id.to_string())?; - - // Check if the member info file not exists - assert!( - !dir.join(SERVER_FILE_MEMBER_INFO.replace(ID_PARAM, member_id)) - .exists() - ); - - Ok(()) -} diff --git a/crates/vcs/vcs_test/src/test_virtual_file_creation_and_update.rs b/crates/vcs/vcs_test/src/test_virtual_file_creation_and_update.rs deleted file mode 100644 index d86c13a..0000000 --- a/crates/vcs/vcs_test/src/test_virtual_file_creation_and_update.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::time::Duration; - -use cfg_file::config::ConfigFile; -use tcp_connection_test::{ - handle::{ClientHandle, ServerHandle}, - target::TcpServerTarget, - target_configure::ServerTargetConfig, -}; -use tokio::{ - join, - time::{sleep, timeout}, -}; -use vcs::{ - constants::SERVER_FILE_VAULT, - data::{ - member::Member, - vault::{Vault, config::VaultConfig, virtual_file::VirtualFileVersionDescription}, - }, -}; - -use crate::get_test_dir; - -struct VirtualFileCreateClientHandle; -struct VirtualFileCreateServerHandle; - -impl ClientHandle for VirtualFileCreateClientHandle { - async fn process(mut instance: tcp_connection::instance::ConnectionInstance) { - let dir = get_test_dir("virtual_file_creation_and_update_2") - .await - .unwrap(); - // Create first test file for virtual file creation - let test_content_1 = b"Test file content for virtual file creation"; - let temp_file_path_1 = dir.join("test_virtual_file_1.txt"); - - tokio::fs::write(&temp_file_path_1, test_content_1) - .await - .unwrap(); - - // Send the first file to server for virtual file creation - instance.write_file(&temp_file_path_1).await.unwrap(); - - // Create second test file for virtual file update - let test_content_2 = b"Updated test file content for virtual file"; - let temp_file_path_2 = dir.join("test_virtual_file_2.txt"); - - tokio::fs::write(&temp_file_path_2, test_content_2) - .await - .unwrap(); - - // Send the second file to server for virtual file update - instance.write_file(&temp_file_path_2).await.unwrap(); - } -} - -impl ServerHandle for VirtualFileCreateServerHandle { - async fn process(mut instance: tcp_connection::instance::ConnectionInstance) { - let dir = get_test_dir("virtual_file_creation_and_update") - .await - .unwrap(); - - // Setup vault - Vault::setup_vault(dir.clone()).await.unwrap(); - - // Read vault - let Some(vault) = Vault::init( - VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)) - .await - .unwrap(), - &dir, - ) else { - panic!("No vault found!"); - }; - - // Register member - let member_id = "test_member"; - vault - .register_member_to_vault(Member::new(member_id)) - .await - .unwrap(); - - // Create visual file - let virtual_file_id = vault - .create_virtual_file_from_connection(&mut instance, &member_id.to_string()) - .await - .unwrap(); - - // Grant edit right to member - vault - .grant_virtual_file_edit_right(&member_id.to_string(), &virtual_file_id) - .await - .unwrap(); - - // Update visual file - vault - .update_virtual_file_from_connection( - &mut instance, - &member_id.to_string(), - &virtual_file_id, - &"2".to_string(), - VirtualFileVersionDescription { - creator: member_id.to_string(), - description: "Update".to_string(), - }, - ) - .await - .unwrap(); - } -} - -#[tokio::test] -async fn test_virtual_file_creation_and_update() -> Result<(), std::io::Error> { - let host = "localhost:5009"; - - // Server setup - let Ok(server_target) = TcpServerTarget::< - VirtualFileCreateClientHandle, - VirtualFileCreateServerHandle, - >::from_domain(host) - .await - else { - panic!("Test target built failed from a domain named `{}`", host); - }; - - // Client setup - let Ok(client_target) = TcpServerTarget::< - VirtualFileCreateClientHandle, - VirtualFileCreateServerHandle, - >::from_domain(host) - .await - else { - panic!("Test target built failed from a domain named `{}`", host); - }; - - let future_server = async move { - // Only process once - let configured_server = server_target.server_cfg(ServerTargetConfig::default().once()); - - // Listen here - let _ = configured_server.listen().await; - }; - - let future_client = async move { - // Wait for server start - let _ = sleep(Duration::from_secs_f32(1.5)).await; - - // Connect here - let _ = client_target.connect().await; - }; - - let test_timeout = Duration::from_secs(15); - - timeout(test_timeout, async { join!(future_client, future_server) }) - .await - .map_err(|_| { - std::io::Error::new( - std::io::ErrorKind::TimedOut, - format!("Test timed out after {:?}", test_timeout), - ) - })?; - - Ok(()) -} -- cgit From 8ef9f06df67d3ad2d1d1a9038ee93e26ec615489 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 6 Oct 2025 04:12:07 +0800 Subject: Update workspace configuration for new crate structure - Replace vcs with vcs_data and vcs_actions in workspace members - Update Cargo.lock dependencies to reflect new crate names - Maintain workspace structure with reorganized crates --- Cargo.lock | 20 ++++++++++++++++---- Cargo.toml | 9 ++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1a26a9..7ed2554 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -541,7 +541,8 @@ dependencies = [ "cfg_file", "string_proc", "tcp_connection", - "vcs", + "vcs_actions", + "vcs_data", ] [[package]] @@ -1370,7 +1371,18 @@ dependencies = [ ] [[package]] -name = "vcs" +name = "vcs_actions" +version = "0.1.0" +dependencies = [ + "action_system", + "cfg_file", + "string_proc", + "tcp_connection", + "vcs_data", +] + +[[package]] +name = "vcs_data" version = "0.1.0" dependencies = [ "action_system", @@ -1384,14 +1396,14 @@ dependencies = [ ] [[package]] -name = "vcs_test" +name = "vcs_data_test" version = "0.1.0" dependencies = [ "cfg_file", "tcp_connection", "tcp_connection_test", "tokio", - "vcs", + "vcs_data", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7c32aaf..72f3270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,10 @@ members = [ "crates/system_action", "crates/system_action/action_macros", - "crates/vcs", - "crates/vcs/vcs_test", + "crates/vcs_data", + "crates/vcs_data/vcs_data_test", + + "crates/vcs_actions", ] [workspace.package] @@ -56,4 +58,5 @@ cfg_file = { path = "crates/utils/cfg_file" } tcp_connection = { path = "crates/utils/tcp_connection" } string_proc = { path = "crates/utils/string_proc" } -vcs = { path = "crates/vcs" } +vcs_data = { path = "crates/vcs_data" } +vcs_actions = { path = "crates/vcs_actions" } -- cgit From 85f7c35d6c573b715c166fe7501225ecab6731ea Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 6 Oct 2025 04:12:15 +0800 Subject: Update main lib.rs for new crate architecture - Remove old vcs module exports - Prepare for new vcs_data and vcs_actions integration - Update library structure to reflect architectural changes --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 3a6c652..746f66f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,9 @@ pub mod vcs { extern crate vcs; pub use vcs::*; + + extern crate vcs_actions; + pub use vcs_actions::*; } pub mod utils { -- cgit