diff options
| author | 魏曹先生 <1992414357@qq.com> | 2025-09-26 10:59:31 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2025-09-26 10:59:31 +0800 |
| commit | 47e56cc4a912c5bd7d1685f49b8ab2161f58daf0 (patch) | |
| tree | 55d18d2878a1af9c00711c76a4a9888beb11f45a | |
| parent | 06b2e2b384da34e30688d1a217859c5cf68ca3bd (diff) | |
feat: add sheet management system for vaults
- Add Sheet struct with holder, inputs, and mapping functionality
- Implement SheetData with serialization support
- Add sheets.rs with vault sheet management methods:
- Load all sheets in vault
- Search for sheet names
- Read individual sheets
- Create new sheets with validation
- Delete sheets (both permanently and safely to trash)
- Restore sheets from trash
- Update data module to include sheet submodule
- Minor fixes and improvements to related modules
| -rw-r--r-- | crates/vcs/src/constants.rs | 1 | ||||
| -rw-r--r-- | crates/vcs/src/data.rs | 1 | ||||
| -rw-r--r-- | crates/vcs/src/data/local/config.rs | 2 | ||||
| -rw-r--r-- | crates/vcs/src/data/member.rs | 2 | ||||
| -rw-r--r-- | crates/vcs/src/data/sheet.rs | 109 | ||||
| -rw-r--r-- | crates/vcs/src/data/user/accounts.rs | 5 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault.rs | 3 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault/config.rs | 3 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault/member.rs | 4 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault/sheets.rs | 256 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault/virtual_file.rs | 2 |
11 files changed, 379 insertions, 9 deletions
diff --git a/crates/vcs/src/constants.rs b/crates/vcs/src/constants.rs index bc30672..05fcd1f 100644 --- a/crates/vcs/src/constants.rs +++ b/crates/vcs/src/constants.rs @@ -12,6 +12,7 @@ pub const PORT: u16 = 25331; pub const SERVER_FILE_VAULT: &str = "./vault.toml"; // crates::env::vault::vault_config // Server - Sheets +pub const REF_SHEET_NAME: &str = "ref"; pub const SERVER_PATH_SHEETS: &str = "./sheets/"; pub const SERVER_FILE_SHEET: &str = "./sheets/{sheet-name}.yaml"; diff --git a/crates/vcs/src/data.rs b/crates/vcs/src/data.rs index 63411a6..ed9383a 100644 --- a/crates/vcs/src/data.rs +++ b/crates/vcs/src/data.rs @@ -1,4 +1,5 @@ pub mod local; pub mod member; +pub mod sheet; pub mod user; pub mod vault; diff --git a/crates/vcs/src/data/local/config.rs b/crates/vcs/src/data/local/config.rs index e024569..5444047 100644 --- a/crates/vcs/src/data/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::data::vault::MemberId; +use crate::data::member::MemberId; #[derive(Serialize, Deserialize, ConfigFile)] #[cfg_file(path = CLIENT_FILE_WORKSPACE)] diff --git a/crates/vcs/src/data/member.rs b/crates/vcs/src/data/member.rs index 208c78c..b5136a1 100644 --- a/crates/vcs/src/data/member.rs +++ b/crates/vcs/src/data/member.rs @@ -4,6 +4,8 @@ use cfg_file::ConfigFile; use serde::{Deserialize, Serialize}; use string_proc::snake_case; +pub type MemberId = String; + #[derive(Debug, Eq, Clone, ConfigFile, Serialize, Deserialize)] pub struct Member { /// Member ID, the unique identifier of the member diff --git a/crates/vcs/src/data/sheet.rs b/crates/vcs/src/data/sheet.rs new file mode 100644 index 0000000..3acc8ff --- /dev/null +++ b/crates/vcs/src/data/sheet.rs @@ -0,0 +1,109 @@ +use std::{collections::HashMap, path::PathBuf}; + +use cfg_file::{ConfigFile, config::ConfigFile}; +use serde::{Deserialize, Serialize}; + +use crate::{ + constants::SERVER_FILE_SHEET, + data::{ + member::MemberId, + vault::{Vault, virtual_file::VirtualFileId}, + }, +}; + +pub type SheetName = String; +pub type SheetPathBuf = PathBuf; +pub type InputName = String; +pub type InputPackage = (InputName, Vec<(InputRaltivePathBuf, VirtualFileId)>); +pub type InputRaltivePathBuf = PathBuf; + +const SHEET_NAME: &str = "{sheet-name}"; + +pub struct Sheet<'a> { + /// The name of the current sheet + pub(crate) name: SheetName, + + /// Sheet data + pub(crate) data: SheetData, + + /// Sheet path + pub(crate) vault_reference: &'a Vault, +} + +#[derive(Default, Serialize, Deserialize, ConfigFile)] +pub struct SheetData { + /// The holder of the current sheet, who has full operation rights to the sheet mapping + pub(crate) holder: MemberId, + + /// Inputs + pub(crate) inputs: Vec<InputPackage>, + + /// Mapping of sheet paths to virtual file IDs + pub(crate) mapping: HashMap<SheetPathBuf, VirtualFileId>, +} + +impl<'a> Sheet<'a> { + /// Get the holder of this sheet + pub fn holder(&self) -> &MemberId { + &self.data.holder + } + + /// Get the inputs of this sheet + pub fn inputs(&self) -> &Vec<InputPackage> { + &self.data.inputs + } + + /// Get the mapping of this sheet + pub fn mapping(&self) -> &HashMap<SheetPathBuf, VirtualFileId> { + &self.data.mapping + } + + /// Add an input package to the sheet + pub fn add_input( + &mut self, + input_name: InputName, + files: Vec<(InputRaltivePathBuf, VirtualFileId)>, + ) { + self.data.inputs.push((input_name, files)); + } + + /// Remove an input package from the sheet + pub fn remove_input(&mut self, input_name: &InputName) -> Option<InputPackage> { + self.data + .inputs + .iter() + .position(|(name, _)| name == input_name) + .map(|pos| self.data.inputs.remove(pos)) + } + + /// Add a mapping entry to the sheet + pub fn add_mapping(&mut self, sheet_path: SheetPathBuf, virtual_file_id: VirtualFileId) { + self.data.mapping.insert(sheet_path, virtual_file_id); + } + + /// Remove a mapping entry from the sheet + pub fn remove_mapping(&mut self, sheet_path: &SheetPathBuf) -> Option<VirtualFileId> { + self.data.mapping.remove(sheet_path) + } + + /// Persist the sheet to disk + /// + /// Why not use a reference? + /// Because I don't want a second instance of the sheet to be kept in memory. + /// If needed, please deserialize and reload it. + pub async fn persist(self) -> Result<(), std::io::Error> { + SheetData::write_to(&self.data, &self.sheet_path()).await + } + + /// Get the path to the sheet file + pub fn sheet_path(&self) -> PathBuf { + Sheet::sheet_path_with_name(self.vault_reference, &self.name) + } + + /// Get the path to the sheet file with the given name + pub fn sheet_path_with_name(vault: &Vault, name: impl AsRef<str>) -> PathBuf { + vault + .vault_path() + .join(SERVER_FILE_SHEET.replace(SHEET_NAME, name.as_ref())) + } +} diff --git a/crates/vcs/src/data/user/accounts.rs b/crates/vcs/src/data/user/accounts.rs index 83ebda9..d77bc02 100644 --- a/crates/vcs/src/data/user/accounts.rs +++ b/crates/vcs/src/data/user/accounts.rs @@ -8,7 +8,10 @@ use cfg_file::config::ConfigFile; use crate::{ constants::{USER_FILE_ACCOUNTS, USER_FILE_KEY, USER_FILE_MEMBER}, - data::{member::Member, user::UserDirectory, vault::MemberId}, + data::{ + member::{Member, MemberId}, + user::UserDirectory, + }, }; const SELF_ID: &str = "{self_id}"; diff --git a/crates/vcs/src/data/vault.rs b/crates/vcs/src/data/vault.rs index 9b400bb..152836e 100644 --- a/crates/vcs/src/data/vault.rs +++ b/crates/vcs/src/data/vault.rs @@ -17,10 +17,9 @@ use crate::{ pub mod config; pub mod member; +pub mod sheets; pub mod virtual_file; -pub type MemberId = String; - pub struct Vault { config: VaultConfig, vault_path: PathBuf, diff --git a/crates/vcs/src/data/vault/config.rs b/crates/vcs/src/data/vault/config.rs index 11917de..e879325 100644 --- a/crates/vcs/src/data/vault/config.rs +++ b/crates/vcs/src/data/vault/config.rs @@ -2,8 +2,7 @@ use cfg_file::ConfigFile; use serde::{Deserialize, Serialize}; use crate::constants::SERVER_FILE_VAULT; -use crate::data::member::Member; -use crate::data::vault::MemberId; +use crate::data::member::{Member, MemberId}; #[derive(Serialize, Deserialize, ConfigFile)] #[cfg_file(path = SERVER_FILE_VAULT)] diff --git a/crates/vcs/src/data/vault/member.rs b/crates/vcs/src/data/vault/member.rs index 9482d30..aebd92d 100644 --- a/crates/vcs/src/data/vault/member.rs +++ b/crates/vcs/src/data/vault/member.rs @@ -9,8 +9,8 @@ use cfg_file::config::ConfigFile; use crate::{ constants::{SERVER_FILE_MEMBER_INFO, SERVER_FILE_MEMBER_PUB, SERVER_PATH_MEMBERS}, data::{ - member::Member, - vault::{MemberId, Vault}, + member::{Member, MemberId}, + vault::Vault, }, }; diff --git a/crates/vcs/src/data/vault/sheets.rs b/crates/vcs/src/data/vault/sheets.rs new file mode 100644 index 0000000..ede4077 --- /dev/null +++ b/crates/vcs/src/data/vault/sheets.rs @@ -0,0 +1,256 @@ +use std::{collections::HashMap, io::Error}; + +use cfg_file::config::ConfigFile; +use string_proc::snake_case; +use tokio::fs; + +use crate::{ + constants::SERVER_PATH_SHEETS, + data::{ + member::MemberId, + sheet::{Sheet, SheetData, SheetName}, + vault::Vault, + }, +}; + +impl Vault { + /// Load all sheets in the vault + /// + /// It is generally not recommended to call this function frequently. + /// Although a vault typically won't contain too many sheets, + /// if individual sheet contents are large, this operation may cause + /// significant performance bottlenecks. + pub async fn sheets<'a>(&'a self) -> Result<Vec<Sheet<'a>>, std::io::Error> { + let sheet_names = self.sheet_names()?; + let mut sheets = Vec::new(); + + for sheet_name in sheet_names { + let sheet = self.sheet(&sheet_name).await?; + sheets.push(sheet); + } + + Ok(sheets) + } + + /// Search for all sheet names in the vault + /// + /// The complexity of this operation is proportional to the number of sheets, + /// but generally there won't be too many sheets in a Vault + pub fn sheet_names(&self) -> Result<Vec<SheetName>, std::io::Error> { + // Get the sheets directory path + let sheets_dir = self.vault_path.join(SERVER_PATH_SHEETS); + + // If the directory doesn't exist, return an empty list + if !sheets_dir.exists() { + return Ok(vec![]); + } + + let mut sheet_names = Vec::new(); + + // Iterate through all files in the sheets directory + for entry in std::fs::read_dir(sheets_dir)? { + let entry = entry?; + let path = entry.path(); + + // Check if it's a YAML file + if path.is_file() && path.extension().map_or(false, |ext| ext == "yaml") { + if 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(SheetName::new()); + } + } + } + + Ok(sheet_names) + } + + /// Read a sheet from its name + /// + /// If the sheet information is successfully found in the vault, + /// it will be deserialized and read as a sheet. + /// This is the only correct way to obtain a sheet instance. + pub async fn sheet<'a>(&'a self, sheet_name: &SheetName) -> Result<Sheet<'a>, std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Get the path to the sheet file + let sheet_path = Sheet::sheet_path_with_name(self, &sheet_name); + + // Ensure the sheet file exists + if !sheet_path.exists() { + // If the sheet does not exist, try to restore it from the trash + if self.restore_sheet(&sheet_name).await.is_err() { + // If restoration fails, return an error + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Sheet `{}` not found!", sheet_name), + )); + } + } + + // Read the sheet data from the file + let data = SheetData::read_from(sheet_path).await?; + + Ok(Sheet { + name: sheet_name.clone(), + data, + vault_reference: self, + }) + } + + /// Create a sheet locally and return the sheet instance + /// + /// This method creates a new sheet in the vault with the given name and holder. + /// It will verify that the member exists and that the sheet doesn't already exist + /// before creating the sheet file with default empty data. + pub async fn create_sheet<'a>( + &'a self, + sheet_name: &SheetName, + holder: &MemberId, + ) -> Result<Sheet<'a>, std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Ensure member exists + if !self.member_cfg_path(holder).exists() { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Member `{}` not found!", &holder), + )); + } + + // Ensure sheet does not already exist + let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); + if sheet_file_path.exists() { + return Err(Error::new( + std::io::ErrorKind::AlreadyExists, + format!("Sheet `{}` already exists!", &sheet_name), + )); + } + + // Create the sheet file + let sheet_data = SheetData { + holder: sheet_name.clone(), + inputs: Vec::new(), + mapping: HashMap::new(), + }; + SheetData::write_to(&sheet_data, sheet_file_path).await?; + + Ok(Sheet { + name: sheet_name, + data: sheet_data, + vault_reference: self, + }) + } + + /// Delete the sheet file from local disk by name + /// + /// This method will remove the sheet file with the given name from the vault. + /// It will verify that the sheet exists before attempting to delete it. + /// If the sheet is successfully deleted, it will return Ok(()). + /// + /// Warning: This operation is dangerous. Deleting a sheet will cause local workspaces + /// using this sheet to become invalid. Please ensure the sheet is not currently in use + /// and will not be used in the future. + /// + /// For a safer deletion method, consider using `delete_sheet_safety`. + 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. + pub async fn delete_sheet_safely(&self, sheet_name: &SheetName) -> Result<(), std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Ensure the sheet exists + let sheet_file_path = Sheet::sheet_path_with_name(self, &sheet_name); + if !sheet_file_path.exists() { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Sheet `{}` not found!", &sheet_name), + )); + } + + // Create the trash directory + let trash_dir = self.vault_path.join(".trash"); + if !trash_dir.exists() { + fs::create_dir_all(&trash_dir).await?; + } + + // Generate a unique filename in the trash + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + let trash_file_name = format!("{}_{}.yaml", sheet_name, timestamp); + let trash_path = trash_dir.join(trash_file_name); + + // Move the sheet file to the trash + fs::rename(&sheet_file_path, &trash_path).await?; + + Ok(()) + } + + /// Restore the sheet from the trash + /// + /// Restore the specified sheet from the trash to its original location, making it accessible normally. + pub async fn restore_sheet(&self, sheet_name: &SheetName) -> Result<(), std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + // Search for matching files in the trash + let trash_dir = self.vault_path.join(".trash"); + if !trash_dir.exists() { + return Err(Error::new( + std::io::ErrorKind::NotFound, + format!("Trash directory does not exist!"), + )); + } + + 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() { + if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) { + // Check if the filename starts with the sheet name + if file_name.starts_with(&sheet_name) { + found_path = Some(path); + break; + } + } + } + } + + let trash_path = found_path.ok_or_else(|| { + Error::new( + std::io::ErrorKind::NotFound, + format!("Sheet `{}` not found in trash!", &sheet_name), + ) + })?; + + // Restore the sheet to its original location + let original_path = Sheet::sheet_path_with_name(self, &sheet_name); + fs::rename(&trash_path, &original_path).await?; + + Ok(()) + } +} diff --git a/crates/vcs/src/data/vault/virtual_file.rs b/crates/vcs/src/data/vault/virtual_file.rs index 04b5236..23e964a 100644 --- a/crates/vcs/src/data/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, }, - data::vault::{MemberId, Vault}, + data::{member::MemberId, vault::Vault}, }; pub type VirtualFileId = String; |
