summaryrefslogtreecommitdiff
path: root/crates/vcs_data
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2025-10-06 04:11:34 +0800
committer魏曹先生 <1992414357@qq.com>2025-10-06 04:11:34 +0800
commit87c3ec3fdcbd2294c3b9258d28ff47959e6eff68 (patch)
treea601cb7e7d97917e4a79ae6db3698c8ecd31717c /crates/vcs_data
parentce4545a21d435d63827fb972406e749354ac687a (diff)
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
Diffstat (limited to 'crates/vcs_data')
-rw-r--r--crates/vcs_data/Cargo.toml22
-rw-r--r--crates/vcs_data/src/constants.rs54
-rw-r--r--crates/vcs_data/src/current.rs78
-rw-r--r--crates/vcs_data/src/data.rs5
-rw-r--r--crates/vcs_data/src/data/local.rs100
-rw-r--r--crates/vcs_data/src/data/local/config.rs53
-rw-r--r--crates/vcs_data/src/data/member.rs69
-rw-r--r--crates/vcs_data/src/data/sheet.rs347
-rw-r--r--crates/vcs_data/src/data/user.rs28
-rw-r--r--crates/vcs_data/src/data/user/accounts.rs164
-rw-r--r--crates/vcs_data/src/data/vault.rs146
-rw-r--r--crates/vcs_data/src/data/vault/config.rs77
-rw-r--r--crates/vcs_data/src/data/vault/member.rs140
-rw-r--r--crates/vcs_data/src/data/vault/sheets.rs268
-rw-r--r--crates/vcs_data/src/data/vault/virtual_file.rs473
-rw-r--r--crates/vcs_data/src/lib.rs5
-rw-r--r--crates/vcs_data/todo.txt36
-rw-r--r--crates/vcs_data/vcs_data_test/Cargo.toml13
-rw-r--r--crates/vcs_data/vcs_data_test/lib.rs11
-rw-r--r--crates/vcs_data/vcs_data_test/src/lib.rs27
-rw-r--r--crates/vcs_data/vcs_data_test/src/test_local_workspace_setup_and_account_management.rs248
-rw-r--r--crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs307
-rw-r--r--crates/vcs_data/vcs_data_test/src/test_vault_setup_and_member_register.rs67
-rw-r--r--crates/vcs_data/vcs_data_test/src/test_virtual_file_creation_and_update.rs162
24 files changed, 2900 insertions, 0 deletions
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<PathBuf> {
+ 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<PathBuf> {
+ 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<PathBuf>) -> Option<PathBuf> {
+ 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<PathBuf>) -> Option<PathBuf> {
+ 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<PathBuf> {
+ 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<PathBuf>) -> Option<Self> {
+ 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<Self> {
+ let local_path = current_local_path()?;
+ Some(Self { config, local_path })
+ }
+
+ /// Setup local workspace
+ pub async fn setup_local_workspace(
+ local_path: impl Into<PathBuf>,
+ ) -> 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<String, String>,
+}
+
+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<str> for Member {
+ fn as_ref(&self) -> &str {
+ &self.id
+ }
+}
+
+impl Member {
+ /// Create member struct by id
+ pub fn new(new_id: impl Into<String>) -> 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<String>) -> Option<&String> {
+ self.metadata.get(&key.into())
+ }
+
+ /// Set metadata
+ pub fn set_metadata(
+ &mut self,
+ key: impl AsRef<str>,
+ value: impl Into<String>,
+ ) -> Option<String> {
+ 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<InputPackage>,
+
+ /// Mapping of sheet paths to virtual file IDs
+ pub(crate) mapping: HashMap<SheetPathBuf, VirtualFileId>,
+}
+
+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<InputPackage> {
+ &self.data.inputs
+ }
+
+ /// Get the names of the inputs of this sheet
+ pub fn input_names(&self) -> Vec<String> {
+ self.data
+ .inputs
+ .iter()
+ .map(|input| input.name.clone())
+ .collect()
+ }
+
+ /// Get the mapping of this sheet
+ pub fn mapping(&self) -> &HashMap<SheetPathBuf, VirtualFileId> {
+ &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<InputPackage> {
+ 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<VirtualFileId> {
+ 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<str>) -> 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<InputPackage, std::io::Error> {
+ 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::<Result<Vec<_>, _>>()?;
+
+ 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<Self> {
+ 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<P: Into<PathBuf>>(path: P) -> Option<Self> {
+ 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<Member, std::io::Error> {
+ 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<Vec<MemberId>, 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<Vec<Member>, 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<PathBuf> {
+ 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<PathBuf> {
+ 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<PathBuf>) -> Option<Self> {
+ let vault_path = find_vault_path(vault_path)?;
+ Some(Self { config, vault_path })
+ }
+
+ /// Initialize vault
+ pub fn init_current_dir(config: VaultConfig) -> Option<Self> {
+ let vault_path = current_vault_path()?;
+ Some(Self { config, vault_path })
+ }
+
+ /// Setup vault
+ pub async fn setup_vault(vault_path: impl Into<PathBuf>) -> 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<MemberId>,
+
+ /// 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<String>) {
+ 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<Member, std::io::Error> {
+ 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<Vec<MemberId>, 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<Vec<Member>, 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<PathBuf> {
+ 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<PathBuf> {
+ 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<Vec<Sheet<'a>>, 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<Vec<SheetName>, 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<Sheet<'a>, 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<Sheet<'a>, 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<VirtualFileVersion, VirtualFileVersionDescription>,
+
+ /// Histories
+ histories: Vec<VirtualFileVersion>,
+}
+
+#[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<PathBuf, std::io::Error> {
+ 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<String, std::io::Error> {
+ // 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<VirtualFile<'_>, 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<VirtualFileMeta, std::io::Error> {
+ 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<VirtualFileId, std::io::Error> {
+ 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::<VirtualFileVersion, VirtualFileVersionDescription>::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<bool, std::io::Error> {
+ 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<VirtualFileMeta, std::io::Error> {
+ self.current_vault.virtual_file_meta(&self.id).await
+ }
+}
+
+impl VirtualFileMeta {
+ /// Get all versions of the virtual file
+ pub fn versions(&self) -> &Vec<VirtualFileVersion> {
+ &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<i32> {
+ 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<VirtualFileVersion> {
+ 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<PathBuf, std::io::Error> {
+ 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<VirtualFileCreateServerHandle> 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<VirtualFileCreateClientHandle> 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(())
+}