From 5416c501e61e591cea85c1f30daa53818baa5f23 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 3 Nov 2025 18:48:52 +0800 Subject: feat: Add sheet creation action - Implement make_sheet_action for creating sheets - Add sheet-related constants - Update sheet data structures --- crates/vcs_data/src/data/sheet.rs | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) (limited to 'crates/vcs_data/src/data') diff --git a/crates/vcs_data/src/data/sheet.rs b/crates/vcs_data/src/data/sheet.rs index b558c0d..ce450a6 100644 --- a/crates/vcs_data/src/data/sheet.rs +++ b/crates/vcs_data/src/data/sheet.rs @@ -51,7 +51,7 @@ pub struct Sheet<'a> { #[derive(Default, Serialize, Deserialize, ConfigFile, Clone)] pub struct SheetData { /// The holder of the current sheet, who has full operation rights to the sheet mapping - pub(crate) holder: MemberId, + pub(crate) holder: Option, /// Inputs pub(crate) inputs: Vec, @@ -66,8 +66,8 @@ impl<'a> Sheet<'a> { } /// Get the holder of this sheet - pub fn holder(&self) -> &MemberId { - &self.data.holder + pub fn holder(&self) -> Option<&MemberId> { + self.data.holder.as_ref() } /// Get the inputs of this sheet @@ -143,10 +143,11 @@ impl<'a> Sheet<'a> { /// Add (or Edit) a mapping entry to the sheet /// /// This operation performs safety checks to ensure the member has the right to add the mapping: - /// 1. If the virtual file ID doesn't exist in the vault, the mapping is added directly - /// 2. If the virtual file exists, check if the member has edit rights to the virtual file - /// 3. If member has edit rights, the mapping is not allowed to be modified and returns an error - /// 4. If member doesn't have edit rights, the mapping is allowed (member is giving up the file) + /// 1. The sheet must have a holder (member) to perform this operation + /// 2. If the virtual file ID doesn't exist in the vault, the mapping is added directly + /// 3. If the virtual file exists, check if the member has edit rights to the virtual file + /// 4. If member has edit rights, the mapping is not allowed to be modified and returns an error + /// 5. If member doesn't have edit rights, the mapping is allowed (member is giving up the file) /// /// Note: Full validation adds overhead - avoid frequent calls pub async fn add_mapping( @@ -161,10 +162,18 @@ impl<'a> Sheet<'a> { return Ok(()); } + // Check if the sheet has a holder + let Some(holder) = self.holder() else { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "This sheet has no holder", + )); + }; + // Check if the holder has edit rights to the virtual file match self .vault_reference - .has_virtual_file_edit_right(self.holder(), &virtual_file_id) + .has_virtual_file_edit_right(holder, &virtual_file_id) .await { Ok(false) => { @@ -191,9 +200,10 @@ impl<'a> Sheet<'a> { /// Remove a mapping entry from the sheet /// /// This operation performs safety checks to ensure the member has the right to remove the mapping: - /// 1. Member must NOT have edit rights to the virtual file to release it (ensuring clear ownership) - /// 2. If the virtual file doesn't exist, the mapping is removed but no ID is returned - /// 3. If member has no edit rights and the file exists, returns the removed virtual file ID + /// 1. The sheet must have a holder (member) to perform this operation + /// 2. Member must NOT have edit rights to the virtual file to release it (ensuring clear ownership) + /// 3. If the virtual file doesn't exist, the mapping is removed but no ID is returned + /// 4. If member has no edit rights and the file exists, returns the removed virtual file ID /// /// Note: Full validation adds overhead - avoid frequent calls pub async fn remove_mapping(&mut self, sheet_path: &SheetPathBuf) -> Option { @@ -212,10 +222,15 @@ impl<'a> Sheet<'a> { return None; } + // Check if the sheet has a holder + let Some(holder) = self.holder() else { + return None; + }; + // Check if the holder has edit rights to the virtual file match self .vault_reference - .has_virtual_file_edit_right(self.holder(), virtual_file_id) + .has_virtual_file_edit_right(holder, virtual_file_id) .await { Ok(false) => { -- cgit From eb80c53983e4802384b95a9adc74756c13bd726a Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 3 Nov 2025 18:48:59 +0800 Subject: feat: Implement sheet usage management - Add sheet_in_use field to LocalConfig - Implement use_sheet and exit_sheet methods - Add draft folder management for sheet switching --- crates/vcs_data/src/data/local/config.rs | 223 +++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) (limited to 'crates/vcs_data/src/data') diff --git a/crates/vcs_data/src/data/local/config.rs b/crates/vcs_data/src/data/local/config.rs index 338d01b..2fec457 100644 --- a/crates/vcs_data/src/data/local/config.rs +++ b/crates/vcs_data/src/data/local/config.rs @@ -1,12 +1,26 @@ use cfg_file::ConfigFile; +use cfg_file::config::ConfigFile; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; +use std::path::Path; +use std::path::PathBuf; +use string_proc::format_processer::FormatProcesser; +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::PORT; +use crate::current::current_local_path; +use crate::data::local::latest_info; +use crate::data::local::latest_info::LatestInfo; use crate::data::member::MemberId; +use crate::data::sheet::SheetName; use crate::data::vault::config::VaultUuid; +const SHEET_NAME: &str = "{sheet_name}"; + #[derive(Serialize, Deserialize, ConfigFile)] #[cfg_file(path = CLIENT_FILE_WORKSPACE)] pub struct LocalConfig { @@ -25,6 +39,9 @@ pub struct LocalConfig { /// If the value is None, it means not stained; /// otherwise, it contains the stain identifier (i.e., the upstream vault's unique ID) stained_uuid: Option, + + /// The name of the sheet currently in use. + sheet_in_use: Option, } impl Default for LocalConfig { @@ -36,6 +53,7 @@ impl Default for LocalConfig { )), using_account: "unknown".to_string(), stained_uuid: None, + sheet_in_use: None, } } } @@ -56,6 +74,177 @@ impl LocalConfig { self.using_account = account; } + /// 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().await else { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No latest info found", + )); + }; + + // Check if the sheet exists + if !latest_info.my_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(&sheet, &local_path); + + if draft_folder.exists() { + // Exists + // Move the contents of the draft folder to the local path + 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(&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(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + } + + // Move all files and folders (except .jv folder) to the draft folder + 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(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + + 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 + async fn move_draft_to_local( + &self, + draft_folder: &Path, + local_path: &Path, + ) -> Result<(), std::io::Error> { + let draft_entries = std::fs::read_dir(draft_folder) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + + for entry in draft_entries { + let entry = entry.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + 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| std::io::Error::new(std::io::ErrorKind::Other, e))?; + } + + // Remove the now-empty draft folder + std::fs::remove_dir(draft_folder) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + + Ok(()) + } + + /// Move contents from local path to draft folder (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 = std::fs::read_dir(local_path) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + + for entry in entries { + let entry = entry.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + 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| std::io::Error::new(std::io::ErrorKind::Other, e))?; + } + + Ok(()) + } + /// Get the currently used account pub fn current_account(&self) -> MemberId { self.using_account.clone() @@ -75,4 +264,38 @@ impl LocalConfig { 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, + sheet_name: &SheetName, + local_workspace_path: impl Into, + ) -> PathBuf { + let sheet_name_str = snake_case!(sheet_name.as_str()); + let draft_path = CLIENT_PATH_LOCAL_DRAFT.replace(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 Some(current_dir) = current_local_path() else { + return None; + }; + + Some(self.draft_folder(sheet_name, current_dir)) + } } -- cgit From dea59dad70681d3eb86f5212932a2e242e225cbb Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 3 Nov 2025 18:49:13 +0800 Subject: update: Local actions and data structures - Update local actions implementation - Improve latest info handling - Enhance vault sheets management --- crates/vcs_data/src/data/local/latest_info.rs | 10 ++++++++-- crates/vcs_data/src/data/vault/sheets.rs | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'crates/vcs_data/src/data') diff --git a/crates/vcs_data/src/data/local/latest_info.rs b/crates/vcs_data/src/data/local/latest_info.rs index 5a76277..6b116d8 100644 --- a/crates/vcs_data/src/data/local/latest_info.rs +++ b/crates/vcs_data/src/data/local/latest_info.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::{ constants::CLIENT_FILE_LATEST_INFO, data::{ - member::Member, + member::{Member, MemberId}, sheet::{SheetData, SheetName}, }, }; @@ -16,7 +16,7 @@ pub struct LatestInfo { /// My sheets, indicating which sheets I can edit pub my_sheets: Vec, /// Other sheets, indicating which sheets I can export files to (these sheets are not readable to me) - pub other_sheets: Vec, + pub other_sheets: Vec, /// Reference sheet data, indicating what files I can get from the reference sheet pub ref_sheet_content: SheetData, @@ -26,3 +26,9 @@ pub struct LatestInfo { } impl LatestInfo {} + +#[derive(Default, Serialize, Deserialize)] +pub struct SheetInfo { + pub sheet_name: SheetName, + pub holder_name: Option, +} diff --git a/crates/vcs_data/src/data/vault/sheets.rs b/crates/vcs_data/src/data/vault/sheets.rs index 0bba4f5..1407350 100644 --- a/crates/vcs_data/src/data/vault/sheets.rs +++ b/crates/vcs_data/src/data/vault/sheets.rs @@ -130,7 +130,7 @@ impl Vault { // Create the sheet file let sheet_data = SheetData { - holder: holder.clone(), + holder: Some(holder.clone()), inputs: Vec::new(), mapping: HashMap::new(), }; -- cgit From 9f61c907773ae45fad57f63f8de66ca4f72f3254 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 3 Nov 2025 19:57:55 +0800 Subject: feat: Add rollback support for file movement - Implement MovedItem struct to track file operations - Add rollback logic for draft-to-local and local-to-draft moves - Improve error handling with automatic rollback on failures --- crates/vcs_data/src/data/local/config.rs | 76 ++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 18 deletions(-) (limited to 'crates/vcs_data/src/data') diff --git a/crates/vcs_data/src/data/local/config.rs b/crates/vcs_data/src/data/local/config.rs index 2fec457..3dc5248 100644 --- a/crates/vcs_data/src/data/local/config.rs +++ b/crates/vcs_data/src/data/local/config.rs @@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::path::Path; use std::path::PathBuf; -use string_proc::format_processer::FormatProcesser; use string_proc::snake_case; use crate::constants::CLIENT_FILE_WORKSPACE; @@ -13,7 +12,6 @@ use crate::constants::CLIENT_PATH_LOCAL_DRAFT; use crate::constants::CLIENT_PATH_WORKSPACE_ROOT; use crate::constants::PORT; use crate::current::current_local_path; -use crate::data::local::latest_info; use crate::data::local::latest_info::LatestInfo; use crate::data::member::MemberId; use crate::data::sheet::SheetName; @@ -113,7 +111,7 @@ impl LocalConfig { if draft_folder.exists() { // Exists - // Move the contents of the draft folder to the local path + // Move the contents of the draft folder to the local path with rollback support self.move_draft_to_local(&draft_folder, &local_path).await?; } @@ -145,7 +143,7 @@ impl LocalConfig { .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; } - // Move all files and folders (except .jv folder) to the draft folder + // 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 @@ -187,44 +185,65 @@ impl LocalConfig { Ok(()) } - /// Move contents from draft folder to local path + /// 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 = std::fs::read_dir(draft_folder) + let draft_entries: Vec<_> = std::fs::read_dir(draft_folder) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? + .collect::, _>>() .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; - for entry in draft_entries { - let entry = entry.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + 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| std::io::Error::new(std::io::ErrorKind::Other, e))?; + 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::new(std::io::ErrorKind::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| std::io::Error::new(std::io::ErrorKind::Other, e))?; + 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::new(std::io::ErrorKind::Other, e) + })?; Ok(()) } - /// Move contents from local path to draft folder (except .jv folder) + /// 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 = std::fs::read_dir(local_path) + let entries: Vec<_> = std::fs::read_dir(local_path) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? + .collect::, _>>() .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; - for entry in entries { - let entry = entry.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + let mut moved_items: Vec = Vec::new(); + + for entry in &entries { let entry_path = entry.path(); // Skip the .jv folder @@ -238,8 +257,18 @@ impl LocalConfig { 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| std::io::Error::new(std::io::ErrorKind::Other, e))?; + 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::new(std::io::ErrorKind::Other, e) + })?; + + moved_items.push(MovedItem { + source: entry_path.clone(), + target: target_path.clone(), + }); } Ok(()) @@ -255,6 +284,11 @@ impl LocalConfig { 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); @@ -299,3 +333,9 @@ impl LocalConfig { Some(self.draft_folder(sheet_name, current_dir)) } } + +#[derive(Clone)] +struct MovedItem { + source: PathBuf, + target: PathBuf, +} -- cgit