summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2025-09-26 11:01:16 +0800
committerGitHub <noreply@github.com>2025-09-26 11:01:16 +0800
commit87448666c38fcfaa7ee381ee966fa925db7279e1 (patch)
tree55d18d2878a1af9c00711c76a4a9888beb11f45a /crates
parent3497a285c430d0390bfa074c6f9dab5c732b59a1 (diff)
parent47e56cc4a912c5bd7d1685f49b8ab2161f58daf0 (diff)
Merge pull request #7 from JustEnoughVCS/jvcs_dev
feat: add sheet management system for vaults
Diffstat (limited to 'crates')
-rw-r--r--crates/vcs/src/constants.rs1
-rw-r--r--crates/vcs/src/data.rs1
-rw-r--r--crates/vcs/src/data/local/config.rs2
-rw-r--r--crates/vcs/src/data/member.rs2
-rw-r--r--crates/vcs/src/data/sheet.rs109
-rw-r--r--crates/vcs/src/data/user/accounts.rs5
-rw-r--r--crates/vcs/src/data/vault.rs3
-rw-r--r--crates/vcs/src/data/vault/config.rs3
-rw-r--r--crates/vcs/src/data/vault/member.rs4
-rw-r--r--crates/vcs/src/data/vault/sheets.rs256
-rw-r--r--crates/vcs/src/data/vault/virtual_file.rs2
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;