summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock23
-rw-r--r--crates/vcs_data/Cargo.toml3
-rw-r--r--crates/vcs_data/src/constants.rs2
-rw-r--r--crates/vcs_data/src/data/sheet.rs5
-rw-r--r--crates/vcs_data/src/data/vault.rs1
-rw-r--r--crates/vcs_data/src/data/vault/sheet_share.rs414
-rw-r--r--crates/vcs_data/vcs_data_test/src/lib.rs3
-rw-r--r--crates/vcs_data/vcs_data_test/src/test_sheet_share_creation_and_management.rs636
8 files changed, 1085 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index cbb0ab1..17cd6a5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -953,12 +953,22 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
- "rand_chacha",
+ "rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand"
version = "0.10.0-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ec474812b9de55111b29da8a1559f1718ef3dc20fa36f031f1b5d9e3836ad6c"
@@ -978,6 +988,16 @@ dependencies = [
]
[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
+]
+
+[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1534,6 +1554,7 @@ dependencies = [
"chrono",
"data_struct",
"dirs",
+ "rand 0.9.2",
"serde",
"sha1_hash",
"string_proc",
diff --git a/crates/vcs_data/Cargo.toml b/crates/vcs_data/Cargo.toml
index d1f7e94..1971b6a 100644
--- a/crates/vcs_data/Cargo.toml
+++ b/crates/vcs_data/Cargo.toml
@@ -16,6 +16,9 @@ string_proc = { path = "../utils/string_proc" }
action_system = { path = "../system_action" }
vcs_docs = { path = "../vcs_docs" }
+# Random
+rand = "0.9.2"
+
# Identity
uuid = { version = "1.18.1", features = ["v4", "serde"] }
whoami = "1.6.1"
diff --git a/crates/vcs_data/src/constants.rs b/crates/vcs_data/src/constants.rs
index 0963114..6437073 100644
--- a/crates/vcs_data/src/constants.rs
+++ b/crates/vcs_data/src/constants.rs
@@ -19,7 +19,7 @@ pub const SERVER_SUFFIX_SHEET_FILE: &str = ".json";
pub const SERVER_SUFFIX_SHEET_FILE_NO_DOT: &str = "json";
pub const REF_SHEET_NAME: &str = "ref";
pub const SERVER_PATH_SHEETS: &str = "./sheets/";
-pub const SERVER_PATH_SHARES: &str = "./sheets/shares/";
+pub const SERVER_PATH_SHARES: &str = "./sheets/shares/{sheet_name}/";
pub const SERVER_FILE_SHEET: &str = "./sheets/{sheet_name}.json";
pub const SERVER_FILE_SHEET_SHARE: &str = "./sheets/shares/{sheet_name}/{share_id}.json";
diff --git a/crates/vcs_data/src/data/sheet.rs b/crates/vcs_data/src/data/sheet.rs
index f55689c..0a52e26 100644
--- a/crates/vcs_data/src/data/sheet.rs
+++ b/crates/vcs_data/src/data/sheet.rs
@@ -266,4 +266,9 @@ impl SheetData {
pub fn id_mapping(&self) -> &Option<HashMap<VirtualFileId, SheetPathBuf>> {
&self.id_mapping
}
+
+ /// Get the muttable id_mapping of this sheet data
+ pub fn id_mapping_mut(&mut self) -> &mut Option<HashMap<VirtualFileId, SheetPathBuf>> {
+ &mut self.id_mapping
+ }
}
diff --git a/crates/vcs_data/src/data/vault.rs b/crates/vcs_data/src/data/vault.rs
index 15be1e9..595997a 100644
--- a/crates/vcs_data/src/data/vault.rs
+++ b/crates/vcs_data/src/data/vault.rs
@@ -15,6 +15,7 @@ use crate::{
pub mod config;
pub mod member;
pub mod service;
+pub mod sheet_share;
pub mod sheets;
pub mod virtual_file;
diff --git a/crates/vcs_data/src/data/vault/sheet_share.rs b/crates/vcs_data/src/data/vault/sheet_share.rs
new file mode 100644
index 0000000..703d935
--- /dev/null
+++ b/crates/vcs_data/src/data/vault/sheet_share.rs
@@ -0,0 +1,414 @@
+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::{
+ SERVER_FILE_SHEET_SHARE, SERVER_PATH_SHARES, SERVER_SUFFIX_SHEET_FILE,
+ SERVER_SUFFIX_SHEET_FILE_NO_DOT,
+ },
+ data::{
+ member::MemberId,
+ sheet::{Sheet, SheetMappingMetadata, SheetName, SheetPathBuf},
+ vault::Vault,
+ },
+};
+
+pub type SheetShareId = String;
+
+const SHEET_NAME: &str = "{sheet_name}";
+const SHARE_ID: &str = "{share_id}";
+
+#[derive(Default, Serialize, Deserialize, ConfigFile, Clone, Debug)]
+pub struct Share {
+ /// Sharer: the member who created this share item
+ pub sharer: MemberId,
+
+ /// Description of the share item
+ pub description: String,
+
+ /// Metadata path
+ #[serde(skip)]
+ pub path: Option<PathBuf>,
+
+ /// From: which sheet the member exported the file from
+ pub from_sheet: SheetName,
+
+ /// Mappings: the sheet mappings contained in the share item
+ pub mappings: HashMap<SheetPathBuf, SheetMappingMetadata>,
+}
+
+#[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,
+}
+
+#[derive(Default, Serialize, Deserialize, ConfigFile, Clone)]
+pub struct ShareMergeConflict {
+ /// Duplicate mappings exist
+ pub duplicate_mapping: Vec<PathBuf>,
+
+ /// Duplicate files exist
+ pub duplicate_file: Vec<PathBuf>,
+}
+
+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 = snake_case!(share_id.clone());
+
+ // Format the path to remove "./" prefix and normalize it
+ let path_str = SERVER_FILE_SHEET_SHARE
+ .replace(SHEET_NAME, &sheet_name)
+ .replace(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<PathBuf> {
+ let sheet_name = snake_case!(sheet_name.clone());
+ let shares_dir = self
+ .vault_path()
+ .join(SERVER_PATH_SHARES.replace(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_FILE_NO_DOT)
+ {
+ result.push(path);
+ }
+ }
+ }
+ result
+ }
+}
+
+impl<'a> Sheet<'a> {
+ /// Get the shares of a sheet
+ pub async fn get_shares(&self) -> Result<Vec<Share>, 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<Share, std::io::Error> {
+ 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 precheck = self.precheck(&copy_share);
+
+ match share_merge_mode {
+ // Safe mode: conflicts are not allowed
+ ShareMergeMode::Safe => {
+ // Conflicts found
+ if !precheck.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 precheck.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 precheck.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 precheck.duplicate_mapping {
+ copy_share.mappings.remove(&path);
+ }
+ for path in precheck.duplicate_file {
+ copy_share.mappings.remove(&path);
+ }
+ }
+ }
+
+ // 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<PathBuf>,
+ sharer: &MemberId,
+ description: String,
+ ) -> Result<Share, std::io::Error> {
+ 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, SERVER_SUFFIX_SHEET_FILE
+ )
+ }
+
+ /// 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/crates/vcs_data/vcs_data_test/src/lib.rs b/crates/vcs_data/vcs_data_test/src/lib.rs
index 8ad03e1..ced2d3d 100644
--- a/crates/vcs_data/vcs_data_test/src/lib.rs
+++ b/crates/vcs_data/vcs_data_test/src/lib.rs
@@ -14,6 +14,9 @@ pub mod test_local_workspace_setup_and_account_management;
#[cfg(test)]
pub mod test_sheet_creation_management_and_persistence;
+#[cfg(test)]
+pub mod test_sheet_share_creation_and_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_data/vcs_data_test/src/test_sheet_share_creation_and_management.rs b/crates/vcs_data/vcs_data_test/src/test_sheet_share_creation_and_management.rs
new file mode 100644
index 0000000..d5ccbc2
--- /dev/null
+++ b/crates/vcs_data/vcs_data_test/src/test_sheet_share_creation_and_management.rs
@@ -0,0 +1,636 @@
+use std::io::Error;
+
+use cfg_file::config::ConfigFile;
+use vcs_data::{
+ constants::{SERVER_FILE_VAULT, SERVER_SUFFIX_SHEET_FILE},
+ data::{
+ member::{Member, MemberId},
+ sheet::{SheetName, SheetPathBuf},
+ vault::{
+ Vault,
+ config::VaultConfig,
+ sheet_share::{Share, ShareMergeMode, SheetShareId},
+ virtual_file::VirtualFileId,
+ },
+ },
+};
+
+use crate::get_test_dir;
+
+#[tokio::test]
+async fn test_share_creation_and_retrieval() -> Result<(), std::io::Error> {
+ let dir = get_test_dir("share_creation").await?;
+
+ // Setup vault
+ Vault::setup_vault(dir.clone(), "TestVault").await?;
+
+ // Get vault
+ let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?;
+ let Some(vault) = Vault::init(config, &dir) else {
+ return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!"));
+ };
+
+ // Add members
+ let sharer_id: MemberId = "sharer_member".to_string();
+ let target_member_id: MemberId = "target_member".to_string();
+
+ vault
+ .register_member_to_vault(Member::new(&sharer_id))
+ .await?;
+ vault
+ .register_member_to_vault(Member::new(&target_member_id))
+ .await?;
+
+ // Create source sheet for sharer
+ let source_sheet_name: SheetName = "source_sheet".to_string();
+ let _source_sheet = vault.create_sheet(&source_sheet_name, &sharer_id).await?;
+
+ // Create target sheet for target member
+ let target_sheet_name: SheetName = "target_sheet".to_string();
+ let _target_sheet = vault
+ .create_sheet(&target_sheet_name, &target_member_id)
+ .await?;
+
+ // Add mappings to source sheet
+ let mut source_sheet = vault.sheet(&source_sheet_name).await?;
+
+ let main_rs_path = SheetPathBuf::from("src/main.rs");
+ let lib_rs_path = SheetPathBuf::from("src/lib.rs");
+ let main_rs_id = VirtualFileId::from("main_rs_id_1");
+ let lib_rs_id = VirtualFileId::from("lib_rs_id_1");
+
+ source_sheet
+ .add_mapping(
+ main_rs_path.clone(),
+ main_rs_id.clone(),
+ "1.0.0".to_string(),
+ )
+ .await?;
+ source_sheet
+ .add_mapping(lib_rs_path.clone(), lib_rs_id.clone(), "1.0.0".to_string())
+ .await?;
+
+ // Persist source sheet
+ source_sheet.persist().await?;
+
+ // Test 1: Share mappings from source sheet to target sheet
+ let description = "Test share of main.rs and lib.rs".to_string();
+ // Need to get the sheet again after persist
+ let source_sheet = vault.sheet(&source_sheet_name).await?;
+
+ source_sheet
+ .share_mappings(
+ &target_sheet_name,
+ vec![main_rs_path.clone(), lib_rs_path.clone()],
+ &sharer_id,
+ description.clone(),
+ )
+ .await?;
+
+ // Test 2: Get shares from target sheet
+ let target_sheet = vault.sheet(&target_sheet_name).await?;
+
+ let shares = target_sheet.get_shares().await?;
+
+ assert_eq!(shares.len(), 1, "Expected 1 share, found {}", shares.len());
+ let share = &shares[0];
+
+ assert_eq!(share.sharer, sharer_id);
+ assert_eq!(share.description, description);
+ assert_eq!(share.from_sheet, source_sheet_name);
+ assert_eq!(share.mappings.len(), 2);
+ assert!(share.mappings.contains_key(&main_rs_path));
+ assert!(share.mappings.contains_key(&lib_rs_path));
+ assert!(share.path.is_some());
+
+ // Test 3: Get specific share by ID
+ let share_id = Share::gen_share_id(&sharer_id);
+ let _specific_share = target_sheet.get_share(&share_id).await;
+
+ // Note: The share ID might not match exactly due to random generation,
+ // but we can verify the share exists by checking the shares list
+ assert!(shares.iter().any(|s| s.sharer == sharer_id));
+
+ // Clean up
+ vault.remove_member_from_vault(&sharer_id)?;
+ vault.remove_member_from_vault(&target_member_id)?;
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn test_share_merge_modes() -> Result<(), std::io::Error> {
+ let dir = get_test_dir("share_merge_modes").await?;
+
+ // Setup vault
+ Vault::setup_vault(dir.clone(), "TestVault").await?;
+
+ // Get vault
+ let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?;
+ let Some(vault) = Vault::init(config, &dir) else {
+ return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!"));
+ };
+
+ // Add members
+ let sharer_id: MemberId = "sharer".to_string();
+ let target_member_id: MemberId = "target".to_string();
+
+ vault
+ .register_member_to_vault(Member::new(&sharer_id))
+ .await?;
+ vault
+ .register_member_to_vault(Member::new(&target_member_id))
+ .await?;
+
+ // Create source and target sheets
+ let source_sheet_name: SheetName = "source".to_string();
+ let target_sheet_name: SheetName = "target".to_string();
+
+ let _source_sheet = vault.create_sheet(&source_sheet_name, &sharer_id).await?;
+ let _target_sheet = vault
+ .create_sheet(&target_sheet_name, &target_member_id)
+ .await?;
+
+ // Add mappings to source sheet
+ let mut source_sheet = vault.sheet(&source_sheet_name).await?;
+
+ let file1_path = SheetPathBuf::from("src/file1.rs");
+ let file2_path = SheetPathBuf::from("src/file2.rs");
+ let file1_id = VirtualFileId::from("file1_id_1");
+ let file2_id = VirtualFileId::from("file2_id_1");
+
+ source_sheet
+ .add_mapping(file1_path.clone(), file1_id.clone(), "1.0.0".to_string())
+ .await?;
+ source_sheet
+ .add_mapping(file2_path.clone(), file2_id.clone(), "1.0.0".to_string())
+ .await?;
+
+ source_sheet.persist().await?;
+
+ // Share mappings
+ // Need to get the sheet again after persist
+ let source_sheet = vault.sheet(&source_sheet_name).await?;
+ source_sheet
+ .share_mappings(
+ &target_sheet_name,
+ vec![file1_path.clone(), file2_path.clone()],
+ &sharer_id,
+ "Test share".to_string(),
+ )
+ .await?;
+
+ // Get the share
+ let target_sheet = vault.sheet(&target_sheet_name).await?;
+ let shares = target_sheet.get_shares().await?;
+ assert_eq!(shares.len(), 1);
+ let share = shares[0].clone();
+
+ // Test 4: Safe mode merge (should succeed with no conflicts)
+ let result = target_sheet
+ .merge_share(share.clone(), ShareMergeMode::Safe)
+ .await;
+
+ assert!(
+ result.is_ok(),
+ "Safe mode should succeed with no conflicts "
+ );
+
+ // Verify mappings were added to target sheet
+ let updated_target_sheet = vault.sheet(&target_sheet_name).await?;
+ assert_eq!(updated_target_sheet.mapping().len(), 2);
+ assert!(updated_target_sheet.mapping().contains_key(&file1_path));
+ assert!(updated_target_sheet.mapping().contains_key(&file2_path));
+
+ // Clean up
+ vault.remove_member_from_vault(&sharer_id)?;
+ vault.remove_member_from_vault(&target_member_id)?;
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn test_share_merge_conflicts() -> Result<(), std::io::Error> {
+ let dir = get_test_dir("share_conflicts").await?;
+
+ // Setup vault
+ Vault::setup_vault(dir.clone(), "TestVault").await?;
+
+ // Get vault
+ let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?;
+ let Some(vault) = Vault::init(config, &dir) else {
+ return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!"));
+ };
+
+ // Add members
+ let sharer_id: MemberId = "sharer".to_string();
+ let target_member_id: MemberId = "target".to_string();
+
+ vault
+ .register_member_to_vault(Member::new(&sharer_id))
+ .await?;
+ vault
+ .register_member_to_vault(Member::new(&target_member_id))
+ .await?;
+
+ // Create source and target sheets
+ let source_sheet_name: SheetName = "source".to_string();
+ let target_sheet_name: SheetName = "target".to_string();
+
+ let _source_sheet = vault.create_sheet(&source_sheet_name, &sharer_id).await?;
+ let _target_sheet = vault
+ .create_sheet(&target_sheet_name, &target_member_id)
+ .await?;
+
+ // Add conflicting mappings to both sheets
+ let mut source_sheet = vault.sheet(&source_sheet_name).await?;
+ let mut target_sheet_mut = vault.sheet(&target_sheet_name).await?;
+
+ let conflicting_path = SheetPathBuf::from("src/conflicting.rs");
+ let source_file_id = VirtualFileId::from("source_file_id_1");
+ let target_file_id = VirtualFileId::from("target_file_id_1");
+
+ // Add same path with different IDs to both sheets (conflict)
+ source_sheet
+ .add_mapping(
+ conflicting_path.clone(),
+ source_file_id.clone(),
+ "1.0.0".to_string(),
+ )
+ .await?;
+
+ target_sheet_mut
+ .add_mapping(
+ conflicting_path.clone(),
+ target_file_id.clone(),
+ "1.0.0".to_string(),
+ )
+ .await?;
+
+ source_sheet.persist().await?;
+ target_sheet_mut.persist().await?;
+
+ // Share the conflicting mapping
+ // Need to get the sheet again after persist
+ let source_sheet = vault.sheet(&source_sheet_name).await?;
+ source_sheet
+ .share_mappings(
+ &target_sheet_name,
+ vec![conflicting_path.clone()],
+ &sharer_id,
+ "Conflicting share".to_string(),
+ )
+ .await?;
+
+ // Get the share
+ let target_sheet = vault.sheet(&target_sheet_name).await?;
+ let shares = target_sheet.get_shares().await?;
+ assert_eq!(shares.len(), 1);
+ let share = shares[0].clone();
+
+ // Test 5: Safe mode merge with conflict (should fail)
+ let target_sheet_clone = vault.sheet(&target_sheet_name).await?;
+ let result = target_sheet_clone
+ .merge_share(share.clone(), ShareMergeMode::Safe)
+ .await;
+
+ assert!(result.is_err(), "Safe mode should fail with conflicts");
+
+ // Test 6: Overwrite mode merge with conflict (should succeed)
+ let target_sheet_clone = vault.sheet(&target_sheet_name).await?;
+ let result = target_sheet_clone
+ .merge_share(share.clone(), ShareMergeMode::Overwrite)
+ .await;
+
+ assert!(
+ result.is_ok(),
+ "Overwrite mode should succeed with conflicts"
+ );
+
+ // Verify the mapping was overwritten
+ let updated_target_sheet = vault.sheet(&target_sheet_name).await?;
+ let mapping = updated_target_sheet.mapping().get(&conflicting_path);
+ assert!(mapping.is_some());
+ assert_eq!(mapping.unwrap().id, source_file_id); // Should be source's ID, not target's
+
+ // Clean up
+ vault.remove_member_from_vault(&sharer_id)?;
+ vault.remove_member_from_vault(&target_member_id)?;
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn test_share_skip_mode() -> Result<(), std::io::Error> {
+ let dir = get_test_dir("share_skip_mode").await?;
+
+ // Setup vault
+ Vault::setup_vault(dir.clone(), "TestVault").await?;
+
+ // Get vault
+ let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?;
+ let Some(vault) = Vault::init(config, &dir) else {
+ return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!"));
+ };
+
+ // Add members
+ let sharer_id: MemberId = "sharer".to_string();
+ let target_member_id: MemberId = "target".to_string();
+
+ vault
+ .register_member_to_vault(Member::new(&sharer_id))
+ .await?;
+ vault
+ .register_member_to_vault(Member::new(&target_member_id))
+ .await?;
+
+ // Create source and target sheets
+ let source_sheet_name: SheetName = "source".to_string();
+ let target_sheet_name: SheetName = "target".to_string();
+
+ let _source_sheet = vault.create_sheet(&source_sheet_name, &sharer_id).await?;
+ let _target_sheet = vault
+ .create_sheet(&target_sheet_name, &target_member_id)
+ .await?;
+
+ // Add mappings to both sheets
+ let mut source_sheet = vault.sheet(&source_sheet_name).await?;
+ let mut target_sheet_mut = vault.sheet(&target_sheet_name).await?;
+
+ let conflicting_path = SheetPathBuf::from("src/conflicting.rs");
+ let non_conflicting_path = SheetPathBuf::from("src/non_conflicting.rs");
+
+ let source_file_id = VirtualFileId::from("source_file_id_2");
+ let target_file_id = VirtualFileId::from("target_file_id_2");
+ let non_conflicting_id = VirtualFileId::from("non_conflicting_id_1");
+
+ // Add conflicting mapping to both sheets
+ source_sheet
+ .add_mapping(
+ conflicting_path.clone(),
+ source_file_id.clone(),
+ "1.0.0".to_string(),
+ )
+ .await?;
+
+ target_sheet_mut
+ .add_mapping(
+ conflicting_path.clone(),
+ target_file_id.clone(),
+ "1.0.0".to_string(),
+ )
+ .await?;
+
+ // Add non-conflicting mapping only to source
+ source_sheet
+ .add_mapping(
+ non_conflicting_path.clone(),
+ non_conflicting_id.clone(),
+ "1.0.0".to_string(),
+ )
+ .await?;
+
+ source_sheet.persist().await?;
+ target_sheet_mut.persist().await?;
+
+ // Share both mappings
+ // Need to get the sheet again after persist
+ let source_sheet = vault.sheet(&source_sheet_name).await?;
+ source_sheet
+ .share_mappings(
+ &target_sheet_name,
+ vec![conflicting_path.clone(), non_conflicting_path.clone()],
+ &sharer_id,
+ "Mixed share".to_string(),
+ )
+ .await?;
+
+ // Get the share
+ let target_sheet = vault.sheet(&target_sheet_name).await?;
+ let shares = target_sheet.get_shares().await?;
+ assert_eq!(shares.len(), 1);
+ let share = shares[0].clone();
+
+ // Test 7: Skip mode merge with conflict (should skip conflicting, add non-conflicting)
+ let result = target_sheet
+ .merge_share(share.clone(), ShareMergeMode::Skip)
+ .await;
+
+ assert!(result.is_ok(), "Skip mode should succeed");
+
+ // Verify only non-conflicting mapping was added
+ let updated_target_sheet = vault.sheet(&target_sheet_name).await?;
+
+ // Conflicting mapping should still have target's ID
+ let conflicting_mapping = updated_target_sheet.mapping().get(&conflicting_path);
+ assert!(conflicting_mapping.is_some());
+ assert_eq!(conflicting_mapping.unwrap().id, target_file_id);
+
+ // Non-conflicting mapping should be added
+ let non_conflicting_mapping = updated_target_sheet.mapping().get(&non_conflicting_path);
+ assert!(non_conflicting_mapping.is_some());
+ assert_eq!(non_conflicting_mapping.unwrap().id, non_conflicting_id);
+
+ // Clean up
+ vault.remove_member_from_vault(&sharer_id)?;
+ vault.remove_member_from_vault(&target_member_id)?;
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn test_share_removal() -> Result<(), std::io::Error> {
+ let dir = get_test_dir("share_removal").await?;
+
+ // Setup vault
+ Vault::setup_vault(dir.clone(), "TestVault").await?;
+
+ // Get vault
+ let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?;
+ let Some(vault) = Vault::init(config, &dir) else {
+ return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!"));
+ };
+
+ // Add members
+ let sharer_id: MemberId = "sharer".to_string();
+ let target_member_id: MemberId = "target".to_string();
+
+ vault
+ .register_member_to_vault(Member::new(&sharer_id))
+ .await?;
+ vault
+ .register_member_to_vault(Member::new(&target_member_id))
+ .await?;
+
+ // Create source and target sheets
+ let source_sheet_name: SheetName = "source".to_string();
+ let target_sheet_name: SheetName = "target".to_string();
+
+ let _source_sheet = vault.create_sheet(&source_sheet_name, &sharer_id).await?;
+ let _target_sheet = vault
+ .create_sheet(&target_sheet_name, &target_member_id)
+ .await?;
+
+ // Add mapping to source sheet
+ let mut source_sheet = vault.sheet(&source_sheet_name).await?;
+
+ let file_path = SheetPathBuf::from("src/file.rs");
+ let file_id = VirtualFileId::from("file_id_1");
+
+ source_sheet
+ .add_mapping(file_path.clone(), file_id.clone(), "1.0.0".to_string())
+ .await?;
+
+ source_sheet.persist().await?;
+
+ // Need to get the sheet again after persist
+ let source_sheet = vault.sheet(&source_sheet_name).await?;
+ // Share mapping
+ source_sheet
+ .share_mappings(
+ &target_sheet_name,
+ vec![file_path.clone()],
+ &sharer_id,
+ "Test share for removal".to_string(),
+ )
+ .await?;
+
+ // Get the share
+ let target_sheet = vault.sheet(&target_sheet_name).await?;
+ let shares = target_sheet.get_shares().await?;
+ assert_eq!(shares.len(), 1);
+ let share = shares[0].clone();
+
+ // Test 8: Remove share
+ let result = share.remove().await;
+
+ // Check if removal succeeded or failed gracefully
+ match result {
+ Ok(_) => {
+ // Share was successfully removed
+ let shares_after_removal = target_sheet.get_shares().await?;
+ assert_eq!(shares_after_removal.len(), 0);
+ }
+ Err((returned_share, _error)) => {
+ // Share removal failed, but we got the share backZ
+ // Error message may vary, just check that we got an error
+ // The share should be returned in the error
+ assert_eq!(returned_share.sharer, sharer_id);
+ }
+ }
+
+ // Clean up
+ vault.remove_member_from_vault(&sharer_id)?;
+ vault.remove_member_from_vault(&target_member_id)?;
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn test_share_error_conditions() -> Result<(), std::io::Error> {
+ let dir = get_test_dir("share_errors").await?;
+
+ // Setup vault
+ Vault::setup_vault(dir.clone(), "TestVault").await?;
+
+ // Get vault
+ let config = VaultConfig::read_from(dir.join(SERVER_FILE_VAULT)).await?;
+ let Some(vault) = Vault::init(config, &dir) else {
+ return Err(Error::new(std::io::ErrorKind::NotFound, "Vault not found!"));
+ };
+
+ // Add member
+ let sharer_id: MemberId = "sharer".to_string();
+ vault
+ .register_member_to_vault(Member::new(&sharer_id))
+ .await?;
+
+ // Create source sheet
+ let source_sheet_name: SheetName = "source".to_string();
+ let _source_sheet = vault.create_sheet(&source_sheet_name, &sharer_id).await?;
+
+ // Add mapping to source sheet
+ let mut source_sheet = vault.sheet(&source_sheet_name).await?;
+
+ let file_path = SheetPathBuf::from("src/file.rs");
+ let file_id = VirtualFileId::from("file_id_2");
+
+ source_sheet
+ .add_mapping(file_path.clone(), file_id.clone(), "1.0.0".to_string())
+ .await?;
+
+ source_sheet.persist().await?;
+
+ // Test 9: Share to non-existent sheet should fail
+ let non_existent_sheet: SheetName = "non_existent".to_string();
+ // Need to get the sheet again after persist
+ let source_sheet = vault.sheet(&source_sheet_name).await?;
+ let result = source_sheet
+ .share_mappings(
+ &non_existent_sheet,
+ vec![file_path.clone()],
+ &sharer_id,
+ "Test".to_string(),
+ )
+ .await;
+
+ assert!(result.is_err());
+
+ // Test 10: Share non-existent mapping should fail
+ let target_sheet_name: SheetName = "target".to_string();
+ let _target_sheet = vault.create_sheet(&target_sheet_name, &sharer_id).await?;
+
+ let non_existent_path = SheetPathBuf::from("src/non_existent.rs");
+ let result = source_sheet
+ .share_mappings(
+ &target_sheet_name,
+ vec![non_existent_path],
+ &sharer_id,
+ "Test".to_string(),
+ )
+ .await;
+
+ assert!(result.is_err());
+
+ // Test 11: Merge non-existent share should fail
+ let target_sheet = vault.sheet(&target_sheet_name).await?;
+ let non_existent_share_id: SheetShareId = "non_existent_share".to_string();
+ let result = target_sheet
+ .merge_share_by_id(&non_existent_share_id, ShareMergeMode::Safe)
+ .await;
+
+ assert!(result.is_err());
+
+ // Clean up
+ vault.remove_member_from_vault(&sharer_id)?;
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn test_share_id_generation() -> Result<(), std::io::Error> {
+ // Test 12: Share ID generation
+ let sharer_id: MemberId = "test_sharer".to_string();
+
+ // Generate multiple IDs to ensure they're different
+ let id1 = Share::gen_share_id(&sharer_id);
+ let id2 = Share::gen_share_id(&sharer_id);
+ let id3 = Share::gen_share_id(&sharer_id);
+
+ // IDs should be different due to random component
+ assert_ne!(id1, id2);
+ assert_ne!(id1, id3);
+ assert_ne!(id2, id3);
+
+ // IDs should contain sharer name and file suffix
+ assert!(id1.contains("test_sharer"));
+ assert!(id1.ends_with(SERVER_SUFFIX_SHEET_FILE));
+
+ assert!(id2.contains("test_sharer"));
+ assert!(id2.ends_with(SERVER_SUFFIX_SHEET_FILE));
+
+ assert!(id3.contains("test_sharer"));
+ assert!(id3.ends_with(SERVER_SUFFIX_SHEET_FILE));
+
+ Ok(())
+}