diff options
| author | 魏曹先生 <1992414357@qq.com> | 2025-09-25 17:22:05 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-09-25 17:22:05 +0800 |
| commit | 3497a285c430d0390bfa074c6f9dab5c732b59a1 (patch) | |
| tree | 243626035d251cf5d19deee9845ebe6af2a6582a | |
| parent | 647cc441eece20218d7387f37d94042e88042057 (diff) | |
| parent | 06b2e2b384da34e30688d1a217859c5cf68ca3bd (diff) | |
Merge pull request #6 from JustEnoughVCS/jvcs_dev
Jvcs dev
18 files changed, 493 insertions, 20 deletions
diff --git a/crates/vcs/src/constants.rs b/crates/vcs/src/constants.rs index a55aef5..bc30672 100644 --- a/crates/vcs/src/constants.rs +++ b/crates/vcs/src/constants.rs @@ -46,5 +46,5 @@ pub const CLIENT_FILE_README: &str = "./README.md"; // User - Verify (Documents path) pub const USER_FILE_ACCOUNTS: &str = "./accounts/"; -pub const USER_FILE_KEY_PUB: &str = "./accounts/{self_id}_private.pem"; +pub const USER_FILE_KEY: &str = "./accounts/{self_id}_private.pem"; pub const USER_FILE_MEMBER: &str = "./accounts/{self_id}.toml"; diff --git a/crates/vcs/src/workspace.rs b/crates/vcs/src/data.rs index 63411a6..63411a6 100644 --- a/crates/vcs/src/workspace.rs +++ b/crates/vcs/src/data.rs diff --git a/crates/vcs/src/workspace/local.rs b/crates/vcs/src/data/local.rs index 8399b6d..1c99832 100644 --- a/crates/vcs/src/workspace/local.rs +++ b/crates/vcs/src/data/local.rs @@ -6,7 +6,7 @@ use tokio::fs; use crate::{ constants::{CLIENT_FILE_README, CLIENT_FILE_WORKSPACE}, current::{current_local_path, find_local_path}, - workspace::local::config::LocalConfig, + data::local::config::LocalConfig, }; pub mod config; diff --git a/crates/vcs/src/workspace/local/config.rs b/crates/vcs/src/data/local/config.rs index d641880..e024569 100644 --- a/crates/vcs/src/workspace/local/config.rs +++ b/crates/vcs/src/data/local/config.rs @@ -4,7 +4,7 @@ use std::net::SocketAddr; use crate::constants::CLIENT_FILE_WORKSPACE; use crate::constants::PORT; -use crate::workspace::vault::MemberId; +use crate::data::vault::MemberId; #[derive(Serialize, Deserialize, ConfigFile)] #[cfg_file(path = CLIENT_FILE_WORKSPACE)] diff --git a/crates/vcs/src/workspace/member.rs b/crates/vcs/src/data/member.rs index 208c78c..208c78c 100644 --- a/crates/vcs/src/workspace/member.rs +++ b/crates/vcs/src/data/member.rs diff --git a/crates/vcs/src/data/user.rs b/crates/vcs/src/data/user.rs new file mode 100644 index 0000000..0abd098 --- /dev/null +++ b/crates/vcs/src/data/user.rs @@ -0,0 +1,28 @@ +use crate::current::current_doc_dir; +use std::path::PathBuf; + +pub mod accounts; + +pub struct UserDirectory { + local_path: PathBuf, +} + +impl UserDirectory { + /// Create a user ditectory struct from the current system's document directory + pub fn current_doc_dir() -> Option<Self> { + Some(UserDirectory { + local_path: current_doc_dir()?, + }) + } + + /// Create a user directory struct from a specified directory path + /// Returns None if the directory does not exist + pub fn from_path<P: Into<PathBuf>>(path: P) -> Option<Self> { + let local_path = path.into(); + if local_path.exists() { + Some(UserDirectory { local_path }) + } else { + None + } + } +} diff --git a/crates/vcs/src/data/user/accounts.rs b/crates/vcs/src/data/user/accounts.rs new file mode 100644 index 0000000..83ebda9 --- /dev/null +++ b/crates/vcs/src/data/user/accounts.rs @@ -0,0 +1,161 @@ +use std::{ + fs, + io::{Error, ErrorKind}, + path::PathBuf, +}; + +use cfg_file::config::ConfigFile; + +use crate::{ + constants::{USER_FILE_ACCOUNTS, USER_FILE_KEY, USER_FILE_MEMBER}, + data::{member::Member, user::UserDirectory, vault::MemberId}, +}; + +const SELF_ID: &str = "{self_id}"; + +/// Account Management +impl UserDirectory { + /// Read account from configuration file + pub async fn account(&self, id: &MemberId) -> Result<Member, std::io::Error> { + if let Some(cfg_file) = self.account_cfg(id) { + let member = Member::read_from(cfg_file).await?; + return Ok(member); + } + + Err(Error::new(ErrorKind::NotFound, "Account not found!")) + } + + /// List all account IDs in the user directory + pub fn account_ids(&self) -> Result<Vec<MemberId>, std::io::Error> { + let accounts_path = self + .local_path + .join(USER_FILE_ACCOUNTS.replace(SELF_ID, "")); + + if !accounts_path.exists() { + return Ok(Vec::new()); + } + + let mut account_ids = Vec::new(); + + for entry in fs::read_dir(accounts_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() + && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) + && path.extension().and_then(|s| s.to_str()) == Some("toml") + { + // Remove the "_private" suffix from key files if present + let account_id = file_name.replace("_private", ""); + account_ids.push(account_id); + } + } + + Ok(account_ids) + } + + /// Get all accounts + /// This method will read and deserialize account information, please pay attention to performance issues + pub async fn accounts(&self) -> Result<Vec<Member>, std::io::Error> { + let mut accounts = Vec::new(); + + for account_id in self.account_ids()? { + if let Ok(account) = self.account(&account_id).await { + accounts.push(account); + } + } + + Ok(accounts) + } + + /// Update account info + pub async fn update_account(&self, member: Member) -> Result<(), std::io::Error> { + // Ensure account exist + if self.account_cfg(&member.id()).is_some() { + let account_cfg_path = self.account_cfg_path(&member.id()); + Member::write_to(&member, account_cfg_path).await?; + return Ok(()); + } + + Err(Error::new(ErrorKind::NotFound, "Account not found!")) + } + + /// Register an account to user directory + pub async fn register_account(&self, member: Member) -> Result<(), std::io::Error> { + // Ensure account not exist + if self.account_cfg(&member.id()).is_some() { + return Err(Error::new( + ErrorKind::DirectoryNotEmpty, + format!("Account `{}` already registered!", member.id()), + )); + } + + // Ensure accounts directory exists + let accounts_dir = self + .local_path + .join(USER_FILE_ACCOUNTS.replace(SELF_ID, "")); + if !accounts_dir.exists() { + fs::create_dir_all(&accounts_dir)?; + } + + // Write config file to accounts dir + let account_cfg_path = self.account_cfg_path(&member.id()); + Member::write_to(&member, account_cfg_path).await?; + + Ok(()) + } + + /// Remove account from user directory + pub fn remove_account(&self, id: &MemberId) -> Result<(), std::io::Error> { + // Remove config file if exists + if let Some(account_cfg_path) = self.account_cfg(id) { + fs::remove_file(account_cfg_path)?; + } + + // Remove private key file if exists + if let Some(private_key_path) = self.account_private_key(id) + && private_key_path.exists() + { + fs::remove_file(private_key_path)?; + } + + Ok(()) + } + + /// Try to get the account's configuration file to determine if the account exists + pub fn account_cfg(&self, id: &MemberId) -> Option<PathBuf> { + let cfg_file = self.account_cfg_path(id); + if cfg_file.exists() { + Some(cfg_file) + } else { + None + } + } + + /// Try to get the account's private key file to determine if the account has a private key + pub fn account_private_key(&self, id: &MemberId) -> Option<PathBuf> { + let key_file = self.account_private_key_path(id); + if key_file.exists() { + Some(key_file) + } else { + None + } + } + + /// Check if account has private key + pub fn has_private_key(&self, id: &MemberId) -> bool { + self.account_private_key(id).is_some() + } + + /// Get the account's configuration file path, but do not check if the file exists + pub fn account_cfg_path(&self, id: &MemberId) -> PathBuf { + self.local_path + .join(USER_FILE_MEMBER.replace(SELF_ID, id.to_string().as_str())) + } + + /// Get the account's private key file path, but do not check if the file exists + pub fn account_private_key_path(&self, id: &MemberId) -> PathBuf { + self.local_path + .join(USER_FILE_KEY.replace(SELF_ID, id.to_string().as_str())) + } +} diff --git a/crates/vcs/src/workspace/vault.rs b/crates/vcs/src/data/vault.rs index 7f52c9c..9b400bb 100644 --- a/crates/vcs/src/workspace/vault.rs +++ b/crates/vcs/src/data/vault.rs @@ -12,7 +12,7 @@ use crate::{ SERVER_PATH_SHEETS, SERVER_PATH_VF_ROOT, }, current::{current_vault_path, find_vault_path}, - workspace::vault::config::VaultConfig, + data::vault::config::VaultConfig, }; pub mod config; diff --git a/crates/vcs/src/workspace/vault/config.rs b/crates/vcs/src/data/vault/config.rs index 5414e4d..11917de 100644 --- a/crates/vcs/src/workspace/vault/config.rs +++ b/crates/vcs/src/data/vault/config.rs @@ -2,8 +2,8 @@ use cfg_file::ConfigFile; use serde::{Deserialize, Serialize}; use crate::constants::SERVER_FILE_VAULT; -use crate::workspace::member::Member; -use crate::workspace::vault::MemberId; +use crate::data::member::Member; +use crate::data::vault::MemberId; #[derive(Serialize, Deserialize, ConfigFile)] #[cfg_file(path = SERVER_FILE_VAULT)] diff --git a/crates/vcs/src/workspace/vault/member.rs b/crates/vcs/src/data/vault/member.rs index 2d00081..9482d30 100644 --- a/crates/vcs/src/workspace/vault/member.rs +++ b/crates/vcs/src/data/vault/member.rs @@ -7,8 +7,8 @@ use std::{ use cfg_file::config::ConfigFile; use crate::{ - constants::{SERVER_FILE_MEMBER_INFO, SERVER_FILE_MEMBER_PUB}, - workspace::{ + constants::{SERVER_FILE_MEMBER_INFO, SERVER_FILE_MEMBER_PUB, SERVER_PATH_MEMBERS}, + data::{ member::Member, vault::{MemberId, Vault}, }, @@ -28,6 +28,45 @@ impl Vault { Err(Error::new(ErrorKind::NotFound, "Member not found!")) } + /// List all member IDs in the vault + pub fn member_ids(&self) -> Result<Vec<MemberId>, std::io::Error> { + let members_path = self.vault_path.join(SERVER_PATH_MEMBERS); + + if !members_path.exists() { + return Ok(Vec::new()); + } + + let mut member_ids = Vec::new(); + + for entry in fs::read_dir(members_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() + && let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) + && path.extension().and_then(|s| s.to_str()) == Some("toml") + { + member_ids.push(file_name.to_string()); + } + } + + Ok(member_ids) + } + + /// Get all members + /// This method will read and deserialize member information, please pay attention to performance issues + pub async fn members(&self) -> Result<Vec<Member>, std::io::Error> { + let mut members = Vec::new(); + + for member_id in self.member_ids()? { + if let Ok(member) = self.member(&member_id).await { + members.push(member); + } + } + + Ok(members) + } + /// Update member info pub async fn update_member(&self, member: Member) -> Result<(), std::io::Error> { // Ensure member exist @@ -89,17 +128,13 @@ impl Vault { /// 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 + 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 + self.vault_path .join(SERVER_FILE_MEMBER_PUB.replace(ID_PARAM, id.to_string().as_str())) } } diff --git a/crates/vcs/src/workspace/vault/virtual_file.rs b/crates/vcs/src/data/vault/virtual_file.rs index c83f700..04b5236 100644 --- a/crates/vcs/src/workspace/vault/virtual_file.rs +++ b/crates/vcs/src/data/vault/virtual_file.rs @@ -16,7 +16,7 @@ use crate::{ SERVER_FILE_VF_META, SERVER_FILE_VF_VERSION_INSTANCE, SERVER_PATH_VF_ROOT, SERVER_PATH_VF_STORAGE, SERVER_PATH_VF_TEMP, }, - workspace::vault::{MemberId, Vault}, + data::vault::{MemberId, Vault}, }; pub type VirtualFileId = String; diff --git a/crates/vcs/src/lib.rs b/crates/vcs/src/lib.rs index 15a315f..1b41391 100644 --- a/crates/vcs/src/lib.rs +++ b/crates/vcs/src/lib.rs @@ -2,4 +2,4 @@ pub mod constants; pub mod current; #[allow(dead_code)] -pub mod workspace; +pub mod data; diff --git a/crates/vcs/src/workspace/user.rs b/crates/vcs/src/workspace/user.rs deleted file mode 100644 index 9bb4894..0000000 --- a/crates/vcs/src/workspace/user.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod accounts; diff --git a/crates/vcs/src/workspace/user/accounts.rs b/crates/vcs/src/workspace/user/accounts.rs deleted file mode 100644 index 8b13789..0000000 --- a/crates/vcs/src/workspace/user/accounts.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/vcs/vcs_test/src/lib.rs b/crates/vcs/vcs_test/src/lib.rs index 357ea3f..d9e6f94 100644 --- a/crates/vcs/vcs_test/src/lib.rs +++ b/crates/vcs/vcs_test/src/lib.rs @@ -8,6 +8,9 @@ pub mod test_vault_setup_and_member_register; #[cfg(test)] pub mod test_virtual_file_creation_and_update; +#[cfg(test)] +pub mod test_local_workspace_setup_and_account_management; + pub async fn get_test_dir(area: &str) -> Result<PathBuf, std::io::Error> { let dir = current_dir()?.join(".temp").join("test").join(area); if !dir.exists() { diff --git a/crates/vcs/vcs_test/src/test_local_workspace_setup_and_account_management.rs b/crates/vcs/vcs_test/src/test_local_workspace_setup_and_account_management.rs new file mode 100644 index 0000000..df766f7 --- /dev/null +++ b/crates/vcs/vcs_test/src/test_local_workspace_setup_and_account_management.rs @@ -0,0 +1,248 @@ +use std::io::Error; + +use cfg_file::config::ConfigFile; +use vcs::{ + constants::{CLIENT_FILE_README, CLIENT_FILE_WORKSPACE, USER_FILE_KEY, USER_FILE_MEMBER}, + data::{ + local::{LocalWorkspace, config::LocalConfig}, + member::Member, + user::UserDirectory, + }, +}; + +use crate::get_test_dir; + +#[tokio::test] +async fn test_local_workspace_setup_and_account_management() -> Result<(), std::io::Error> { + let dir = get_test_dir("local_workspace_account_management").await?; + + // Setup local workspace + LocalWorkspace::setup_local_workspace(dir.clone()).await?; + + // Check if the following files are created in `dir`: + // Files: CLIENT_FILE_WORKSPACE, CLIENT_FILE_README + assert!(dir.join(CLIENT_FILE_WORKSPACE).exists()); + assert!(dir.join(CLIENT_FILE_README).exists()); + + // Get local workspace + let config = LocalConfig::read_from(dir.join(CLIENT_FILE_WORKSPACE)).await?; + let Some(_local_workspace) = LocalWorkspace::init(config, &dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "Local workspace not found!", + )); + }; + + // Create user directory from workspace path + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Test account registration + let member_id = "test_account"; + let member = Member::new(member_id); + + // Register account + user_directory.register_account(member.clone()).await?; + + // Check if the account config file exists + assert!( + dir.join(USER_FILE_MEMBER.replace("{self_id}", member_id)) + .exists() + ); + + // Test account retrieval + let retrieved_member = user_directory.account(&member_id.to_string()).await?; + assert_eq!(retrieved_member.id(), member.id()); + + // Test account IDs listing + let account_ids = user_directory.account_ids()?; + assert!(account_ids.contains(&member_id.to_string())); + + // Test accounts listing + let accounts = user_directory.accounts().await?; + assert_eq!(accounts.len(), 1); + assert_eq!(accounts[0].id(), member.id()); + + // Test account existence check + assert!(user_directory.account_cfg(&member_id.to_string()).is_some()); + + // Test private key check (should be false initially) + assert!(!user_directory.has_private_key(&member_id.to_string())); + + // Test account update + let mut updated_member = member.clone(); + updated_member.set_metadata("email", "test@example.com"); + user_directory + .update_account(updated_member.clone()) + .await?; + + // Verify update + let updated_retrieved = user_directory.account(&member_id.to_string()).await?; + assert_eq!( + updated_retrieved.metadata("email"), + Some(&"test@example.com".to_string()) + ); + + // Test account removal + user_directory.remove_account(&member_id.to_string())?; + + // Check if the account config file no longer exists + assert!( + !dir.join(USER_FILE_MEMBER.replace("{self_id}", member_id)) + .exists() + ); + + // Check if account is no longer in the list + let account_ids_after_removal = user_directory.account_ids()?; + assert!(!account_ids_after_removal.contains(&member_id.to_string())); + + Ok(()) +} + +#[tokio::test] +async fn test_account_private_key_management() -> Result<(), std::io::Error> { + let dir = get_test_dir("account_private_key_management").await?; + + // Create user directory + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Register account + let member_id = "test_account_with_key"; + let member = Member::new(member_id); + user_directory.register_account(member).await?; + + // Create a dummy private key file for testing + let private_key_path = dir.join(USER_FILE_KEY.replace("{self_id}", member_id)); + std::fs::create_dir_all(private_key_path.parent().unwrap())?; + std::fs::write(&private_key_path, "dummy_private_key_content")?; + + // Test private key existence check + assert!(user_directory.has_private_key(&member_id.to_string())); + + // Test private key path retrieval + assert!( + user_directory + .account_private_key(&member_id.to_string()) + .is_some() + ); + + // Remove account (should also remove private key) + user_directory.remove_account(&member_id.to_string())?; + + // Check if private key file is also removed + assert!(!private_key_path.exists()); + + Ok(()) +} + +#[tokio::test] +async fn test_multiple_account_management() -> Result<(), std::io::Error> { + let dir = get_test_dir("multiple_account_management").await?; + + // Create user directory + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Register multiple accounts + let account_names = vec!["alice", "bob", "charlie"]; + + for name in &account_names { + user_directory.register_account(Member::new(*name)).await?; + } + + // Test account IDs listing + let account_ids = user_directory.account_ids()?; + assert_eq!(account_ids.len(), 3); + + for name in &account_names { + assert!(account_ids.contains(&name.to_string())); + } + + // Test accounts listing + let accounts = user_directory.accounts().await?; + assert_eq!(accounts.len(), 3); + + // Remove one account + user_directory.remove_account(&"bob".to_string())?; + + // Verify removal + let account_ids_after_removal = user_directory.account_ids()?; + assert_eq!(account_ids_after_removal.len(), 2); + assert!(!account_ids_after_removal.contains(&"bob".to_string())); + assert!(account_ids_after_removal.contains(&"alice".to_string())); + assert!(account_ids_after_removal.contains(&"charlie".to_string())); + + Ok(()) +} + +#[tokio::test] +async fn test_account_registration_duplicate_prevention() -> Result<(), std::io::Error> { + let dir = get_test_dir("account_duplicate_prevention").await?; + + // Create user directory + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Register account + let member_id = "duplicate_test"; + user_directory + .register_account(Member::new(member_id)) + .await?; + + // Try to register same account again - should fail + let result = user_directory + .register_account(Member::new(member_id)) + .await; + assert!(result.is_err()); + + Ok(()) +} + +#[tokio::test] +async fn test_nonexistent_account_operations() -> Result<(), std::io::Error> { + let dir = get_test_dir("nonexistent_account_operations").await?; + + // Create user directory + let Some(user_directory) = UserDirectory::from_path(&dir) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "User directory not found!", + )); + }; + + // Try to read non-existent account - should fail + let result = user_directory.account(&"nonexistent".to_string()).await; + assert!(result.is_err()); + + // Try to update non-existent account - should fail + let result = user_directory + .update_account(Member::new("nonexistent")) + .await; + assert!(result.is_err()); + + // Try to remove non-existent account - should succeed (idempotent) + let result = user_directory.remove_account(&"nonexistent".to_string()); + assert!(result.is_ok()); + + // Check private key for non-existent account - should be false + assert!(!user_directory.has_private_key(&"nonexistent".to_string())); + + Ok(()) +} diff --git a/crates/vcs/vcs_test/src/test_vault_setup_and_member_register.rs b/crates/vcs/vcs_test/src/test_vault_setup_and_member_register.rs index ced027d..6a30cf7 100644 --- a/crates/vcs/vcs_test/src/test_vault_setup_and_member_register.rs +++ b/crates/vcs/vcs_test/src/test_vault_setup_and_member_register.rs @@ -6,7 +6,7 @@ use vcs::{ SERVER_FILE_MEMBER_INFO, SERVER_FILE_README, SERVER_FILE_VAULT, SERVER_PATH_MEMBER_PUB, SERVER_PATH_MEMBERS, SERVER_PATH_SHEETS, SERVER_PATH_VF_ROOT, }, - workspace::{ + data::{ member::Member, vault::{Vault, config::VaultConfig}, }, diff --git a/crates/vcs/vcs_test/src/test_virtual_file_creation_and_update.rs b/crates/vcs/vcs_test/src/test_virtual_file_creation_and_update.rs index bfcf817..598e7be 100644 --- a/crates/vcs/vcs_test/src/test_virtual_file_creation_and_update.rs +++ b/crates/vcs/vcs_test/src/test_virtual_file_creation_and_update.rs @@ -12,7 +12,7 @@ use tokio::{ }; use vcs::{ constants::SERVER_FILE_VAULT, - workspace::{ + data::{ member::Member, vault::{Vault, config::VaultConfig, virtual_file::VirtualFileVersionDescription}, }, |
