diff options
| author | 魏曹先生 <1992414357@qq.com> | 2025-10-06 04:11:58 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2025-10-06 04:11:58 +0800 |
| commit | ee7cba690582b9c47e8c856bf0bd331eedda7908 (patch) | |
| tree | 21218393cd14b4465296b15ad999fc89be97757c /crates/vcs/src | |
| parent | 03a5ff8a1629cde933120faf47963fcb59118261 (diff) | |
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
Diffstat (limited to 'crates/vcs/src')
| -rw-r--r-- | crates/vcs/src/constants.rs | 54 | ||||
| -rw-r--r-- | crates/vcs/src/current.rs | 78 | ||||
| -rw-r--r-- | crates/vcs/src/data.rs | 5 | ||||
| -rw-r--r-- | crates/vcs/src/data/local.rs | 100 | ||||
| -rw-r--r-- | crates/vcs/src/data/local/config.rs | 53 | ||||
| -rw-r--r-- | crates/vcs/src/data/member.rs | 69 | ||||
| -rw-r--r-- | crates/vcs/src/data/sheet.rs | 347 | ||||
| -rw-r--r-- | crates/vcs/src/data/user.rs | 28 | ||||
| -rw-r--r-- | crates/vcs/src/data/user/accounts.rs | 164 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault.rs | 146 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault/config.rs | 77 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault/member.rs | 140 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault/sheets.rs | 268 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault/virtual_file.rs | 473 | ||||
| -rw-r--r-- | crates/vcs/src/lib.rs | 5 |
15 files changed, 0 insertions, 2007 deletions
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<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/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<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/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<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/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<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/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<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/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<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/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<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/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<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, - - /// 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<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/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<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/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<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/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<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/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; |
