From bac88ab54b28825a17f808def90209517f484355 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 12 Jan 2026 06:01:52 +0800 Subject: Rename modules for better clarity and organization --- actions/src/connection/action_service.rs | 2 +- actions/src/registry/client_registry.rs | 2 +- actions/src/remote_actions.rs | 2 +- actions/src/remote_actions/content_manage/track.rs | 4 +- .../change_virtual_file_edit_right.rs | 2 +- .../remote_actions/mapping_manage/edit_mapping.rs | 2 +- .../mapping_manage/merge_share_mapping.rs | 4 +- .../src/remote_actions/sheet_manage/drop_sheet.rs | 2 +- .../src/remote_actions/sheet_manage/make_sheet.rs | 2 +- .../workspace_manage/set_upstream_vault.rs | 2 +- .../workspace_manage/update_to_latest_info.rs | 4 +- data/src/current.rs | 84 ---- data/src/data/local.rs | 10 +- data/src/data/local/align.rs | 110 ------ data/src/data/local/align_tasks.rs | 110 ++++++ data/src/data/local/cached_sheet.rs | 2 +- data/src/data/local/config.rs | 374 ------------------ data/src/data/local/latest_file_data.rs | 2 +- data/src/data/local/latest_info.rs | 2 +- data/src/data/local/modified_status.rs | 30 ++ data/src/data/local/vault_modified.rs | 30 -- data/src/data/local/workspace_config.rs | 374 ++++++++++++++++++ data/src/data/user.rs | 2 +- data/src/data/vault.rs | 14 +- data/src/data/vault/config.rs | 233 ------------ data/src/data/vault/lock_status.rs | 40 ++ data/src/data/vault/mapping_share.rs | 422 +++++++++++++++++++++ data/src/data/vault/member.rs | 144 ------- data/src/data/vault/member_manage.rs | 144 +++++++ data/src/data/vault/service.rs | 40 -- data/src/data/vault/sheet_manage.rs | 274 +++++++++++++ data/src/data/vault/sheet_share.rs | 422 --------------------- data/src/data/vault/sheets.rs | 274 ------------- data/src/data/vault/vault_config.rs | 233 ++++++++++++ data/src/env.rs | 84 ++++ data/src/lib.rs | 2 +- ...local_workspace_setup_and_account_management.rs | 2 +- ...st_sheet_creation_management_and_persistence.rs | 2 +- .../test_sheet_share_creation_and_management.rs | 4 +- .../src/test_vault_setup_and_member_register.rs | 2 +- .../src/test_virtual_file_creation_and_update.rs | 2 +- 41 files changed, 1748 insertions(+), 1748 deletions(-) delete mode 100644 data/src/current.rs delete mode 100644 data/src/data/local/align.rs create mode 100644 data/src/data/local/align_tasks.rs delete mode 100644 data/src/data/local/config.rs create mode 100644 data/src/data/local/modified_status.rs delete mode 100644 data/src/data/local/vault_modified.rs create mode 100644 data/src/data/local/workspace_config.rs delete mode 100644 data/src/data/vault/config.rs create mode 100644 data/src/data/vault/lock_status.rs create mode 100644 data/src/data/vault/mapping_share.rs delete mode 100644 data/src/data/vault/member.rs create mode 100644 data/src/data/vault/member_manage.rs delete mode 100644 data/src/data/vault/service.rs create mode 100644 data/src/data/vault/sheet_manage.rs delete mode 100644 data/src/data/vault/sheet_share.rs delete mode 100644 data/src/data/vault/sheets.rs create mode 100644 data/src/data/vault/vault_config.rs create mode 100644 data/src/env.rs diff --git a/actions/src/connection/action_service.rs b/actions/src/connection/action_service.rs index f137126..3e05a70 100644 --- a/actions/src/connection/action_service.rs +++ b/actions/src/connection/action_service.rs @@ -15,7 +15,7 @@ use tokio::{ select, signal, spawn, sync::mpsc, }; -use vcs_data::data::vault::{Vault, config::VaultConfig}; +use vcs_data::data::vault::{Vault, vault_config::VaultConfig}; use crate::{ connection::protocol::RemoteActionInvoke, registry::server_registry::server_action_pool, diff --git a/actions/src/registry/client_registry.rs b/actions/src/registry/client_registry.rs index c31b19a..a1bccae 100644 --- a/actions/src/registry/client_registry.rs +++ b/actions/src/registry/client_registry.rs @@ -4,7 +4,7 @@ use action_system::{action::ActionContext, action_pool::ActionPool}; use cfg_file::config::ConfigFile; use tcp_connection::error::TcpTargetError; use vcs_data::data::{ - local::{LocalWorkspace, config::LocalConfig}, + local::{LocalWorkspace, workspace_config::LocalConfig}, user::UserDirectory, }; diff --git a/actions/src/remote_actions.rs b/actions/src/remote_actions.rs index 33f01aa..d15edc9 100644 --- a/actions/src/remote_actions.rs +++ b/actions/src/remote_actions.rs @@ -7,7 +7,7 @@ use tokio::sync::{Mutex, mpsc::Sender}; use vcs_data::{ constants::{SERVER_PATH_MEMBER_PUB, VAULT_HOST_NAME}, data::{ - local::{LocalWorkspace, config::LocalConfig, latest_info::LatestInfo}, + local::{LocalWorkspace, latest_info::LatestInfo, workspace_config::LocalConfig}, member::MemberId, sheet::SheetName, user::UserDirectory, diff --git a/actions/src/remote_actions/content_manage/track.rs b/actions/src/remote_actions/content_manage/track.rs index c158700..a59ca76 100644 --- a/actions/src/remote_actions/content_manage/track.rs +++ b/actions/src/remote_actions/content_manage/track.rs @@ -16,13 +16,13 @@ use vcs_data::{ data::{ local::{ cached_sheet::CachedSheet, latest_file_data::LatestFileData, - local_sheet::LocalMappingMetadata, vault_modified::sign_vault_modified, + local_sheet::LocalMappingMetadata, modified_status::sign_vault_modified, workspace_analyzer::AnalyzeResult, }, member::MemberId, sheet::SheetName, vault::{ - config::VaultUuid, + vault_config::VaultUuid, virtual_file::{VirtualFileId, VirtualFileVersion, VirtualFileVersionDescription}, }, }, diff --git a/actions/src/remote_actions/edit_right_manage/change_virtual_file_edit_right.rs b/actions/src/remote_actions/edit_right_manage/change_virtual_file_edit_right.rs index b999e42..1045bce 100644 --- a/actions/src/remote_actions/edit_right_manage/change_virtual_file_edit_right.rs +++ b/actions/src/remote_actions/edit_right_manage/change_virtual_file_edit_right.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use action_system::{action::ActionContext, macros::action_gen}; use serde::{Deserialize, Serialize}; use tcp_connection::error::TcpTargetError; -use vcs_data::data::local::vault_modified::sign_vault_modified; +use vcs_data::data::local::modified_status::sign_vault_modified; use crate::remote_actions::{ auth_member, check_connection_instance, get_current_sheet_name, try_get_vault, diff --git a/actions/src/remote_actions/mapping_manage/edit_mapping.rs b/actions/src/remote_actions/mapping_manage/edit_mapping.rs index b66f568..3c39c5d 100644 --- a/actions/src/remote_actions/mapping_manage/edit_mapping.rs +++ b/actions/src/remote_actions/mapping_manage/edit_mapping.rs @@ -4,7 +4,7 @@ use action_system::{action::ActionContext, macros::action_gen}; use serde::{Deserialize, Serialize}; use tcp_connection::error::TcpTargetError; use vcs_data::data::local::{ - vault_modified::sign_vault_modified, + modified_status::sign_vault_modified, workspace_analyzer::{FromRelativePathBuf, ToRelativePathBuf}, }; diff --git a/actions/src/remote_actions/mapping_manage/merge_share_mapping.rs b/actions/src/remote_actions/mapping_manage/merge_share_mapping.rs index 1034029..df889a1 100644 --- a/actions/src/remote_actions/mapping_manage/merge_share_mapping.rs +++ b/actions/src/remote_actions/mapping_manage/merge_share_mapping.rs @@ -4,8 +4,8 @@ use action_system::{action::ActionContext, macros::action_gen}; use serde::{Deserialize, Serialize}; use tcp_connection::error::TcpTargetError; use vcs_data::data::{ - local::vault_modified::sign_vault_modified, - vault::sheet_share::{ShareMergeMode, SheetShareId}, + local::modified_status::sign_vault_modified, + vault::mapping_share::{ShareMergeMode, SheetShareId}, }; use crate::{ diff --git a/actions/src/remote_actions/sheet_manage/drop_sheet.rs b/actions/src/remote_actions/sheet_manage/drop_sheet.rs index 0dcd790..e21f3dd 100644 --- a/actions/src/remote_actions/sheet_manage/drop_sheet.rs +++ b/actions/src/remote_actions/sheet_manage/drop_sheet.rs @@ -3,7 +3,7 @@ use std::io::ErrorKind; use action_system::{action::ActionContext, macros::action_gen}; use serde::{Deserialize, Serialize}; use tcp_connection::error::TcpTargetError; -use vcs_data::data::{local::vault_modified::sign_vault_modified, sheet::SheetName}; +use vcs_data::data::{local::modified_status::sign_vault_modified, sheet::SheetName}; use crate::{ remote_actions::{ diff --git a/actions/src/remote_actions/sheet_manage/make_sheet.rs b/actions/src/remote_actions/sheet_manage/make_sheet.rs index ac5e140..a323413 100644 --- a/actions/src/remote_actions/sheet_manage/make_sheet.rs +++ b/actions/src/remote_actions/sheet_manage/make_sheet.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use tcp_connection::error::TcpTargetError; use vcs_data::{ constants::VAULT_HOST_NAME, - data::{local::vault_modified::sign_vault_modified, sheet::SheetName}, + data::{local::modified_status::sign_vault_modified, sheet::SheetName}, }; use crate::{ diff --git a/actions/src/remote_actions/workspace_manage/set_upstream_vault.rs b/actions/src/remote_actions/workspace_manage/set_upstream_vault.rs index d3201b2..ba45214 100644 --- a/actions/src/remote_actions/workspace_manage/set_upstream_vault.rs +++ b/actions/src/remote_actions/workspace_manage/set_upstream_vault.rs @@ -5,7 +5,7 @@ use cfg_file::config::ConfigFile; use log::info; use serde::{Deserialize, Serialize}; use tcp_connection::error::TcpTargetError; -use vcs_data::data::{local::config::LocalConfig, vault::config::VaultUuid}; +use vcs_data::data::{local::workspace_config::LocalConfig, vault::vault_config::VaultUuid}; use crate::remote_actions::{ auth_member, check_connection_instance, try_get_local_workspace, try_get_vault, diff --git a/actions/src/remote_actions/workspace_manage/update_to_latest_info.rs b/actions/src/remote_actions/workspace_manage/update_to_latest_info.rs index 695d1f0..cd17c32 100644 --- a/actions/src/remote_actions/workspace_manage/update_to_latest_info.rs +++ b/actions/src/remote_actions/workspace_manage/update_to_latest_info.rs @@ -20,12 +20,12 @@ use vcs_data::{ cached_sheet::CachedSheet, latest_file_data::LatestFileData, latest_info::{LatestInfo, SheetInfo}, - vault_modified::sign_vault_modified, + modified_status::sign_vault_modified, }, member::MemberId, sheet::{SheetData, SheetName, SheetPathBuf}, vault::{ - sheet_share::{Share, SheetShareId}, + mapping_share::{Share, SheetShareId}, virtual_file::{VirtualFileId, VirtualFileVersion, VirtualFileVersionDescription}, }, }, diff --git a/data/src/current.rs b/data/src/current.rs deleted file mode 100644 index 209c0cc..0000000 --- a/data/src/current.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::constants::*; -use std::io::{self, Error}; -use std::{env::set_current_dir, path::PathBuf}; - -/// Find the nearest vault or local workspace and correct the `current_dir` to it -pub fn correct_current_dir() -> Result<(), io::Error> { - if let Some(local_workspace) = current_local_path() { - set_current_dir(local_workspace)?; - return Ok(()); - } - if let Some(vault) = current_vault_path() { - set_current_dir(vault)?; - return Ok(()); - } - Err(Error::new( - io::ErrorKind::NotFound, - "Could not find any vault or local workspace!", - )) -} - -/// Get the nearest Vault directory from `current_dir` -pub fn current_vault_path() -> Option { - let current_dir = std::env::current_dir().ok()?; - find_vault_path(current_dir) -} - -/// Get the nearest local workspace from `current_dir` -pub fn current_local_path() -> Option { - let current_dir = std::env::current_dir().ok()?; - find_local_path(current_dir) -} - -/// Get the nearest Vault directory from the specified path -pub fn find_vault_path(path: impl Into) -> Option { - let mut current_path = path.into(); - let vault_file = SERVER_FILE_VAULT; - - loop { - let vault_toml_path = current_path.join(vault_file); - if vault_toml_path.exists() { - return Some(current_path); - } - - if let Some(parent) = current_path.parent() { - current_path = parent.to_path_buf(); - } else { - break; - } - } - - None -} - -/// Get the nearest local workspace from the specified path -pub fn find_local_path(path: impl Into) -> Option { - let mut current_path = path.into(); - let workspace_dir = CLIENT_PATH_WORKSPACE_ROOT; - - loop { - let jvc_path = current_path.join(workspace_dir); - if jvc_path.exists() { - return Some(current_path); - } - - if let Some(parent) = current_path.parent() { - current_path = parent.to_path_buf(); - } else { - break; - } - } - - None -} - -/// Get the system's document directory and join with the appropriate application name -pub fn current_cfg_dir() -> Option { - dirs::config_local_dir().map(|path| { - if cfg!(target_os = "linux") { - path.join("jvcs") - } else { - path.join("JustEnoughVCS") - } - }) -} diff --git a/data/src/data/local.rs b/data/src/data/local.rs index 0927e3f..d4115c6 100644 --- a/data/src/data/local.rs +++ b/data/src/data/local.rs @@ -16,26 +16,26 @@ use crate::{ CLIENT_FILE_TODOLIST, CLIENT_FILE_WORKSPACE, CLIENT_FOLDER_WORKSPACE_ROOT_NAME, CLIENT_PATH_LOCAL_SHEET, CLIENT_SUFFIX_LOCAL_SHEET_FILE, KEY_ACCOUNT, KEY_SHEET_NAME, }, - current::{current_local_path, find_local_path}, data::{ local::{ - config::LocalConfig, local_sheet::{LocalSheet, LocalSheetData, LocalSheetPathBuf}, + workspace_config::LocalConfig, }, member::MemberId, sheet::SheetName, }, + env::{current_local_path, find_local_path}, }; -pub mod align; +pub mod align_tasks; pub mod cached_sheet; -pub mod config; pub mod latest_file_data; pub mod latest_info; pub mod local_files; pub mod local_sheet; -pub mod vault_modified; +pub mod modified_status; pub mod workspace_analyzer; +pub mod workspace_config; pub struct LocalWorkspace { config: Arc>, diff --git a/data/src/data/local/align.rs b/data/src/data/local/align.rs deleted file mode 100644 index b72804c..0000000 --- a/data/src/data/local/align.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - path::PathBuf, -}; - -use data_struct::data_sort::quick_sort_with_cmp; - -use crate::data::local::workspace_analyzer::AnalyzeResult; - -pub type AlignTaskName = String; -pub type AlignPathBuf = PathBuf; -pub type AlignLostPathBuf = PathBuf; -pub type AlignCreatedPathBuf = PathBuf; - -pub struct AlignTasks { - pub created: Vec<(AlignTaskName, AlignPathBuf)>, - pub lost: Vec<(AlignTaskName, AlignPathBuf)>, - pub moved: Vec<(AlignTaskName, (AlignLostPathBuf, AlignCreatedPathBuf))>, - pub erased: Vec<(AlignTaskName, AlignPathBuf)>, -} - -impl AlignTasks { - pub fn clone_from_analyze_result(result: &AnalyzeResult) -> Self { - AlignTasks { - created: path_hash_set_sort_helper(result.created.clone(), "created"), - lost: path_hash_set_sort_helper(result.lost.clone(), "lost"), - moved: path_hash_map_sort_helper(result.moved.clone(), "moved"), - erased: path_hash_set_sort_helper(result.erased.clone(), "erased"), - } - } - - pub fn from_analyze_result(result: AnalyzeResult) -> Self { - AlignTasks { - created: path_hash_set_sort_helper(result.created, "created"), - lost: path_hash_set_sort_helper(result.lost, "lost"), - moved: path_hash_map_sort_helper(result.moved, "moved"), - erased: path_hash_set_sort_helper(result.erased, "erased"), - } - } -} - -fn path_hash_set_sort_helper( - hash_set: HashSet, - prefix: impl Into, -) -> Vec<(String, PathBuf)> { - let prefix_str = prefix.into(); - let mut vec: Vec<(String, PathBuf)> = hash_set - .into_iter() - .map(|path| { - let hash = sha1_hash::calc_sha1_string(path.to_string_lossy()); - let hash_prefix: String = hash.chars().take(8).collect(); - let name = format!("{}:{}", prefix_str, hash_prefix); - (name, path) - }) - .collect(); - - quick_sort_with_cmp(&mut vec, false, |a, b| { - // Compare by path depth first - let a_depth = a.1.components().count(); - let b_depth = b.1.components().count(); - - if a_depth != b_depth { - return if a_depth < b_depth { -1 } else { 1 }; - } - - // If same depth, compare lexicographically - match a.1.cmp(&b.1) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - } - }); - - vec -} - -fn path_hash_map_sort_helper( - hash_map: HashMap, - prefix: impl Into, -) -> Vec<(String, (PathBuf, PathBuf))> { - let prefix_str = prefix.into(); - let mut vec: Vec<(String, (PathBuf, PathBuf))> = hash_map - .into_values() - .map(|(path1, path2)| { - let hash = sha1_hash::calc_sha1_string(path1.to_string_lossy()); - let hash_prefix: String = hash.chars().take(8).collect(); - let name = format!("{}:{}", prefix_str, hash_prefix); - (name, (path1, path2)) - }) - .collect(); - - quick_sort_with_cmp(&mut vec, false, |a, b| { - // Compare by first PathBuf's path depth first - let a_depth = a.1.0.components().count(); - let b_depth = b.1.0.components().count(); - - if a_depth != b_depth { - return if a_depth < b_depth { -1 } else { 1 }; - } - - // If same depth, compare lexicographically by first PathBuf - match a.1.0.cmp(&b.1.0) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - } - }); - - vec -} diff --git a/data/src/data/local/align_tasks.rs b/data/src/data/local/align_tasks.rs new file mode 100644 index 0000000..b72804c --- /dev/null +++ b/data/src/data/local/align_tasks.rs @@ -0,0 +1,110 @@ +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; + +use data_struct::data_sort::quick_sort_with_cmp; + +use crate::data::local::workspace_analyzer::AnalyzeResult; + +pub type AlignTaskName = String; +pub type AlignPathBuf = PathBuf; +pub type AlignLostPathBuf = PathBuf; +pub type AlignCreatedPathBuf = PathBuf; + +pub struct AlignTasks { + pub created: Vec<(AlignTaskName, AlignPathBuf)>, + pub lost: Vec<(AlignTaskName, AlignPathBuf)>, + pub moved: Vec<(AlignTaskName, (AlignLostPathBuf, AlignCreatedPathBuf))>, + pub erased: Vec<(AlignTaskName, AlignPathBuf)>, +} + +impl AlignTasks { + pub fn clone_from_analyze_result(result: &AnalyzeResult) -> Self { + AlignTasks { + created: path_hash_set_sort_helper(result.created.clone(), "created"), + lost: path_hash_set_sort_helper(result.lost.clone(), "lost"), + moved: path_hash_map_sort_helper(result.moved.clone(), "moved"), + erased: path_hash_set_sort_helper(result.erased.clone(), "erased"), + } + } + + pub fn from_analyze_result(result: AnalyzeResult) -> Self { + AlignTasks { + created: path_hash_set_sort_helper(result.created, "created"), + lost: path_hash_set_sort_helper(result.lost, "lost"), + moved: path_hash_map_sort_helper(result.moved, "moved"), + erased: path_hash_set_sort_helper(result.erased, "erased"), + } + } +} + +fn path_hash_set_sort_helper( + hash_set: HashSet, + prefix: impl Into, +) -> Vec<(String, PathBuf)> { + let prefix_str = prefix.into(); + let mut vec: Vec<(String, PathBuf)> = hash_set + .into_iter() + .map(|path| { + let hash = sha1_hash::calc_sha1_string(path.to_string_lossy()); + let hash_prefix: String = hash.chars().take(8).collect(); + let name = format!("{}:{}", prefix_str, hash_prefix); + (name, path) + }) + .collect(); + + quick_sort_with_cmp(&mut vec, false, |a, b| { + // Compare by path depth first + let a_depth = a.1.components().count(); + let b_depth = b.1.components().count(); + + if a_depth != b_depth { + return if a_depth < b_depth { -1 } else { 1 }; + } + + // If same depth, compare lexicographically + match a.1.cmp(&b.1) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + } + }); + + vec +} + +fn path_hash_map_sort_helper( + hash_map: HashMap, + prefix: impl Into, +) -> Vec<(String, (PathBuf, PathBuf))> { + let prefix_str = prefix.into(); + let mut vec: Vec<(String, (PathBuf, PathBuf))> = hash_map + .into_values() + .map(|(path1, path2)| { + let hash = sha1_hash::calc_sha1_string(path1.to_string_lossy()); + let hash_prefix: String = hash.chars().take(8).collect(); + let name = format!("{}:{}", prefix_str, hash_prefix); + (name, (path1, path2)) + }) + .collect(); + + quick_sort_with_cmp(&mut vec, false, |a, b| { + // Compare by first PathBuf's path depth first + let a_depth = a.1.0.components().count(); + let b_depth = b.1.0.components().count(); + + if a_depth != b_depth { + return if a_depth < b_depth { -1 } else { 1 }; + } + + // If same depth, compare lexicographically by first PathBuf + match a.1.0.cmp(&b.1.0) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + } + }); + + vec +} diff --git a/data/src/data/local/cached_sheet.rs b/data/src/data/local/cached_sheet.rs index 8394006..46b390f 100644 --- a/data/src/data/local/cached_sheet.rs +++ b/data/src/data/local/cached_sheet.rs @@ -9,8 +9,8 @@ use crate::{ CLIENT_FILE_CACHED_SHEET, CLIENT_PATH_CACHED_SHEET, CLIENT_SUFFIX_CACHED_SHEET_FILE, KEY_SHEET_NAME, }, - current::current_local_path, data::sheet::{SheetData, SheetName}, + env::current_local_path, }; pub type CachedSheetPathBuf = PathBuf; diff --git a/data/src/data/local/config.rs b/data/src/data/local/config.rs deleted file mode 100644 index 08c25ff..0000000 --- a/data/src/data/local/config.rs +++ /dev/null @@ -1,374 +0,0 @@ -use cfg_file::ConfigFile; -use cfg_file::config::ConfigFile; -use serde::{Deserialize, Serialize}; -use std::io::Error; -use std::net::SocketAddr; -use std::path::Path; -use std::path::PathBuf; -use string_proc::snake_case; - -use crate::constants::CLIENT_FILE_WORKSPACE; -use crate::constants::CLIENT_FOLDER_WORKSPACE_ROOT_NAME; -use crate::constants::CLIENT_PATH_LOCAL_DRAFT; -use crate::constants::CLIENT_PATH_WORKSPACE_ROOT; -use crate::constants::KEY_ACCOUNT; -use crate::constants::KEY_SHEET_NAME; -use crate::constants::PORT; -use crate::current::current_local_path; -use crate::data::local::latest_info::LatestInfo; -use crate::data::member::MemberId; -use crate::data::sheet::SheetName; -use crate::data::vault::config::VaultUuid; - -#[derive(Serialize, Deserialize, ConfigFile, Clone)] -#[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. - #[serde(rename = "addr")] - 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. - #[serde(rename = "as")] - using_account: MemberId, - - /// Whether the current member is interacting as a host. - /// In host mode, full Vault operation permissions are available except for adding new content. - #[serde(rename = "host")] - using_host_mode: bool, - - /// Whether the local workspace is stained. - /// - /// If stained, it can only set an upstream server with the same identifier. - /// - /// If the value is None, it means not stained; - /// otherwise, it contains the stain identifier (i.e., the upstream vault's unique ID) - #[serde(rename = "up_uid")] - stained_uuid: Option, - - /// The name of the sheet currently in use. - #[serde(rename = "use")] - sheet_in_use: Option, -} - -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(), - using_host_mode: false, - stained_uuid: None, - sheet_in_use: None, - } - } -} - -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) -> Result<(), std::io::Error> { - if self.sheet_in_use().is_some() { - return Err(Error::new( - std::io::ErrorKind::DirectoryNotEmpty, - "Please exit the current sheet before switching accounts", - )); - } - self.using_account = account; - Ok(()) - } - - /// Set the host mode - pub fn set_host_mode(&mut self, host_mode: bool) { - self.using_host_mode = host_mode; - } - - /// Set the currently used sheet - pub async fn use_sheet(&mut self, sheet: SheetName) -> Result<(), std::io::Error> { - let sheet = snake_case!(sheet); - - // Check if the sheet is already in use - if self.sheet_in_use().is_some() { - return Err(std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - "Sheet already in use", - )); - }; - - // Check if the local path exists - let local_path = self.get_local_path().await?; - - // Get latest info - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_path, - &self.current_account(), - )) - .await - else { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "No latest info found", - )); - }; - - // Check if the sheet exists - if !latest_info.visible_sheets.contains(&sheet) { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Sheet not found", - )); - } - - // Check if there are any files or folders other than .jv - self.check_local_path_empty(&local_path).await?; - - // Get the draft folder path - let draft_folder = self.draft_folder(&self.using_account, &sheet, &local_path); - - if draft_folder.exists() { - // Exists - // Move the contents of the draft folder to the local path with rollback support - self.move_draft_to_local(&draft_folder, &local_path).await?; - } - - self.sheet_in_use = Some(sheet); - LocalConfig::write(self).await?; - - Ok(()) - } - - /// Exit the currently used sheet - pub async fn exit_sheet(&mut self) -> Result<(), std::io::Error> { - // Check if the sheet is already in use - if self.sheet_in_use().is_none() { - return Ok(()); - } - - // Check if the local path exists - let local_path = self.get_local_path().await?; - - // Get the current sheet name - let sheet_name = self.sheet_in_use().as_ref().unwrap().clone(); - - // Get the draft folder path - let draft_folder = self.draft_folder(&self.using_account, &sheet_name, &local_path); - - // Create the draft folder if it doesn't exist - if !draft_folder.exists() { - std::fs::create_dir_all(&draft_folder).map_err(std::io::Error::other)?; - } - - // Move all files and folders (except .jv folder) to the draft folder with rollback support - self.move_local_to_draft(&local_path, &draft_folder).await?; - - // Clear the sheet in use - self.sheet_in_use = None; - LocalConfig::write(self).await?; - - Ok(()) - } - - /// Get local path or return error - async fn get_local_path(&self) -> Result { - current_local_path().ok_or_else(|| { - std::io::Error::new(std::io::ErrorKind::NotFound, "Fail to get local path") - }) - } - - /// Check if local path is empty (except for .jv folder) - async fn check_local_path_empty(&self, local_path: &Path) -> Result<(), std::io::Error> { - let jv_folder = local_path.join(CLIENT_PATH_WORKSPACE_ROOT); - let mut entries = std::fs::read_dir(local_path).map_err(std::io::Error::other)?; - - if entries.any(|entry| { - if let Ok(entry) = entry { - let path = entry.path(); - path != jv_folder - && path.file_name().and_then(|s| s.to_str()) - != Some(CLIENT_FOLDER_WORKSPACE_ROOT_NAME) - } else { - false - } - }) { - return Err(std::io::Error::new( - std::io::ErrorKind::DirectoryNotEmpty, - "Local path is not empty!", - )); - } - - Ok(()) - } - - /// Move contents from draft folder to local path with rollback support - async fn move_draft_to_local( - &self, - draft_folder: &Path, - local_path: &Path, - ) -> Result<(), std::io::Error> { - let draft_entries: Vec<_> = std::fs::read_dir(draft_folder) - .map_err(std::io::Error::other)? - .collect::, _>>() - .map_err(std::io::Error::other)?; - - let mut moved_items: Vec = Vec::new(); - - for entry in &draft_entries { - let entry_path = entry.path(); - let target_path = local_path.join(entry_path.file_name().unwrap()); - - // Move each file/directory from draft folder to local path - std::fs::rename(&entry_path, &target_path).map_err(|e| { - // Rollback all previously moved items - for moved_item in &moved_items { - let _ = std::fs::rename(&moved_item.target, &moved_item.source); - } - std::io::Error::other(e) - })?; - - moved_items.push(MovedItem { - source: entry_path.clone(), - target: target_path.clone(), - }); - } - - // Remove the now-empty draft folder - std::fs::remove_dir(draft_folder).map_err(|e| { - // Rollback all moved items if folder removal fails - for moved_item in &moved_items { - let _ = std::fs::rename(&moved_item.target, &moved_item.source); - } - std::io::Error::other(e) - })?; - - Ok(()) - } - - /// Move contents from local path to draft folder with rollback support (except .jv folder) - async fn move_local_to_draft( - &self, - local_path: &Path, - draft_folder: &Path, - ) -> Result<(), std::io::Error> { - let jv_folder = local_path.join(CLIENT_PATH_WORKSPACE_ROOT); - let entries: Vec<_> = std::fs::read_dir(local_path) - .map_err(std::io::Error::other)? - .collect::, _>>() - .map_err(std::io::Error::other)?; - - let mut moved_items: Vec = Vec::new(); - - for entry in &entries { - let entry_path = entry.path(); - - // Skip the .jv folder - if entry_path == jv_folder - || entry_path.file_name().and_then(|s| s.to_str()) - == Some(CLIENT_FOLDER_WORKSPACE_ROOT_NAME) - { - continue; - } - - let target_path = draft_folder.join(entry_path.file_name().unwrap()); - - // Move each file/directory from local path to draft folder - std::fs::rename(&entry_path, &target_path).map_err(|e| { - // Rollback all previously moved items - for moved_item in &moved_items { - let _ = std::fs::rename(&moved_item.target, &moved_item.source); - } - std::io::Error::other(e) - })?; - - moved_items.push(MovedItem { - source: entry_path.clone(), - target: target_path.clone(), - }); - } - - Ok(()) - } - - /// Get the currently used account - pub fn current_account(&self) -> MemberId { - self.using_account.clone() - } - - /// Check if the current member is interacting as a host. - pub fn is_host_mode(&self) -> bool { - self.using_host_mode - } - - /// Check if the local workspace is stained. - pub fn stained(&self) -> bool { - self.stained_uuid.is_some() - } - - /// Get the UUID of the vault that the local workspace is stained with. - pub fn stained_uuid(&self) -> Option { - self.stained_uuid - } - - /// Stain the local workspace with the given UUID. - pub fn stain(&mut self, uuid: VaultUuid) { - self.stained_uuid = Some(uuid); - } - - /// Unstain the local workspace. - pub fn unstain(&mut self) { - self.stained_uuid = None; - } - - /// Get the upstream address. - pub fn upstream_addr(&self) -> SocketAddr { - self.upstream_addr - } - - /// Get the currently used sheet - pub fn sheet_in_use(&self) -> &Option { - &self.sheet_in_use - } - - /// Get draft folder - pub fn draft_folder( - &self, - account: &MemberId, - sheet_name: &SheetName, - local_workspace_path: impl Into, - ) -> PathBuf { - let account_str = snake_case!(account.as_str()); - let sheet_name_str = snake_case!(sheet_name.as_str()); - let draft_path = CLIENT_PATH_LOCAL_DRAFT - .replace(KEY_ACCOUNT, &account_str) - .replace(KEY_SHEET_NAME, &sheet_name_str); - local_workspace_path.into().join(draft_path) - } - - /// Get current draft folder - pub fn current_draft_folder(&self) -> Option { - let Some(sheet_name) = self.sheet_in_use() else { - return None; - }; - - let current_dir = current_local_path()?; - - Some(self.draft_folder(&self.using_account, sheet_name, current_dir)) - } -} - -#[derive(Clone)] -struct MovedItem { - source: PathBuf, - target: PathBuf, -} diff --git a/data/src/data/local/latest_file_data.rs b/data/src/data/local/latest_file_data.rs index 95de015..f9b3aeb 100644 --- a/data/src/data/local/latest_file_data.rs +++ b/data/src/data/local/latest_file_data.rs @@ -5,11 +5,11 @@ use serde::{Deserialize, Serialize}; use crate::{ constants::{CLIENT_FILE_LATEST_DATA, CLIENT_FILE_MEMBER_HELD_NOSET, KEY_ACCOUNT}, - current::current_local_path, data::{ member::MemberId, vault::virtual_file::{VirtualFileId, VirtualFileVersion, VirtualFileVersionDescription}, }, + env::current_local_path, }; /// # Latest file data diff --git a/data/src/data/local/latest_info.rs b/data/src/data/local/latest_info.rs index ec1e482..5748793 100644 --- a/data/src/data/local/latest_info.rs +++ b/data/src/data/local/latest_info.rs @@ -13,7 +13,7 @@ use crate::{ member::{Member, MemberId}, sheet::{SheetData, SheetName, SheetPathBuf}, vault::{ - sheet_share::{Share, SheetShareId}, + mapping_share::{Share, SheetShareId}, virtual_file::VirtualFileId, }, }, diff --git a/data/src/data/local/modified_status.rs b/data/src/data/local/modified_status.rs new file mode 100644 index 0000000..e0e6dd5 --- /dev/null +++ b/data/src/data/local/modified_status.rs @@ -0,0 +1,30 @@ +use crate::{constants::CLIENT_FILE_VAULT_MODIFIED, env::current_local_path}; + +pub async fn check_vault_modified() -> bool { + let Some(current_dir) = current_local_path() else { + return false; + }; + + let record_file = current_dir.join(CLIENT_FILE_VAULT_MODIFIED); + if !record_file.exists() { + return false; + } + + let Ok(contents) = tokio::fs::read_to_string(&record_file).await else { + return false; + }; + + matches!(contents.trim().to_lowercase().as_str(), "true") +} + +pub async fn sign_vault_modified(modified: bool) { + let Some(current_dir) = current_local_path() else { + return; + }; + + let record_file = current_dir.join(CLIENT_FILE_VAULT_MODIFIED); + + let contents = if modified { "true" } else { "false" }; + + let _ = tokio::fs::write(&record_file, contents).await; +} diff --git a/data/src/data/local/vault_modified.rs b/data/src/data/local/vault_modified.rs deleted file mode 100644 index 563d11f..0000000 --- a/data/src/data/local/vault_modified.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{constants::CLIENT_FILE_VAULT_MODIFIED, current::current_local_path}; - -pub async fn check_vault_modified() -> bool { - let Some(current_dir) = current_local_path() else { - return false; - }; - - let record_file = current_dir.join(CLIENT_FILE_VAULT_MODIFIED); - if !record_file.exists() { - return false; - } - - let Ok(contents) = tokio::fs::read_to_string(&record_file).await else { - return false; - }; - - matches!(contents.trim().to_lowercase().as_str(), "true") -} - -pub async fn sign_vault_modified(modified: bool) { - let Some(current_dir) = current_local_path() else { - return; - }; - - let record_file = current_dir.join(CLIENT_FILE_VAULT_MODIFIED); - - let contents = if modified { "true" } else { "false" }; - - let _ = tokio::fs::write(&record_file, contents).await; -} diff --git a/data/src/data/local/workspace_config.rs b/data/src/data/local/workspace_config.rs new file mode 100644 index 0000000..f97d049 --- /dev/null +++ b/data/src/data/local/workspace_config.rs @@ -0,0 +1,374 @@ +use cfg_file::ConfigFile; +use cfg_file::config::ConfigFile; +use serde::{Deserialize, Serialize}; +use std::io::Error; +use std::net::SocketAddr; +use std::path::Path; +use std::path::PathBuf; +use string_proc::snake_case; + +use crate::constants::CLIENT_FILE_WORKSPACE; +use crate::constants::CLIENT_FOLDER_WORKSPACE_ROOT_NAME; +use crate::constants::CLIENT_PATH_LOCAL_DRAFT; +use crate::constants::CLIENT_PATH_WORKSPACE_ROOT; +use crate::constants::KEY_ACCOUNT; +use crate::constants::KEY_SHEET_NAME; +use crate::constants::PORT; +use crate::data::local::latest_info::LatestInfo; +use crate::data::member::MemberId; +use crate::data::sheet::SheetName; +use crate::data::vault::vault_config::VaultUuid; +use crate::env::current_local_path; + +#[derive(Serialize, Deserialize, ConfigFile, Clone)] +#[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. + #[serde(rename = "addr")] + 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. + #[serde(rename = "as")] + using_account: MemberId, + + /// Whether the current member is interacting as a host. + /// In host mode, full Vault operation permissions are available except for adding new content. + #[serde(rename = "host")] + using_host_mode: bool, + + /// Whether the local workspace is stained. + /// + /// If stained, it can only set an upstream server with the same identifier. + /// + /// If the value is None, it means not stained; + /// otherwise, it contains the stain identifier (i.e., the upstream vault's unique ID) + #[serde(rename = "up_uid")] + stained_uuid: Option, + + /// The name of the sheet currently in use. + #[serde(rename = "use")] + sheet_in_use: Option, +} + +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(), + using_host_mode: false, + stained_uuid: None, + sheet_in_use: None, + } + } +} + +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) -> Result<(), std::io::Error> { + if self.sheet_in_use().is_some() { + return Err(Error::new( + std::io::ErrorKind::DirectoryNotEmpty, + "Please exit the current sheet before switching accounts", + )); + } + self.using_account = account; + Ok(()) + } + + /// Set the host mode + pub fn set_host_mode(&mut self, host_mode: bool) { + self.using_host_mode = host_mode; + } + + /// Set the currently used sheet + pub async fn use_sheet(&mut self, sheet: SheetName) -> Result<(), std::io::Error> { + let sheet = snake_case!(sheet); + + // Check if the sheet is already in use + if self.sheet_in_use().is_some() { + return Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + "Sheet already in use", + )); + }; + + // Check if the local path exists + let local_path = self.get_local_path().await?; + + // Get latest info + let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( + &local_path, + &self.current_account(), + )) + .await + else { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No latest info found", + )); + }; + + // Check if the sheet exists + if !latest_info.visible_sheets.contains(&sheet) { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Sheet not found", + )); + } + + // Check if there are any files or folders other than .jv + self.check_local_path_empty(&local_path).await?; + + // Get the draft folder path + let draft_folder = self.draft_folder(&self.using_account, &sheet, &local_path); + + if draft_folder.exists() { + // Exists + // Move the contents of the draft folder to the local path with rollback support + self.move_draft_to_local(&draft_folder, &local_path).await?; + } + + self.sheet_in_use = Some(sheet); + LocalConfig::write(self).await?; + + Ok(()) + } + + /// Exit the currently used sheet + pub async fn exit_sheet(&mut self) -> Result<(), std::io::Error> { + // Check if the sheet is already in use + if self.sheet_in_use().is_none() { + return Ok(()); + } + + // Check if the local path exists + let local_path = self.get_local_path().await?; + + // Get the current sheet name + let sheet_name = self.sheet_in_use().as_ref().unwrap().clone(); + + // Get the draft folder path + let draft_folder = self.draft_folder(&self.using_account, &sheet_name, &local_path); + + // Create the draft folder if it doesn't exist + if !draft_folder.exists() { + std::fs::create_dir_all(&draft_folder).map_err(std::io::Error::other)?; + } + + // Move all files and folders (except .jv folder) to the draft folder with rollback support + self.move_local_to_draft(&local_path, &draft_folder).await?; + + // Clear the sheet in use + self.sheet_in_use = None; + LocalConfig::write(self).await?; + + Ok(()) + } + + /// Get local path or return error + async fn get_local_path(&self) -> Result { + current_local_path().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "Fail to get local path") + }) + } + + /// Check if local path is empty (except for .jv folder) + async fn check_local_path_empty(&self, local_path: &Path) -> Result<(), std::io::Error> { + let jv_folder = local_path.join(CLIENT_PATH_WORKSPACE_ROOT); + let mut entries = std::fs::read_dir(local_path).map_err(std::io::Error::other)?; + + if entries.any(|entry| { + if let Ok(entry) = entry { + let path = entry.path(); + path != jv_folder + && path.file_name().and_then(|s| s.to_str()) + != Some(CLIENT_FOLDER_WORKSPACE_ROOT_NAME) + } else { + false + } + }) { + return Err(std::io::Error::new( + std::io::ErrorKind::DirectoryNotEmpty, + "Local path is not empty!", + )); + } + + Ok(()) + } + + /// Move contents from draft folder to local path with rollback support + async fn move_draft_to_local( + &self, + draft_folder: &Path, + local_path: &Path, + ) -> Result<(), std::io::Error> { + let draft_entries: Vec<_> = std::fs::read_dir(draft_folder) + .map_err(std::io::Error::other)? + .collect::, _>>() + .map_err(std::io::Error::other)?; + + let mut moved_items: Vec = Vec::new(); + + for entry in &draft_entries { + let entry_path = entry.path(); + let target_path = local_path.join(entry_path.file_name().unwrap()); + + // Move each file/directory from draft folder to local path + std::fs::rename(&entry_path, &target_path).map_err(|e| { + // Rollback all previously moved items + for moved_item in &moved_items { + let _ = std::fs::rename(&moved_item.target, &moved_item.source); + } + std::io::Error::other(e) + })?; + + moved_items.push(MovedItem { + source: entry_path.clone(), + target: target_path.clone(), + }); + } + + // Remove the now-empty draft folder + std::fs::remove_dir(draft_folder).map_err(|e| { + // Rollback all moved items if folder removal fails + for moved_item in &moved_items { + let _ = std::fs::rename(&moved_item.target, &moved_item.source); + } + std::io::Error::other(e) + })?; + + Ok(()) + } + + /// Move contents from local path to draft folder with rollback support (except .jv folder) + async fn move_local_to_draft( + &self, + local_path: &Path, + draft_folder: &Path, + ) -> Result<(), std::io::Error> { + let jv_folder = local_path.join(CLIENT_PATH_WORKSPACE_ROOT); + let entries: Vec<_> = std::fs::read_dir(local_path) + .map_err(std::io::Error::other)? + .collect::, _>>() + .map_err(std::io::Error::other)?; + + let mut moved_items: Vec = Vec::new(); + + for entry in &entries { + let entry_path = entry.path(); + + // Skip the .jv folder + if entry_path == jv_folder + || entry_path.file_name().and_then(|s| s.to_str()) + == Some(CLIENT_FOLDER_WORKSPACE_ROOT_NAME) + { + continue; + } + + let target_path = draft_folder.join(entry_path.file_name().unwrap()); + + // Move each file/directory from local path to draft folder + std::fs::rename(&entry_path, &target_path).map_err(|e| { + // Rollback all previously moved items + for moved_item in &moved_items { + let _ = std::fs::rename(&moved_item.target, &moved_item.source); + } + std::io::Error::other(e) + })?; + + moved_items.push(MovedItem { + source: entry_path.clone(), + target: target_path.clone(), + }); + } + + Ok(()) + } + + /// Get the currently used account + pub fn current_account(&self) -> MemberId { + self.using_account.clone() + } + + /// Check if the current member is interacting as a host. + pub fn is_host_mode(&self) -> bool { + self.using_host_mode + } + + /// Check if the local workspace is stained. + pub fn stained(&self) -> bool { + self.stained_uuid.is_some() + } + + /// Get the UUID of the vault that the local workspace is stained with. + pub fn stained_uuid(&self) -> Option { + self.stained_uuid + } + + /// Stain the local workspace with the given UUID. + pub fn stain(&mut self, uuid: VaultUuid) { + self.stained_uuid = Some(uuid); + } + + /// Unstain the local workspace. + pub fn unstain(&mut self) { + self.stained_uuid = None; + } + + /// Get the upstream address. + pub fn upstream_addr(&self) -> SocketAddr { + self.upstream_addr + } + + /// Get the currently used sheet + pub fn sheet_in_use(&self) -> &Option { + &self.sheet_in_use + } + + /// Get draft folder + pub fn draft_folder( + &self, + account: &MemberId, + sheet_name: &SheetName, + local_workspace_path: impl Into, + ) -> PathBuf { + let account_str = snake_case!(account.as_str()); + let sheet_name_str = snake_case!(sheet_name.as_str()); + let draft_path = CLIENT_PATH_LOCAL_DRAFT + .replace(KEY_ACCOUNT, &account_str) + .replace(KEY_SHEET_NAME, &sheet_name_str); + local_workspace_path.into().join(draft_path) + } + + /// Get current draft folder + pub fn current_draft_folder(&self) -> Option { + let Some(sheet_name) = self.sheet_in_use() else { + return None; + }; + + let current_dir = current_local_path()?; + + Some(self.draft_folder(&self.using_account, sheet_name, current_dir)) + } +} + +#[derive(Clone)] +struct MovedItem { + source: PathBuf, + target: PathBuf, +} diff --git a/data/src/data/user.rs b/data/src/data/user.rs index 9f52fdc..a2326fa 100644 --- a/data/src/data/user.rs +++ b/data/src/data/user.rs @@ -1,4 +1,4 @@ -use crate::current::current_cfg_dir; +use crate::env::current_cfg_dir; use std::path::PathBuf; pub mod accounts; diff --git a/data/src/data/vault.rs b/data/src/data/vault.rs index 595997a..0f93016 100644 --- a/data/src/data/vault.rs +++ b/data/src/data/vault.rs @@ -8,15 +8,15 @@ use crate::{ REF_SHEET_NAME, 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}, + data::{member::Member, vault::vault_config::VaultConfig}, + env::{current_vault_path, find_vault_path}, }; -pub mod config; -pub mod member; -pub mod service; -pub mod sheet_share; -pub mod sheets; +pub mod lock_status; +pub mod mapping_share; +pub mod member_manage; +pub mod sheet_manage; +pub mod vault_config; pub mod virtual_file; pub struct Vault { diff --git a/data/src/data/vault/config.rs b/data/src/data/vault/config.rs deleted file mode 100644 index caa8552..0000000 --- a/data/src/data/vault/config.rs +++ /dev/null @@ -1,233 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr}; - -use cfg_file::ConfigFile; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::constants::{PORT, SERVER_FILE_VAULT}; -use crate::data::member::{Member, MemberId}; - -pub type VaultName = String; -pub type VaultUuid = Uuid; - -#[derive(Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "lowercase")] -pub enum AuthMode { - /// Use asymmetric keys: both client and server need to register keys, after which they can connect - Key, - - /// Use password: the password stays on the server, and the client needs to set the password locally for connection - #[default] - Password, - - /// No authentication: generally used in a strongly secure environment, skipping verification directly - NoAuth, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "lowercase")] -pub enum LoggerLevel { - Debug, - Trace, - - #[default] - Info, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "lowercase")] -pub enum ServiceEnabled { - Enable, - - #[default] - Disable, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "lowercase")] -pub enum BehaviourEnabled { - Yes, - - #[default] - No, -} - -impl Into for ServiceEnabled { - fn into(self) -> bool { - match self { - ServiceEnabled::Enable => true, - ServiceEnabled::Disable => false, - } - } -} - -impl Into for BehaviourEnabled { - fn into(self) -> bool { - match self { - BehaviourEnabled::Yes => true, - BehaviourEnabled::No => false, - } - } -} - -#[derive(Serialize, Deserialize, ConfigFile)] -#[cfg_file(path = SERVER_FILE_VAULT)] -pub struct VaultConfig { - /// Vault uuid, unique identifier for the vault - #[serde(rename = "uuid")] - vault_uuid: VaultUuid, - - /// Vault name, which can be used as the project name and generally serves as a hint - #[serde(rename = "name")] - vault_name: VaultName, - - /// Vault host ids, a list of member id representing administrator identities - #[serde(rename = "hosts")] - vault_host_list: Vec, - - /// Vault server configuration, which will be loaded when connecting to the server - #[serde(rename = "profile")] - server_config: VaultServerConfig, -} - -#[derive(Serialize, Deserialize)] -pub struct VaultServerConfig { - /// Local IP address to bind to when the server starts - #[serde(rename = "bind")] - local_bind: IpAddr, - - /// TCP port to bind to when the server starts - #[serde(rename = "port")] - port: u16, - - /// Enable logging - #[serde(rename = "logger")] - logger: Option, - - /// Logger Level - #[serde(rename = "logger_level")] - logger_level: Option, - - /// Whether to enable LAN discovery, allowing members on the same LAN to more easily find the upstream server - #[serde(rename = "lan_discovery")] - lan_discovery: Option, // TODO - - /// Authentication mode for the vault server - /// key: Use asymmetric keys for authentication - /// password: Use a password for authentication - /// noauth: No authentication required, requires a strongly secure environment - #[serde(rename = "auth_mode")] - auth_mode: Option, // TODO -} - -impl Default for VaultConfig { - fn default() -> Self { - Self { - vault_uuid: Uuid::new_v4(), - vault_name: "JustEnoughVault".to_string(), - vault_host_list: Vec::new(), - server_config: VaultServerConfig { - local_bind: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - port: PORT, - logger: Some(BehaviourEnabled::default()), - logger_level: Some(LoggerLevel::default()), - lan_discovery: Some(ServiceEnabled::default()), - auth_mode: Some(AuthMode::Key), - }, - } - } -} - -/// Vault Management -impl VaultConfig { - /// Change name of the vault. - pub fn change_name(&mut self, name: impl Into) { - self.vault_name = name.into() - } - - /// Add admin - pub fn add_admin(&mut self, member: &Member) { - let uuid = member.id(); - if !self.vault_host_list.contains(&uuid) { - self.vault_host_list.push(uuid); - } - } - - /// Remove admin - pub fn remove_admin(&mut self, member: &Member) { - let id = member.id(); - self.vault_host_list.retain(|x| x != &id); - } - - /// Get vault UUID - pub fn vault_uuid(&self) -> &VaultUuid { - &self.vault_uuid - } - - /// Set vault UUID - pub fn set_vault_uuid(&mut self, vault_uuid: VaultUuid) { - self.vault_uuid = vault_uuid; - } - - /// Get vault name - pub fn vault_name(&self) -> &VaultName { - &self.vault_name - } - - /// Set vault name - pub fn set_vault_name(&mut self, vault_name: VaultName) { - self.vault_name = vault_name; - } - - /// Get vault admin list - pub fn vault_host_list(&self) -> &Vec { - &self.vault_host_list - } - - /// Set vault admin list - pub fn set_vault_host_list(&mut self, vault_host_list: Vec) { - self.vault_host_list = vault_host_list; - } - - /// Get server config - pub fn server_config(&self) -> &VaultServerConfig { - &self.server_config - } - - /// Set server config - pub fn set_server_config(&mut self, server_config: VaultServerConfig) { - self.server_config = server_config; - } -} - -impl VaultServerConfig { - /// Get local bind IP address - pub fn local_bind(&self) -> &IpAddr { - &self.local_bind - } - - /// Get port - pub fn port(&self) -> u16 { - self.port - } - - /// Check if LAN discovery is enabled - pub fn is_lan_discovery_enabled(&self) -> bool { - self.lan_discovery.clone().unwrap_or_default().into() - } - - /// Get logger enabled status - pub fn is_logger_enabled(&self) -> bool { - self.logger.clone().unwrap_or_default().into() - } - - /// Get logger level - pub fn logger_level(&self) -> LoggerLevel { - self.logger_level.clone().unwrap_or_default() - } - - /// Get authentication mode - pub fn auth_mode(&self) -> AuthMode { - self.auth_mode.clone().unwrap_or_default() - } -} diff --git a/data/src/data/vault/lock_status.rs b/data/src/data/vault/lock_status.rs new file mode 100644 index 0000000..3f59c30 --- /dev/null +++ b/data/src/data/vault/lock_status.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; + +use crate::{constants::SERVER_FILE_LOCKFILE, data::vault::Vault}; + +impl Vault { + /// Get the path of the lock file for the current Vault + pub fn lock_file_path(&self) -> PathBuf { + self.vault_path().join(SERVER_FILE_LOCKFILE) + } + + /// Check if the current Vault is locked + pub fn is_locked(&self) -> bool { + self.lock_file_path().exists() + } + + /// Lock the current Vault + pub fn lock(&self) -> Result<(), std::io::Error> { + if self.is_locked() { + return Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!( + "Vault is locked! This indicates a service is already running here.\nPlease stop other services or delete the lock file at the vault root directory: {}", + self.lock_file_path().display() + ), + )); + } + std::fs::File::create(self.lock_file_path())?; + Ok(()) + } + + /// Unlock the current Vault + pub fn unlock(&self) -> Result<(), std::io::Error> { + if let Err(e) = std::fs::remove_file(self.lock_file_path()) + && e.kind() != std::io::ErrorKind::NotFound + { + return Err(e); + } + Ok(()) + } +} diff --git a/data/src/data/vault/mapping_share.rs b/data/src/data/vault/mapping_share.rs new file mode 100644 index 0000000..5d27859 --- /dev/null +++ b/data/src/data/vault/mapping_share.rs @@ -0,0 +1,422 @@ +use std::{collections::HashMap, io::Error, path::PathBuf}; + +use cfg_file::{ConfigFile, config::ConfigFile}; +use rand::{Rng, rng}; +use serde::{Deserialize, Serialize}; +use string_proc::{format_path, snake_case}; +use tokio::fs; + +use crate::{ + constants::{ + KEY_SHARE_ID, KEY_SHEET_NAME, SERVER_FILE_SHEET_SHARE, SERVER_PATH_SHARES, + SERVER_SUFFIX_SHEET_SHARE_FILE_NO_DOT, + }, + data::{ + member::MemberId, + sheet::{Sheet, SheetMappingMetadata, SheetName, SheetPathBuf}, + vault::Vault, + }, +}; + +pub type SheetShareId = String; + +#[derive(Default, Serialize, Deserialize, ConfigFile, Clone, Debug)] +pub struct Share { + /// Sharer: the member who created this share item + #[serde(rename = "sharer")] + pub sharer: MemberId, + + /// Description of the share item + #[serde(rename = "desc")] + pub description: String, + + /// Metadata path + #[serde(skip)] + pub path: Option, + + /// From: which sheet the member exported the file from + #[serde(rename = "from")] + pub from_sheet: SheetName, + + /// Mappings: the sheet mappings contained in the share item + #[serde(rename = "map")] + pub mappings: HashMap, +} + +#[derive(Default, Serialize, Deserialize, ConfigFile, Clone, PartialEq, Eq)] +pub enum ShareMergeMode { + /// If a path or file already exists during merge, prioritize the incoming share + /// Path conflict: replace the mapping content at the local path with the incoming content + /// File conflict: delete the original file mapping and create a new one + Overwrite, + + /// If a path or file already exists during merge, skip overwriting this entry + Skip, + + /// Pre-check for conflicts, prohibit merging if any conflicts are found + #[default] + Safe, + + /// Reject all shares + RejectAll, +} + +#[derive(Default, Serialize, Deserialize, ConfigFile, Clone)] +pub struct ShareMergeConflict { + /// Duplicate mappings exist + pub duplicate_mapping: Vec, + + /// Duplicate files exist + pub duplicate_file: Vec, +} + +impl ShareMergeConflict { + /// Check if there are no conflicts + pub fn ok(&self) -> bool { + self.duplicate_mapping.is_empty() && self.duplicate_file.is_empty() + } +} + +impl Vault { + /// Get the path of a share item in a sheet + pub fn share_file_path(&self, sheet_name: &SheetName, share_id: &SheetShareId) -> PathBuf { + let sheet_name = snake_case!(sheet_name.clone()); + let share_id = share_id.clone(); + + // Format the path to remove "./" prefix and normalize it + let path_str = SERVER_FILE_SHEET_SHARE + .replace(KEY_SHEET_NAME, &sheet_name) + .replace(KEY_SHARE_ID, &share_id); + + // Use format_path to normalize the path + match format_path::format_path_str(&path_str) { + Ok(normalized_path) => self.vault_path().join(normalized_path), + Err(_) => { + // Fallback to original behavior if formatting fails + self.vault_path().join(path_str) + } + } + } + + /// Get the actual paths of all share items in a sheet + pub async fn share_file_paths(&self, sheet_name: &SheetName) -> Vec { + let sheet_name = snake_case!(sheet_name.clone()); + let shares_dir = self + .vault_path() + .join(SERVER_PATH_SHARES.replace(KEY_SHEET_NAME, &sheet_name)); + + let mut result = Vec::new(); + if let Ok(mut entries) = fs::read_dir(shares_dir).await { + while let Ok(Some(entry)) = entries.next_entry().await { + let path = entry.path(); + if path.is_file() + && path.extension().and_then(|s| s.to_str()) + == Some(SERVER_SUFFIX_SHEET_SHARE_FILE_NO_DOT) + { + result.push(path); + } + } + } + result + } +} + +impl<'a> Sheet<'a> { + /// Get the shares of a sheet + pub async fn get_shares(&self) -> Result, std::io::Error> { + let paths = self.vault_reference.share_file_paths(&self.name).await; + let mut shares = Vec::new(); + + for path in paths { + match Share::read_from(&path).await { + Ok(mut share) => { + share.path = Some(path); + shares.push(share); + } + Err(e) => return Err(e), + } + } + + Ok(shares) + } + + /// Get a share of a sheet + pub async fn get_share(&self, share_id: &SheetShareId) -> Result { + let path = self.vault_reference.share_file_path(&self.name, share_id); + let mut share = Share::read_from(&path).await?; + share.path = Some(path); + Ok(share) + } + + /// Import a share of a sheet by its ID + pub async fn merge_share_by_id( + self, + share_id: &SheetShareId, + share_merge_mode: ShareMergeMode, + ) -> Result<(), std::io::Error> { + let share = self.get_share(share_id).await?; + self.merge_share(share, share_merge_mode).await + } + + /// Import a share of a sheet + pub async fn merge_share( + mut self, + share: Share, + share_merge_mode: ShareMergeMode, + ) -> Result<(), std::io::Error> { + // Backup original data and edit based on this backup + let mut copy_share = share.clone(); + let mut copy_sheet = self.clone_data(); + + // Pre-check + let conflicts = self.precheck(©_share); + let mut reject_mode = false; + + match share_merge_mode { + // Safe mode: conflicts are not allowed + ShareMergeMode::Safe => { + // Conflicts found + if !conflicts.ok() { + // Do nothing, return Error + return Err(Error::new( + std::io::ErrorKind::AlreadyExists, + "Mappings or files already exist!", + )); + } + } + // Overwrite mode: when conflicts occur, prioritize the share item + ShareMergeMode::Overwrite => { + // Handle duplicate mappings + for path in conflicts.duplicate_mapping { + // Get the share data + let Some(share_value) = copy_share.mappings.remove(&path) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Share value `{}` not found!", &path.display()), + )); + }; + // Overwrite + copy_sheet.mapping_mut().insert(path, share_value); + } + + // Handle duplicate IDs + for path in conflicts.duplicate_file { + // Get the share data + let Some(share_value) = copy_share.mappings.remove(&path) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Share value `{}` not found!", &path.display()), + )); + }; + + // Extract the file ID + let conflict_vfid = &share_value.id; + + // Through the sheet's ID mapping + let Some(id_mapping) = copy_sheet.id_mapping_mut() else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "Id mapping not found!", + )); + }; + + // Get the original path from the ID mapping + let Some(raw_path) = id_mapping.remove(conflict_vfid) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("The path of virtual file `{}' not found!", conflict_vfid), + )); + }; + + // Remove the original path mapping + if copy_sheet.mapping_mut().remove(&raw_path).is_none() { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Remove mapping `{}` failed!", &raw_path.display()), + )); + } + // Insert the new item + copy_sheet.mapping_mut().insert(path, share_value); + } + } + // Skip mode: when conflicts occur, prioritize the local sheet + ShareMergeMode::Skip => { + // Directly remove conflicting items + for path in conflicts.duplicate_mapping { + copy_share.mappings.remove(&path); + } + for path in conflicts.duplicate_file { + copy_share.mappings.remove(&path); + } + } + // Reject all mode: reject all shares + ShareMergeMode::RejectAll => { + reject_mode = true; // Only mark as rejected + } + } + + if !reject_mode { + // Subsequent merging + copy_sheet + .mapping_mut() + .extend(copy_share.mappings.into_iter()); + + // Merge completed + self.data = copy_sheet; // Write the result + + // Merge completed, consume the sheet + self.persist().await.map_err(|err| { + Error::new( + std::io::ErrorKind::NotFound, + format!("Write sheet failed: {}", err), + ) + })?; + } + + // Persistence succeeded, continue to consume the share item + share.remove().await.map_err(|err| { + Error::new( + std::io::ErrorKind::NotFound, + format!("Remove share failed: {}", err.1), + ) + }) + } + + // Pre-check whether the share can be imported into the current sheet without conflicts + fn precheck(&self, share: &Share) -> ShareMergeConflict { + let mut conflicts = ShareMergeConflict::default(); + + for (mapping, metadata) in &share.mappings { + // Check for duplicate mappings + if self.mapping().contains_key(mapping.as_path()) { + conflicts.duplicate_mapping.push(mapping.clone()); + continue; + } + + // Check for duplicate IDs + if let Some(id_mapping) = self.id_mapping() { + if id_mapping.contains_key(&metadata.id) { + conflicts.duplicate_file.push(mapping.clone()); + continue; + } + } + } + + conflicts + } + + /// Share mappings with another sheet + pub async fn share_mappings( + &self, + other_sheet: &SheetName, + mappings: Vec, + sharer: &MemberId, + description: String, + ) -> Result { + let other_sheet = snake_case!(other_sheet.clone()); + let sharer = snake_case!(sharer.clone()); + + // Check if the sheet exists + let sheet_names = self.vault_reference.sheet_names()?; + if !sheet_names.contains(&other_sheet) { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Sheet `{}` not found!", &other_sheet), + )); + } + + // Check if the target file exists, regenerate ID if path already exists, up to 20 attempts + let target_path = { + let mut id; + let mut share_path; + let mut attempts = 0; + + loop { + id = Share::gen_share_id(&sharer); + share_path = self.vault_reference.share_file_path(&other_sheet, &id); + + if !share_path.exists() { + break share_path; + } + + attempts += 1; + if attempts >= 20 { + return Err(Error::new( + std::io::ErrorKind::AlreadyExists, + "Failed to generate unique share ID after 20 attempts!", + )); + } + } + }; + + // Validate that the share is valid + let mut share_mappings = HashMap::new(); + for mapping_path in &mappings { + if let Some(metadata) = self.mapping().get(mapping_path) { + share_mappings.insert(mapping_path.clone(), metadata.clone()); + } else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Mapping `{}` not found in sheet!", mapping_path.display()), + )); + } + } + + // Build share data + let share_data = Share { + sharer, + description, + path: None, // This is only needed during merging (reading), no need to serialize now + from_sheet: self.name.clone(), + mappings: share_mappings, + }; + + // Write data + Share::write_to(&share_data, target_path).await?; + + Ok(share_data) + } +} + +impl Share { + /// Generate a share ID for a given sharer + pub fn gen_share_id(sharer: &MemberId) -> String { + let sharer_snake = snake_case!(sharer.clone()); + let random_part: String = rng() + .sample_iter(&rand::distr::Alphanumeric) + .take(8) + .map(char::from) + .collect(); + format!("{}@{}", sharer_snake, random_part) + } + + /// Delete a share (reject or remove the share item) + /// If deletion succeeds, returns `Ok(())`; + /// If deletion fails, returns `Err((self, std::io::Error))`, containing the original share object and the error information. + pub async fn remove(self) -> Result<(), (Self, std::io::Error)> { + let Some(path) = &self.path else { + return Err(( + self, + Error::new(std::io::ErrorKind::NotFound, "No share path recorded!"), + )); + }; + + if !path.exists() { + return Err(( + self, + Error::new(std::io::ErrorKind::NotFound, "No share file exists!"), + )); + } + + match fs::remove_file(path).await { + Err(err) => Err(( + self, + Error::new( + std::io::ErrorKind::Other, + format!("Failed to delete share file: {}", err), + ), + )), + Ok(_) => Ok(()), + } + } +} diff --git a/data/src/data/vault/member.rs b/data/src/data/vault/member.rs deleted file mode 100644 index 9d22d09..0000000 --- a/data/src/data/vault/member.rs +++ /dev/null @@ -1,144 +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, - SERVER_SUFFIX_MEMBER_INFO_NO_DOT, - }, - data::{ - member::{Member, MemberId}, - vault::Vault, - }, -}; - -const ID_PARAM: &str = "{member_id}"; - -/// Member Manage -impl Vault { - /// Read member from configuration file - pub async fn member(&self, id: &MemberId) -> Result { - if let Some(cfg_file) = self.member_cfg(id) { - let member = Member::read_from(cfg_file).await?; - return Ok(member); - } - - Err(Error::new(ErrorKind::NotFound, "Member not found!")) - } - - /// List all member IDs in the vault - pub fn member_ids(&self) -> Result, std::io::Error> { - let members_path = self.vault_path.join(SERVER_PATH_MEMBERS); - - if !members_path.exists() { - return Ok(Vec::new()); - } - - let mut member_ids = Vec::new(); - - for entry in fs::read_dir(members_path)? { - let entry = entry?; - let path = entry.path(); - - if path.is_file() - && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) - && path.extension().and_then(|s| s.to_str()) - == Some(SERVER_SUFFIX_MEMBER_INFO_NO_DOT) - { - member_ids.push(file_name.to_string()); - } - } - - Ok(member_ids) - } - - /// Get all members - /// This method will read and deserialize member information, please pay attention to performance issues - pub async fn members(&self) -> Result, std::io::Error> { - let mut members = Vec::new(); - - for member_id in self.member_ids()? { - if let Ok(member) = self.member(&member_id).await { - members.push(member); - } - } - - Ok(members) - } - - /// Update member info - pub async fn update_member(&self, member: Member) -> Result<(), std::io::Error> { - // Ensure member exist - if self.member_cfg(&member.id()).is_some() { - let member_cfg_path = self.member_cfg_path(&member.id()); - Member::write_to(&member, member_cfg_path).await?; - return Ok(()); - } - - Err(Error::new(ErrorKind::NotFound, "Member not found!")) - } - - /// Register a member to vault - pub async fn register_member_to_vault(&self, member: Member) -> Result<(), std::io::Error> { - // Ensure member not exist - if self.member_cfg(&member.id()).is_some() { - return Err(Error::new( - ErrorKind::DirectoryNotEmpty, - format!("Member `{}` already registered!", member.id()), - )); - } - - // Wrtie config file to member dir - let member_cfg_path = self.member_cfg_path(&member.id()); - Member::write_to(&member, member_cfg_path).await?; - - Ok(()) - } - - /// Remove member from vault - pub fn remove_member_from_vault(&self, id: &MemberId) -> Result<(), std::io::Error> { - // Ensure member exist - if let Some(member_cfg_path) = self.member_cfg(id) { - fs::remove_file(member_cfg_path)?; - } - - Ok(()) - } - - /// Try to get the member's configuration file to determine if the member exists - pub fn member_cfg(&self, id: &MemberId) -> Option { - let cfg_file = self.member_cfg_path(id); - if cfg_file.exists() { - Some(cfg_file) - } else { - None - } - } - - /// Try to get the member's public key file to determine if the member has login permission - pub fn member_key(&self, id: &MemberId) -> Option { - let key_file = self.member_key_path(id); - if key_file.exists() { - Some(key_file) - } else { - None - } - } - - /// Get the member's configuration file path, but do not check if the file exists - pub fn member_cfg_path(&self, id: &MemberId) -> PathBuf { - self.vault_path - .join(SERVER_FILE_MEMBER_INFO.replace(ID_PARAM, id.to_string().as_str())) - } - - /// Get the member's public key file path, but do not check if the file exists - pub fn member_key_path(&self, id: &MemberId) -> PathBuf { - self.vault_path - .join(SERVER_FILE_MEMBER_PUB.replace(ID_PARAM, id.to_string().as_str())) - } -} diff --git a/data/src/data/vault/member_manage.rs b/data/src/data/vault/member_manage.rs new file mode 100644 index 0000000..9d22d09 --- /dev/null +++ b/data/src/data/vault/member_manage.rs @@ -0,0 +1,144 @@ +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, + SERVER_SUFFIX_MEMBER_INFO_NO_DOT, + }, + data::{ + member::{Member, MemberId}, + vault::Vault, + }, +}; + +const ID_PARAM: &str = "{member_id}"; + +/// Member Manage +impl Vault { + /// Read member from configuration file + pub async fn member(&self, id: &MemberId) -> Result { + if let Some(cfg_file) = self.member_cfg(id) { + let member = Member::read_from(cfg_file).await?; + return Ok(member); + } + + Err(Error::new(ErrorKind::NotFound, "Member not found!")) + } + + /// List all member IDs in the vault + pub fn member_ids(&self) -> Result, std::io::Error> { + let members_path = self.vault_path.join(SERVER_PATH_MEMBERS); + + if !members_path.exists() { + return Ok(Vec::new()); + } + + let mut member_ids = Vec::new(); + + for entry in fs::read_dir(members_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() + && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) + && path.extension().and_then(|s| s.to_str()) + == Some(SERVER_SUFFIX_MEMBER_INFO_NO_DOT) + { + member_ids.push(file_name.to_string()); + } + } + + Ok(member_ids) + } + + /// Get all members + /// This method will read and deserialize member information, please pay attention to performance issues + pub async fn members(&self) -> Result, std::io::Error> { + let mut members = Vec::new(); + + for member_id in self.member_ids()? { + if let Ok(member) = self.member(&member_id).await { + members.push(member); + } + } + + Ok(members) + } + + /// Update member info + pub async fn update_member(&self, member: Member) -> Result<(), std::io::Error> { + // Ensure member exist + if self.member_cfg(&member.id()).is_some() { + let member_cfg_path = self.member_cfg_path(&member.id()); + Member::write_to(&member, member_cfg_path).await?; + return Ok(()); + } + + Err(Error::new(ErrorKind::NotFound, "Member not found!")) + } + + /// Register a member to vault + pub async fn register_member_to_vault(&self, member: Member) -> Result<(), std::io::Error> { + // Ensure member not exist + if self.member_cfg(&member.id()).is_some() { + return Err(Error::new( + ErrorKind::DirectoryNotEmpty, + format!("Member `{}` already registered!", member.id()), + )); + } + + // Wrtie config file to member dir + let member_cfg_path = self.member_cfg_path(&member.id()); + Member::write_to(&member, member_cfg_path).await?; + + Ok(()) + } + + /// Remove member from vault + pub fn remove_member_from_vault(&self, id: &MemberId) -> Result<(), std::io::Error> { + // Ensure member exist + if let Some(member_cfg_path) = self.member_cfg(id) { + fs::remove_file(member_cfg_path)?; + } + + Ok(()) + } + + /// Try to get the member's configuration file to determine if the member exists + pub fn member_cfg(&self, id: &MemberId) -> Option { + let cfg_file = self.member_cfg_path(id); + if cfg_file.exists() { + Some(cfg_file) + } else { + None + } + } + + /// Try to get the member's public key file to determine if the member has login permission + pub fn member_key(&self, id: &MemberId) -> Option { + let key_file = self.member_key_path(id); + if key_file.exists() { + Some(key_file) + } else { + None + } + } + + /// Get the member's configuration file path, but do not check if the file exists + pub fn member_cfg_path(&self, id: &MemberId) -> PathBuf { + self.vault_path + .join(SERVER_FILE_MEMBER_INFO.replace(ID_PARAM, id.to_string().as_str())) + } + + /// Get the member's public key file path, but do not check if the file exists + pub fn member_key_path(&self, id: &MemberId) -> PathBuf { + self.vault_path + .join(SERVER_FILE_MEMBER_PUB.replace(ID_PARAM, id.to_string().as_str())) + } +} diff --git a/data/src/data/vault/service.rs b/data/src/data/vault/service.rs deleted file mode 100644 index 3f59c30..0000000 --- a/data/src/data/vault/service.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::path::PathBuf; - -use crate::{constants::SERVER_FILE_LOCKFILE, data::vault::Vault}; - -impl Vault { - /// Get the path of the lock file for the current Vault - pub fn lock_file_path(&self) -> PathBuf { - self.vault_path().join(SERVER_FILE_LOCKFILE) - } - - /// Check if the current Vault is locked - pub fn is_locked(&self) -> bool { - self.lock_file_path().exists() - } - - /// Lock the current Vault - pub fn lock(&self) -> Result<(), std::io::Error> { - if self.is_locked() { - return Err(std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - format!( - "Vault is locked! This indicates a service is already running here.\nPlease stop other services or delete the lock file at the vault root directory: {}", - self.lock_file_path().display() - ), - )); - } - std::fs::File::create(self.lock_file_path())?; - Ok(()) - } - - /// Unlock the current Vault - pub fn unlock(&self) -> Result<(), std::io::Error> { - if let Err(e) = std::fs::remove_file(self.lock_file_path()) - && e.kind() != std::io::ErrorKind::NotFound - { - return Err(e); - } - Ok(()) - } -} diff --git a/data/src/data/vault/sheet_manage.rs b/data/src/data/vault/sheet_manage.rs new file mode 100644 index 0000000..c22c849 --- /dev/null +++ b/data/src/data/vault/sheet_manage.rs @@ -0,0 +1,274 @@ +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, SERVER_SUFFIX_SHEET_FILE_NO_DOT}, + data::{ + member::MemberId, + sheet::{Sheet, SheetData, SheetName}, + vault::Vault, + }, +}; + +/// Vault Sheets Management +impl Vault { + /// Load all sheets in the vault + /// + /// It is generally not recommended to call this function frequently. + /// Although a vault typically won't contain too many sheets, + /// if individual sheet contents are large, this operation may cause + /// significant performance bottlenecks. + pub async fn sheets<'a>(&'a self) -> Result>, std::io::Error> { + let sheet_names = self.sheet_names()?; + let mut sheets = Vec::new(); + + for sheet_name in sheet_names { + let sheet = self.sheet(&sheet_name).await?; + sheets.push(sheet); + } + + Ok(sheets) + } + + /// Search for all sheet names in the vault + /// + /// The complexity of this operation is proportional to the number of sheets, + /// but generally there won't be too many sheets in a Vault + pub fn sheet_names(&self) -> Result, std::io::Error> { + // Get the sheets directory path + let sheets_dir = self.vault_path.join(SERVER_PATH_SHEETS); + + // If the directory doesn't exist, return an empty list + if !sheets_dir.exists() { + return Ok(vec![]); + } + + let mut sheet_names = Vec::new(); + + // Iterate through all files in the sheets directory + for entry in std::fs::read_dir(sheets_dir)? { + let entry = entry?; + let path = entry.path(); + + // Check if it's a YAML file + if path.is_file() + && path + .extension() + .is_some_and(|ext| ext == SERVER_SUFFIX_SHEET_FILE_NO_DOT) + && let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) + { + // Create a new SheetName and add it to the result list + sheet_names.push(file_stem.to_string()); + } + } + + Ok(sheet_names) + } + + /// Read a sheet from its name + /// + /// If the sheet information is successfully found in the vault, + /// it will be deserialized and read as a sheet. + /// This is the only correct way to obtain a sheet instance. + pub async fn sheet<'a>(&'a self, sheet_name: &SheetName) -> Result, std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Get the path to the sheet file + let sheet_path = Sheet::sheet_path_with_name(self, &sheet_name); + + // Ensure the sheet file exists + if !sheet_path.exists() { + // If the sheet does not exist, try to restore it from the trash + if self.restore_sheet(&sheet_name).await.is_err() { + // If restoration fails, return an error + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Sheet `{}` not found!", sheet_name), + )); + } + } + + // Read the sheet data from the file + let data = SheetData::read_from(sheet_path).await?; + + Ok(Sheet { + name: sheet_name.clone(), + data, + vault_reference: self, + }) + } + + /// Create a sheet locally and return the sheet instance + /// + /// This method creates a new sheet in the vault with the given name and holder. + /// It will verify that the member exists and that the sheet doesn't already exist + /// before creating the sheet file with default empty data. + pub async fn create_sheet<'a>( + &'a self, + sheet_name: &SheetName, + holder: &MemberId, + ) -> Result, std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Ensure member exists + if !self.member_cfg_path(holder).exists() { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Member `{}` not found!", &holder), + )); + } + + // Ensure sheet does not already exist + let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); + if sheet_file_path.exists() { + return Err(Error::new( + std::io::ErrorKind::AlreadyExists, + format!("Sheet `{}` already exists!", &sheet_name), + )); + } + + // Create the sheet file + let sheet_data = SheetData { + holder: Some(holder.clone()), + mapping: HashMap::new(), + id_mapping: None, + write_count: 0, + }; + 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!( + "{}_{}.{}", + sheet_name, timestamp, SERVER_SUFFIX_SHEET_FILE_NO_DOT + ); + 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/data/src/data/vault/sheet_share.rs b/data/src/data/vault/sheet_share.rs deleted file mode 100644 index 5d27859..0000000 --- a/data/src/data/vault/sheet_share.rs +++ /dev/null @@ -1,422 +0,0 @@ -use std::{collections::HashMap, io::Error, path::PathBuf}; - -use cfg_file::{ConfigFile, config::ConfigFile}; -use rand::{Rng, rng}; -use serde::{Deserialize, Serialize}; -use string_proc::{format_path, snake_case}; -use tokio::fs; - -use crate::{ - constants::{ - KEY_SHARE_ID, KEY_SHEET_NAME, SERVER_FILE_SHEET_SHARE, SERVER_PATH_SHARES, - SERVER_SUFFIX_SHEET_SHARE_FILE_NO_DOT, - }, - data::{ - member::MemberId, - sheet::{Sheet, SheetMappingMetadata, SheetName, SheetPathBuf}, - vault::Vault, - }, -}; - -pub type SheetShareId = String; - -#[derive(Default, Serialize, Deserialize, ConfigFile, Clone, Debug)] -pub struct Share { - /// Sharer: the member who created this share item - #[serde(rename = "sharer")] - pub sharer: MemberId, - - /// Description of the share item - #[serde(rename = "desc")] - pub description: String, - - /// Metadata path - #[serde(skip)] - pub path: Option, - - /// From: which sheet the member exported the file from - #[serde(rename = "from")] - pub from_sheet: SheetName, - - /// Mappings: the sheet mappings contained in the share item - #[serde(rename = "map")] - pub mappings: HashMap, -} - -#[derive(Default, Serialize, Deserialize, ConfigFile, Clone, PartialEq, Eq)] -pub enum ShareMergeMode { - /// If a path or file already exists during merge, prioritize the incoming share - /// Path conflict: replace the mapping content at the local path with the incoming content - /// File conflict: delete the original file mapping and create a new one - Overwrite, - - /// If a path or file already exists during merge, skip overwriting this entry - Skip, - - /// Pre-check for conflicts, prohibit merging if any conflicts are found - #[default] - Safe, - - /// Reject all shares - RejectAll, -} - -#[derive(Default, Serialize, Deserialize, ConfigFile, Clone)] -pub struct ShareMergeConflict { - /// Duplicate mappings exist - pub duplicate_mapping: Vec, - - /// Duplicate files exist - pub duplicate_file: Vec, -} - -impl ShareMergeConflict { - /// Check if there are no conflicts - pub fn ok(&self) -> bool { - self.duplicate_mapping.is_empty() && self.duplicate_file.is_empty() - } -} - -impl Vault { - /// Get the path of a share item in a sheet - pub fn share_file_path(&self, sheet_name: &SheetName, share_id: &SheetShareId) -> PathBuf { - let sheet_name = snake_case!(sheet_name.clone()); - let share_id = share_id.clone(); - - // Format the path to remove "./" prefix and normalize it - let path_str = SERVER_FILE_SHEET_SHARE - .replace(KEY_SHEET_NAME, &sheet_name) - .replace(KEY_SHARE_ID, &share_id); - - // Use format_path to normalize the path - match format_path::format_path_str(&path_str) { - Ok(normalized_path) => self.vault_path().join(normalized_path), - Err(_) => { - // Fallback to original behavior if formatting fails - self.vault_path().join(path_str) - } - } - } - - /// Get the actual paths of all share items in a sheet - pub async fn share_file_paths(&self, sheet_name: &SheetName) -> Vec { - let sheet_name = snake_case!(sheet_name.clone()); - let shares_dir = self - .vault_path() - .join(SERVER_PATH_SHARES.replace(KEY_SHEET_NAME, &sheet_name)); - - let mut result = Vec::new(); - if let Ok(mut entries) = fs::read_dir(shares_dir).await { - while let Ok(Some(entry)) = entries.next_entry().await { - let path = entry.path(); - if path.is_file() - && path.extension().and_then(|s| s.to_str()) - == Some(SERVER_SUFFIX_SHEET_SHARE_FILE_NO_DOT) - { - result.push(path); - } - } - } - result - } -} - -impl<'a> Sheet<'a> { - /// Get the shares of a sheet - pub async fn get_shares(&self) -> Result, std::io::Error> { - let paths = self.vault_reference.share_file_paths(&self.name).await; - let mut shares = Vec::new(); - - for path in paths { - match Share::read_from(&path).await { - Ok(mut share) => { - share.path = Some(path); - shares.push(share); - } - Err(e) => return Err(e), - } - } - - Ok(shares) - } - - /// Get a share of a sheet - pub async fn get_share(&self, share_id: &SheetShareId) -> Result { - let path = self.vault_reference.share_file_path(&self.name, share_id); - let mut share = Share::read_from(&path).await?; - share.path = Some(path); - Ok(share) - } - - /// Import a share of a sheet by its ID - pub async fn merge_share_by_id( - self, - share_id: &SheetShareId, - share_merge_mode: ShareMergeMode, - ) -> Result<(), std::io::Error> { - let share = self.get_share(share_id).await?; - self.merge_share(share, share_merge_mode).await - } - - /// Import a share of a sheet - pub async fn merge_share( - mut self, - share: Share, - share_merge_mode: ShareMergeMode, - ) -> Result<(), std::io::Error> { - // Backup original data and edit based on this backup - let mut copy_share = share.clone(); - let mut copy_sheet = self.clone_data(); - - // Pre-check - let conflicts = self.precheck(©_share); - let mut reject_mode = false; - - match share_merge_mode { - // Safe mode: conflicts are not allowed - ShareMergeMode::Safe => { - // Conflicts found - if !conflicts.ok() { - // Do nothing, return Error - return Err(Error::new( - std::io::ErrorKind::AlreadyExists, - "Mappings or files already exist!", - )); - } - } - // Overwrite mode: when conflicts occur, prioritize the share item - ShareMergeMode::Overwrite => { - // Handle duplicate mappings - for path in conflicts.duplicate_mapping { - // Get the share data - let Some(share_value) = copy_share.mappings.remove(&path) else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Share value `{}` not found!", &path.display()), - )); - }; - // Overwrite - copy_sheet.mapping_mut().insert(path, share_value); - } - - // Handle duplicate IDs - for path in conflicts.duplicate_file { - // Get the share data - let Some(share_value) = copy_share.mappings.remove(&path) else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Share value `{}` not found!", &path.display()), - )); - }; - - // Extract the file ID - let conflict_vfid = &share_value.id; - - // Through the sheet's ID mapping - let Some(id_mapping) = copy_sheet.id_mapping_mut() else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "Id mapping not found!", - )); - }; - - // Get the original path from the ID mapping - let Some(raw_path) = id_mapping.remove(conflict_vfid) else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("The path of virtual file `{}' not found!", conflict_vfid), - )); - }; - - // Remove the original path mapping - if copy_sheet.mapping_mut().remove(&raw_path).is_none() { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Remove mapping `{}` failed!", &raw_path.display()), - )); - } - // Insert the new item - copy_sheet.mapping_mut().insert(path, share_value); - } - } - // Skip mode: when conflicts occur, prioritize the local sheet - ShareMergeMode::Skip => { - // Directly remove conflicting items - for path in conflicts.duplicate_mapping { - copy_share.mappings.remove(&path); - } - for path in conflicts.duplicate_file { - copy_share.mappings.remove(&path); - } - } - // Reject all mode: reject all shares - ShareMergeMode::RejectAll => { - reject_mode = true; // Only mark as rejected - } - } - - if !reject_mode { - // Subsequent merging - copy_sheet - .mapping_mut() - .extend(copy_share.mappings.into_iter()); - - // Merge completed - self.data = copy_sheet; // Write the result - - // Merge completed, consume the sheet - self.persist().await.map_err(|err| { - Error::new( - std::io::ErrorKind::NotFound, - format!("Write sheet failed: {}", err), - ) - })?; - } - - // Persistence succeeded, continue to consume the share item - share.remove().await.map_err(|err| { - Error::new( - std::io::ErrorKind::NotFound, - format!("Remove share failed: {}", err.1), - ) - }) - } - - // Pre-check whether the share can be imported into the current sheet without conflicts - fn precheck(&self, share: &Share) -> ShareMergeConflict { - let mut conflicts = ShareMergeConflict::default(); - - for (mapping, metadata) in &share.mappings { - // Check for duplicate mappings - if self.mapping().contains_key(mapping.as_path()) { - conflicts.duplicate_mapping.push(mapping.clone()); - continue; - } - - // Check for duplicate IDs - if let Some(id_mapping) = self.id_mapping() { - if id_mapping.contains_key(&metadata.id) { - conflicts.duplicate_file.push(mapping.clone()); - continue; - } - } - } - - conflicts - } - - /// Share mappings with another sheet - pub async fn share_mappings( - &self, - other_sheet: &SheetName, - mappings: Vec, - sharer: &MemberId, - description: String, - ) -> Result { - let other_sheet = snake_case!(other_sheet.clone()); - let sharer = snake_case!(sharer.clone()); - - // Check if the sheet exists - let sheet_names = self.vault_reference.sheet_names()?; - if !sheet_names.contains(&other_sheet) { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Sheet `{}` not found!", &other_sheet), - )); - } - - // Check if the target file exists, regenerate ID if path already exists, up to 20 attempts - let target_path = { - let mut id; - let mut share_path; - let mut attempts = 0; - - loop { - id = Share::gen_share_id(&sharer); - share_path = self.vault_reference.share_file_path(&other_sheet, &id); - - if !share_path.exists() { - break share_path; - } - - attempts += 1; - if attempts >= 20 { - return Err(Error::new( - std::io::ErrorKind::AlreadyExists, - "Failed to generate unique share ID after 20 attempts!", - )); - } - } - }; - - // Validate that the share is valid - let mut share_mappings = HashMap::new(); - for mapping_path in &mappings { - if let Some(metadata) = self.mapping().get(mapping_path) { - share_mappings.insert(mapping_path.clone(), metadata.clone()); - } else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Mapping `{}` not found in sheet!", mapping_path.display()), - )); - } - } - - // Build share data - let share_data = Share { - sharer, - description, - path: None, // This is only needed during merging (reading), no need to serialize now - from_sheet: self.name.clone(), - mappings: share_mappings, - }; - - // Write data - Share::write_to(&share_data, target_path).await?; - - Ok(share_data) - } -} - -impl Share { - /// Generate a share ID for a given sharer - pub fn gen_share_id(sharer: &MemberId) -> String { - let sharer_snake = snake_case!(sharer.clone()); - let random_part: String = rng() - .sample_iter(&rand::distr::Alphanumeric) - .take(8) - .map(char::from) - .collect(); - format!("{}@{}", sharer_snake, random_part) - } - - /// Delete a share (reject or remove the share item) - /// If deletion succeeds, returns `Ok(())`; - /// If deletion fails, returns `Err((self, std::io::Error))`, containing the original share object and the error information. - pub async fn remove(self) -> Result<(), (Self, std::io::Error)> { - let Some(path) = &self.path else { - return Err(( - self, - Error::new(std::io::ErrorKind::NotFound, "No share path recorded!"), - )); - }; - - if !path.exists() { - return Err(( - self, - Error::new(std::io::ErrorKind::NotFound, "No share file exists!"), - )); - } - - match fs::remove_file(path).await { - Err(err) => Err(( - self, - Error::new( - std::io::ErrorKind::Other, - format!("Failed to delete share file: {}", err), - ), - )), - Ok(_) => Ok(()), - } - } -} diff --git a/data/src/data/vault/sheets.rs b/data/src/data/vault/sheets.rs deleted file mode 100644 index c22c849..0000000 --- a/data/src/data/vault/sheets.rs +++ /dev/null @@ -1,274 +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, SERVER_SUFFIX_SHEET_FILE_NO_DOT}, - data::{ - member::MemberId, - sheet::{Sheet, SheetData, SheetName}, - vault::Vault, - }, -}; - -/// Vault Sheets Management -impl Vault { - /// Load all sheets in the vault - /// - /// It is generally not recommended to call this function frequently. - /// Although a vault typically won't contain too many sheets, - /// if individual sheet contents are large, this operation may cause - /// significant performance bottlenecks. - pub async fn sheets<'a>(&'a self) -> Result>, std::io::Error> { - let sheet_names = self.sheet_names()?; - let mut sheets = Vec::new(); - - for sheet_name in sheet_names { - let sheet = self.sheet(&sheet_name).await?; - sheets.push(sheet); - } - - Ok(sheets) - } - - /// Search for all sheet names in the vault - /// - /// The complexity of this operation is proportional to the number of sheets, - /// but generally there won't be too many sheets in a Vault - pub fn sheet_names(&self) -> Result, std::io::Error> { - // Get the sheets directory path - let sheets_dir = self.vault_path.join(SERVER_PATH_SHEETS); - - // If the directory doesn't exist, return an empty list - if !sheets_dir.exists() { - return Ok(vec![]); - } - - let mut sheet_names = Vec::new(); - - // Iterate through all files in the sheets directory - for entry in std::fs::read_dir(sheets_dir)? { - let entry = entry?; - let path = entry.path(); - - // Check if it's a YAML file - if path.is_file() - && path - .extension() - .is_some_and(|ext| ext == SERVER_SUFFIX_SHEET_FILE_NO_DOT) - && let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) - { - // Create a new SheetName and add it to the result list - sheet_names.push(file_stem.to_string()); - } - } - - Ok(sheet_names) - } - - /// Read a sheet from its name - /// - /// If the sheet information is successfully found in the vault, - /// it will be deserialized and read as a sheet. - /// This is the only correct way to obtain a sheet instance. - pub async fn sheet<'a>(&'a self, sheet_name: &SheetName) -> Result, std::io::Error> { - let sheet_name = snake_case!(sheet_name.clone()); - - // Get the path to the sheet file - let sheet_path = Sheet::sheet_path_with_name(self, &sheet_name); - - // Ensure the sheet file exists - if !sheet_path.exists() { - // If the sheet does not exist, try to restore it from the trash - if self.restore_sheet(&sheet_name).await.is_err() { - // If restoration fails, return an error - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Sheet `{}` not found!", sheet_name), - )); - } - } - - // Read the sheet data from the file - let data = SheetData::read_from(sheet_path).await?; - - Ok(Sheet { - name: sheet_name.clone(), - data, - vault_reference: self, - }) - } - - /// Create a sheet locally and return the sheet instance - /// - /// This method creates a new sheet in the vault with the given name and holder. - /// It will verify that the member exists and that the sheet doesn't already exist - /// before creating the sheet file with default empty data. - pub async fn create_sheet<'a>( - &'a self, - sheet_name: &SheetName, - holder: &MemberId, - ) -> Result, std::io::Error> { - let sheet_name = snake_case!(sheet_name.clone()); - - // Ensure member exists - if !self.member_cfg_path(holder).exists() { - return Err(Error::new( - std::io::ErrorKind::NotFound, - format!("Member `{}` not found!", &holder), - )); - } - - // Ensure sheet does not already exist - let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); - if sheet_file_path.exists() { - return Err(Error::new( - std::io::ErrorKind::AlreadyExists, - format!("Sheet `{}` already exists!", &sheet_name), - )); - } - - // Create the sheet file - let sheet_data = SheetData { - holder: Some(holder.clone()), - mapping: HashMap::new(), - id_mapping: None, - write_count: 0, - }; - 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!( - "{}_{}.{}", - sheet_name, timestamp, SERVER_SUFFIX_SHEET_FILE_NO_DOT - ); - 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/data/src/data/vault/vault_config.rs b/data/src/data/vault/vault_config.rs new file mode 100644 index 0000000..caa8552 --- /dev/null +++ b/data/src/data/vault/vault_config.rs @@ -0,0 +1,233 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use cfg_file::ConfigFile; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::constants::{PORT, SERVER_FILE_VAULT}; +use crate::data::member::{Member, MemberId}; + +pub type VaultName = String; +pub type VaultUuid = Uuid; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum AuthMode { + /// Use asymmetric keys: both client and server need to register keys, after which they can connect + Key, + + /// Use password: the password stays on the server, and the client needs to set the password locally for connection + #[default] + Password, + + /// No authentication: generally used in a strongly secure environment, skipping verification directly + NoAuth, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum LoggerLevel { + Debug, + Trace, + + #[default] + Info, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum ServiceEnabled { + Enable, + + #[default] + Disable, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum BehaviourEnabled { + Yes, + + #[default] + No, +} + +impl Into for ServiceEnabled { + fn into(self) -> bool { + match self { + ServiceEnabled::Enable => true, + ServiceEnabled::Disable => false, + } + } +} + +impl Into for BehaviourEnabled { + fn into(self) -> bool { + match self { + BehaviourEnabled::Yes => true, + BehaviourEnabled::No => false, + } + } +} + +#[derive(Serialize, Deserialize, ConfigFile)] +#[cfg_file(path = SERVER_FILE_VAULT)] +pub struct VaultConfig { + /// Vault uuid, unique identifier for the vault + #[serde(rename = "uuid")] + vault_uuid: VaultUuid, + + /// Vault name, which can be used as the project name and generally serves as a hint + #[serde(rename = "name")] + vault_name: VaultName, + + /// Vault host ids, a list of member id representing administrator identities + #[serde(rename = "hosts")] + vault_host_list: Vec, + + /// Vault server configuration, which will be loaded when connecting to the server + #[serde(rename = "profile")] + server_config: VaultServerConfig, +} + +#[derive(Serialize, Deserialize)] +pub struct VaultServerConfig { + /// Local IP address to bind to when the server starts + #[serde(rename = "bind")] + local_bind: IpAddr, + + /// TCP port to bind to when the server starts + #[serde(rename = "port")] + port: u16, + + /// Enable logging + #[serde(rename = "logger")] + logger: Option, + + /// Logger Level + #[serde(rename = "logger_level")] + logger_level: Option, + + /// Whether to enable LAN discovery, allowing members on the same LAN to more easily find the upstream server + #[serde(rename = "lan_discovery")] + lan_discovery: Option, // TODO + + /// Authentication mode for the vault server + /// key: Use asymmetric keys for authentication + /// password: Use a password for authentication + /// noauth: No authentication required, requires a strongly secure environment + #[serde(rename = "auth_mode")] + auth_mode: Option, // TODO +} + +impl Default for VaultConfig { + fn default() -> Self { + Self { + vault_uuid: Uuid::new_v4(), + vault_name: "JustEnoughVault".to_string(), + vault_host_list: Vec::new(), + server_config: VaultServerConfig { + local_bind: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + port: PORT, + logger: Some(BehaviourEnabled::default()), + logger_level: Some(LoggerLevel::default()), + lan_discovery: Some(ServiceEnabled::default()), + auth_mode: Some(AuthMode::Key), + }, + } + } +} + +/// Vault Management +impl VaultConfig { + /// Change name of the vault. + pub fn change_name(&mut self, name: impl Into) { + self.vault_name = name.into() + } + + /// Add admin + pub fn add_admin(&mut self, member: &Member) { + let uuid = member.id(); + if !self.vault_host_list.contains(&uuid) { + self.vault_host_list.push(uuid); + } + } + + /// Remove admin + pub fn remove_admin(&mut self, member: &Member) { + let id = member.id(); + self.vault_host_list.retain(|x| x != &id); + } + + /// Get vault UUID + pub fn vault_uuid(&self) -> &VaultUuid { + &self.vault_uuid + } + + /// Set vault UUID + pub fn set_vault_uuid(&mut self, vault_uuid: VaultUuid) { + self.vault_uuid = vault_uuid; + } + + /// Get vault name + pub fn vault_name(&self) -> &VaultName { + &self.vault_name + } + + /// Set vault name + pub fn set_vault_name(&mut self, vault_name: VaultName) { + self.vault_name = vault_name; + } + + /// Get vault admin list + pub fn vault_host_list(&self) -> &Vec { + &self.vault_host_list + } + + /// Set vault admin list + pub fn set_vault_host_list(&mut self, vault_host_list: Vec) { + self.vault_host_list = vault_host_list; + } + + /// Get server config + pub fn server_config(&self) -> &VaultServerConfig { + &self.server_config + } + + /// Set server config + pub fn set_server_config(&mut self, server_config: VaultServerConfig) { + self.server_config = server_config; + } +} + +impl VaultServerConfig { + /// Get local bind IP address + pub fn local_bind(&self) -> &IpAddr { + &self.local_bind + } + + /// Get port + pub fn port(&self) -> u16 { + self.port + } + + /// Check if LAN discovery is enabled + pub fn is_lan_discovery_enabled(&self) -> bool { + self.lan_discovery.clone().unwrap_or_default().into() + } + + /// Get logger enabled status + pub fn is_logger_enabled(&self) -> bool { + self.logger.clone().unwrap_or_default().into() + } + + /// Get logger level + pub fn logger_level(&self) -> LoggerLevel { + self.logger_level.clone().unwrap_or_default() + } + + /// Get authentication mode + pub fn auth_mode(&self) -> AuthMode { + self.auth_mode.clone().unwrap_or_default() + } +} diff --git a/data/src/env.rs b/data/src/env.rs new file mode 100644 index 0000000..209c0cc --- /dev/null +++ b/data/src/env.rs @@ -0,0 +1,84 @@ +use crate::constants::*; +use std::io::{self, Error}; +use std::{env::set_current_dir, path::PathBuf}; + +/// Find the nearest vault or local workspace and correct the `current_dir` to it +pub fn correct_current_dir() -> Result<(), io::Error> { + if let Some(local_workspace) = current_local_path() { + set_current_dir(local_workspace)?; + return Ok(()); + } + if let Some(vault) = current_vault_path() { + set_current_dir(vault)?; + return Ok(()); + } + Err(Error::new( + io::ErrorKind::NotFound, + "Could not find any vault or local workspace!", + )) +} + +/// Get the nearest Vault directory from `current_dir` +pub fn current_vault_path() -> Option { + let current_dir = std::env::current_dir().ok()?; + find_vault_path(current_dir) +} + +/// Get the nearest local workspace from `current_dir` +pub fn current_local_path() -> Option { + let current_dir = std::env::current_dir().ok()?; + find_local_path(current_dir) +} + +/// Get the nearest Vault directory from the specified path +pub fn find_vault_path(path: impl Into) -> Option { + let mut current_path = path.into(); + let vault_file = SERVER_FILE_VAULT; + + loop { + let vault_toml_path = current_path.join(vault_file); + if vault_toml_path.exists() { + return Some(current_path); + } + + if let Some(parent) = current_path.parent() { + current_path = parent.to_path_buf(); + } else { + break; + } + } + + None +} + +/// Get the nearest local workspace from the specified path +pub fn find_local_path(path: impl Into) -> Option { + let mut current_path = path.into(); + let workspace_dir = CLIENT_PATH_WORKSPACE_ROOT; + + loop { + let jvc_path = current_path.join(workspace_dir); + if jvc_path.exists() { + return Some(current_path); + } + + if let Some(parent) = current_path.parent() { + current_path = parent.to_path_buf(); + } else { + break; + } + } + + None +} + +/// Get the system's document directory and join with the appropriate application name +pub fn current_cfg_dir() -> Option { + dirs::config_local_dir().map(|path| { + if cfg!(target_os = "linux") { + path.join("jvcs") + } else { + path.join("JustEnoughVCS") + } + }) +} diff --git a/data/src/lib.rs b/data/src/lib.rs index 1b41391..df2ec34 100644 --- a/data/src/lib.rs +++ b/data/src/lib.rs @@ -1,5 +1,5 @@ pub mod constants; -pub mod current; +pub mod env; #[allow(dead_code)] pub mod data; diff --git a/data/tests/src/test_local_workspace_setup_and_account_management.rs b/data/tests/src/test_local_workspace_setup_and_account_management.rs index 8fa2676..edb9295 100644 --- a/data/tests/src/test_local_workspace_setup_and_account_management.rs +++ b/data/tests/src/test_local_workspace_setup_and_account_management.rs @@ -4,7 +4,7 @@ use cfg_file::config::ConfigFile; use vcs_data::{ constants::{CLIENT_FILE_TODOLIST, CLIENT_FILE_WORKSPACE, USER_FILE_KEY, USER_FILE_MEMBER}, data::{ - local::{LocalWorkspace, config::LocalConfig}, + local::{LocalWorkspace, workspace_config::LocalConfig}, member::Member, user::UserDirectory, }, diff --git a/data/tests/src/test_sheet_creation_management_and_persistence.rs b/data/tests/src/test_sheet_creation_management_and_persistence.rs index 6683d06..adff1bd 100644 --- a/data/tests/src/test_sheet_creation_management_and_persistence.rs +++ b/data/tests/src/test_sheet_creation_management_and_persistence.rs @@ -6,7 +6,7 @@ use vcs_data::{ data::{ member::{Member, MemberId}, sheet::SheetName, - vault::{Vault, config::VaultConfig, virtual_file::VirtualFileId}, + vault::{Vault, vault_config::VaultConfig, virtual_file::VirtualFileId}, }, }; diff --git a/data/tests/src/test_sheet_share_creation_and_management.rs b/data/tests/src/test_sheet_share_creation_and_management.rs index 89891d6..5123360 100644 --- a/data/tests/src/test_sheet_share_creation_and_management.rs +++ b/data/tests/src/test_sheet_share_creation_and_management.rs @@ -8,8 +8,8 @@ use vcs_data::{ sheet::{SheetName, SheetPathBuf}, vault::{ Vault, - config::VaultConfig, - sheet_share::{Share, ShareMergeMode, SheetShareId}, + mapping_share::{Share, ShareMergeMode, SheetShareId}, + vault_config::VaultConfig, virtual_file::VirtualFileId, }, }, diff --git a/data/tests/src/test_vault_setup_and_member_register.rs b/data/tests/src/test_vault_setup_and_member_register.rs index 286a4a2..d34e48e 100644 --- a/data/tests/src/test_vault_setup_and_member_register.rs +++ b/data/tests/src/test_vault_setup_and_member_register.rs @@ -8,7 +8,7 @@ use vcs_data::{ }, data::{ member::Member, - vault::{Vault, config::VaultConfig}, + vault::{Vault, vault_config::VaultConfig}, }, }; diff --git a/data/tests/src/test_virtual_file_creation_and_update.rs b/data/tests/src/test_virtual_file_creation_and_update.rs index 2d9d393..a09f7dc 100644 --- a/data/tests/src/test_virtual_file_creation_and_update.rs +++ b/data/tests/src/test_virtual_file_creation_and_update.rs @@ -14,7 +14,7 @@ use vcs_data::{ constants::SERVER_FILE_VAULT, data::{ member::Member, - vault::{Vault, config::VaultConfig, virtual_file::VirtualFileVersionDescription}, + vault::{Vault, vault_config::VaultConfig, virtual_file::VirtualFileVersionDescription}, }, }; -- cgit