summaryrefslogtreecommitdiff
path: root/crates/vcs_data/src/data/vault
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2025-10-06 04:13:32 +0800
committerGitHub <noreply@github.com>2025-10-06 04:13:32 +0800
commit4a32781c096f30cb39e16c745076e6b7537929cd (patch)
tree304075598071f55cb9dc15dc7ac8d9b8740e511e /crates/vcs_data/src/data/vault
parent57959d26c68dc1d403f527f1f8b407abe8059a28 (diff)
parent85f7c35d6c573b715c166fe7501225ecab6731ea (diff)
Merge pull request #17 from JustEnoughVCS/jvcs_dev
Jvcs dev
Diffstat (limited to 'crates/vcs_data/src/data/vault')
-rw-r--r--crates/vcs_data/src/data/vault/config.rs77
-rw-r--r--crates/vcs_data/src/data/vault/member.rs140
-rw-r--r--crates/vcs_data/src/data/vault/sheets.rs268
-rw-r--r--crates/vcs_data/src/data/vault/virtual_file.rs473
4 files changed, 958 insertions, 0 deletions
diff --git a/crates/vcs_data/src/data/vault/config.rs b/crates/vcs_data/src/data/vault/config.rs
new file mode 100644
index 0000000..6eea25a
--- /dev/null
+++ b/crates/vcs_data/src/data/vault/config.rs
@@ -0,0 +1,77 @@
+use std::net::{IpAddr, Ipv4Addr};
+
+use cfg_file::ConfigFile;
+use serde::{Deserialize, Serialize};
+
+use crate::constants::{PORT, SERVER_FILE_VAULT};
+use crate::data::member::{Member, MemberId};
+
+#[derive(Serialize, Deserialize, ConfigFile)]
+#[cfg_file(path = SERVER_FILE_VAULT)]
+pub struct VaultConfig {
+ /// Vault name, which can be used as the project name and generally serves as a hint
+ vault_name: String,
+
+ /// Vault admin id, a list of member id representing administrator identities
+ vault_admin_list: Vec<MemberId>,
+
+ /// Vault server configuration, which will be loaded when connecting to the server
+ server_config: VaultServerConfig,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct VaultServerConfig {
+ /// Local IP address to bind to when the server starts
+ local_bind: IpAddr,
+
+ /// TCP port to bind to when the server starts
+ port: u16,
+
+ /// Whether to enable LAN discovery, allowing members on the same LAN to more easily find the upstream server
+ lan_discovery: bool, // TODO
+
+ /// Authentication strength level
+ /// 0: Weakest - Anyone can claim any identity, fastest speed
+ /// 1: Basic - Any device can claim any registered identity, slightly faster
+ /// 2: Advanced - Uses asymmetric encryption, multiple devices can use key authentication to log in simultaneously, slightly slower
+ /// 3: Secure - Uses asymmetric encryption, only one device can use key for authentication at a time, much slower
+ /// Default is "Advanced", if using a lower security policy, ensure your server is only accessible by trusted devices
+ auth_strength: u8, // TODO
+}
+
+impl Default for VaultConfig {
+ fn default() -> Self {
+ Self {
+ vault_name: "JustEnoughVault".to_string(),
+ vault_admin_list: Vec::new(),
+ server_config: VaultServerConfig {
+ local_bind: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
+ port: PORT,
+ lan_discovery: false,
+ auth_strength: 2,
+ },
+ }
+ }
+}
+
+/// Vault Management
+impl VaultConfig {
+ // Change name of the vault.
+ pub fn change_name(&mut self, name: impl Into<String>) {
+ self.vault_name = name.into()
+ }
+
+ // Add admin
+ pub fn add_admin(&mut self, member: &Member) {
+ let uuid = member.id();
+ if !self.vault_admin_list.contains(&uuid) {
+ self.vault_admin_list.push(uuid);
+ }
+ }
+
+ // Remove admin
+ pub fn remove_admin(&mut self, member: &Member) {
+ let id = member.id();
+ self.vault_admin_list.retain(|x| x != &id);
+ }
+}
diff --git a/crates/vcs_data/src/data/vault/member.rs b/crates/vcs_data/src/data/vault/member.rs
new file mode 100644
index 0000000..aebd92d
--- /dev/null
+++ b/crates/vcs_data/src/data/vault/member.rs
@@ -0,0 +1,140 @@
+use std::{
+ fs,
+ io::{Error, ErrorKind},
+ path::PathBuf,
+};
+
+use cfg_file::config::ConfigFile;
+
+use crate::{
+ constants::{SERVER_FILE_MEMBER_INFO, SERVER_FILE_MEMBER_PUB, SERVER_PATH_MEMBERS},
+ data::{
+ member::{Member, MemberId},
+ vault::Vault,
+ },
+};
+
+const ID_PARAM: &str = "{member_id}";
+
+/// Member Manage
+impl Vault {
+ /// Read member from configuration file
+ pub async fn member(&self, id: &MemberId) -> Result<Member, std::io::Error> {
+ if let Some(cfg_file) = self.member_cfg(id) {
+ let member = Member::read_from(cfg_file).await?;
+ return Ok(member);
+ }
+
+ 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
+ if self.member_cfg(&member.id()).is_some() {
+ let member_cfg_path = self.member_cfg_path(&member.id());
+ Member::write_to(&member, member_cfg_path).await?;
+ return Ok(());
+ }
+
+ Err(Error::new(ErrorKind::NotFound, "Member not found!"))
+ }
+
+ /// Register a member to vault
+ pub async fn register_member_to_vault(&self, member: Member) -> Result<(), std::io::Error> {
+ // Ensure member not exist
+ if self.member_cfg(&member.id()).is_some() {
+ return Err(Error::new(
+ ErrorKind::DirectoryNotEmpty,
+ format!("Member `{}` already registered!", member.id()),
+ ));
+ }
+
+ // Wrtie config file to member dir
+ let member_cfg_path = self.member_cfg_path(&member.id());
+ Member::write_to(&member, member_cfg_path).await?;
+
+ Ok(())
+ }
+
+ /// Remove member from vault
+ pub fn remove_member_from_vault(&self, id: &MemberId) -> Result<(), std::io::Error> {
+ // Ensure member exist
+ if let Some(member_cfg_path) = self.member_cfg(id) {
+ fs::remove_file(member_cfg_path)?;
+ }
+
+ Ok(())
+ }
+
+ /// Try to get the member's configuration file to determine if the member exists
+ pub fn member_cfg(&self, id: &MemberId) -> Option<PathBuf> {
+ let cfg_file = self.member_cfg_path(id);
+ if cfg_file.exists() {
+ Some(cfg_file)
+ } else {
+ None
+ }
+ }
+
+ /// Try to get the member's public key file to determine if the member has login permission
+ pub fn member_key(&self, id: &MemberId) -> Option<PathBuf> {
+ let key_file = self.member_key_path(id);
+ if key_file.exists() {
+ Some(key_file)
+ } else {
+ None
+ }
+ }
+
+ /// 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
+ .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
+ .join(SERVER_FILE_MEMBER_PUB.replace(ID_PARAM, id.to_string().as_str()))
+ }
+}
diff --git a/crates/vcs_data/src/data/vault/sheets.rs b/crates/vcs_data/src/data/vault/sheets.rs
new file mode 100644
index 0000000..0bba4f5
--- /dev/null
+++ b/crates/vcs_data/src/data/vault/sheets.rs
@@ -0,0 +1,268 @@
+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,
+ },
+};
+
+/// Vault Sheets Management
+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().is_some_and(|ext| ext == "yaml")
+ && 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(file_stem.to_string());
+ }
+ }
+
+ 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: holder.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`.
+ ///
+ /// Note: This function is intended for server-side use only and should not be
+ /// arbitrarily called by other members to prevent unauthorized data deletion.
+ 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.
+ ///
+ /// Note: This function is intended for server-side use only and should not be
+ /// arbitrarily called by other members to prevent unauthorized data 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,
+ "Trash directory does not exist!".to_string(),
+ ));
+ }
+
+ 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()
+ && 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_data/src/data/vault/virtual_file.rs b/crates/vcs_data/src/data/vault/virtual_file.rs
new file mode 100644
index 0000000..fe83594
--- /dev/null
+++ b/crates/vcs_data/src/data/vault/virtual_file.rs
@@ -0,0 +1,473 @@
+use std::{
+ collections::HashMap,
+ io::{Error, ErrorKind},
+ path::PathBuf,
+};
+
+use cfg_file::{ConfigFile, config::ConfigFile};
+use serde::{Deserialize, Serialize};
+use string_proc::snake_case;
+use tcp_connection::instance::ConnectionInstance;
+use tokio::fs;
+use uuid::Uuid;
+
+use crate::{
+ constants::{
+ SERVER_FILE_VF_META, SERVER_FILE_VF_VERSION_INSTANCE, SERVER_PATH_VF_ROOT,
+ SERVER_PATH_VF_STORAGE, SERVER_PATH_VF_TEMP,
+ },
+ data::{member::MemberId, vault::Vault},
+};
+
+pub type VirtualFileId = String;
+pub type VirtualFileVersion = String;
+
+const VF_PREFIX: &str = "vf_";
+const ID_PARAM: &str = "{vf_id}";
+const ID_INDEX: &str = "{vf_index}";
+const VERSION_PARAM: &str = "{vf_version}";
+const TEMP_NAME: &str = "{temp_name}";
+
+pub struct VirtualFile<'a> {
+ /// Unique identifier for the virtual file
+ id: VirtualFileId,
+
+ /// Reference of Vault
+ current_vault: &'a Vault,
+}
+
+#[derive(Default, Clone, Serialize, Deserialize, ConfigFile)]
+pub struct VirtualFileMeta {
+ /// Current version of the virtual file
+ current_version: VirtualFileVersion,
+
+ /// The member who holds the edit right of the file
+ hold_member: MemberId,
+
+ /// Description of each version
+ version_description: HashMap<VirtualFileVersion, VirtualFileVersionDescription>,
+
+ /// Histories
+ histories: Vec<VirtualFileVersion>,
+}
+
+#[derive(Default, Clone, Serialize, Deserialize)]
+pub struct VirtualFileVersionDescription {
+ /// The member who created this version
+ pub creator: MemberId,
+
+ /// The description of this version
+ pub description: String,
+}
+
+impl VirtualFileVersionDescription {
+ /// Create a new version description
+ pub fn new(creator: MemberId, description: String) -> Self {
+ Self {
+ creator,
+ description,
+ }
+ }
+}
+
+/// Virtual File Operations
+impl Vault {
+ /// Generate a temporary path for receiving
+ pub fn virtual_file_temp_path(&self) -> PathBuf {
+ let random_receive_name = format!("{}", uuid::Uuid::new_v4());
+ self.vault_path
+ .join(SERVER_PATH_VF_TEMP.replace(TEMP_NAME, &random_receive_name))
+ }
+
+ /// Get the directory where virtual files are stored
+ pub fn virtual_file_storage_dir(&self) -> PathBuf {
+ self.vault_path().join(SERVER_PATH_VF_ROOT)
+ }
+
+ /// Get the directory where a specific virtual file is stored
+ pub fn virtual_file_dir(&self, id: &VirtualFileId) -> Result<PathBuf, std::io::Error> {
+ Ok(self.vault_path().join(
+ SERVER_PATH_VF_STORAGE
+ .replace(ID_PARAM, &id.to_string())
+ .replace(ID_INDEX, &Self::vf_index(id)?),
+ ))
+ }
+
+ // Generate index path of virtual file
+ fn vf_index(id: &VirtualFileId) -> Result<String, std::io::Error> {
+ // Remove VF_PREFIX if present
+ let id_str = if let Some(stripped) = id.strip_prefix(VF_PREFIX) {
+ stripped
+ } else {
+ id
+ };
+
+ // Extract the first part before the first hyphen
+ let first_part = id_str.split('-').next().ok_or_else(|| {
+ std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "Invalid virtual file ID format: no hyphen found",
+ )
+ })?;
+
+ // Ensure the first part has exactly 8 characters
+ if first_part.len() != 8 {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "Invalid virtual file ID format: first part must be 8 characters",
+ ))?;
+ }
+
+ // Split into 2-character chunks and join with path separator
+ let mut path = String::new();
+ for i in (0..first_part.len()).step_by(2) {
+ if i > 0 {
+ path.push('/');
+ }
+ path.push_str(&first_part[i..i + 2]);
+ }
+
+ Ok(path)
+ }
+
+ /// Get the directory where a specific virtual file's metadata is stored
+ pub fn virtual_file_real_path(
+ &self,
+ id: &VirtualFileId,
+ version: &VirtualFileVersion,
+ ) -> PathBuf {
+ self.vault_path().join(
+ SERVER_FILE_VF_VERSION_INSTANCE
+ .replace(ID_PARAM, &id.to_string())
+ .replace(ID_INDEX, &Self::vf_index(id).unwrap_or_default())
+ .replace(VERSION_PARAM, &version.to_string()),
+ )
+ }
+
+ /// Get the directory where a specific virtual file's metadata is stored
+ pub fn virtual_file_meta_path(&self, id: &VirtualFileId) -> PathBuf {
+ self.vault_path().join(
+ SERVER_FILE_VF_META
+ .replace(ID_PARAM, &id.to_string())
+ .replace(ID_INDEX, &Self::vf_index(id).unwrap_or_default()),
+ )
+ }
+
+ /// Get the virtual file with the given ID
+ pub fn virtual_file(&self, id: &VirtualFileId) -> Result<VirtualFile<'_>, std::io::Error> {
+ let dir = self.virtual_file_dir(id);
+ if dir?.exists() {
+ Ok(VirtualFile {
+ id: id.clone(),
+ current_vault: self,
+ })
+ } else {
+ Err(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ "Cannot found virtual file!",
+ ))
+ }
+ }
+
+ /// Get the meta data of the virtual file with the given ID
+ pub async fn virtual_file_meta(
+ &self,
+ id: &VirtualFileId,
+ ) -> Result<VirtualFileMeta, std::io::Error> {
+ let dir = self.virtual_file_meta_path(id);
+ let metadata = VirtualFileMeta::read_from(dir).await?;
+ Ok(metadata)
+ }
+
+ /// Write the meta data of the virtual file with the given ID
+ pub async fn write_virtual_file_meta(
+ &self,
+ id: &VirtualFileId,
+ meta: &VirtualFileMeta,
+ ) -> Result<(), std::io::Error> {
+ let dir = self.virtual_file_meta_path(id);
+ VirtualFileMeta::write_to(meta, dir).await?;
+ Ok(())
+ }
+
+ /// Create a virtual file from a connection instance
+ ///
+ /// It's the only way to create virtual files!
+ ///
+ /// When the target machine executes `write_file`, use this function instead of `read_file`,
+ /// and provide the member ID of the transmitting member.
+ ///
+ /// The system will automatically receive the file and
+ /// create the virtual file.
+ pub async fn create_virtual_file_from_connection(
+ &self,
+ instance: &mut ConnectionInstance,
+ member_id: &MemberId,
+ ) -> Result<VirtualFileId, std::io::Error> {
+ const FIRST_VERSION: &str = "0";
+ let receive_path = self.virtual_file_temp_path();
+ let new_id = format!("{}{}", VF_PREFIX, Uuid::new_v4());
+ let move_path = self.virtual_file_real_path(&new_id, &FIRST_VERSION.to_string());
+
+ match instance.read_file(receive_path.clone()).await {
+ Ok(_) => {
+ // Read successful, create virtual file
+ // Create default version description
+ let mut version_description =
+ HashMap::<VirtualFileVersion, VirtualFileVersionDescription>::new();
+ version_description.insert(
+ FIRST_VERSION.to_string(),
+ VirtualFileVersionDescription {
+ creator: member_id.clone(),
+ description: "Track".to_string(),
+ },
+ );
+ // Create metadata
+ let mut meta = VirtualFileMeta {
+ current_version: FIRST_VERSION.to_string(),
+ hold_member: member_id.clone(), // The holder of the newly created virtual file is the creator by default
+ version_description,
+ histories: Vec::default(),
+ };
+
+ // Add first version
+ meta.histories.push(FIRST_VERSION.to_string());
+
+ // Write metadata to file
+ VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(&new_id)).await?;
+
+ // Move temp file to virtual file directory
+ if let Some(parent) = move_path.parent()
+ && !parent.exists()
+ {
+ fs::create_dir_all(parent).await?;
+ }
+ fs::rename(receive_path, move_path).await?;
+
+ //
+
+ Ok(new_id)
+ }
+ Err(e) => {
+ // Read failed, remove temp file.
+ if receive_path.exists() {
+ fs::remove_file(receive_path).await?;
+ }
+
+ Err(Error::other(e))
+ }
+ }
+ }
+
+ /// Update a virtual file from a connection instance
+ ///
+ /// It's the only way to update virtual files!
+ /// When the target machine executes `write_file`, use this function instead of `read_file`,
+ /// and provide the member ID of the transmitting member.
+ ///
+ /// The system will automatically receive the file and
+ /// update the virtual file.
+ ///
+ /// Note: The specified member must hold the edit right of the file,
+ /// otherwise the file reception will not be allowed.
+ ///
+ /// Make sure to obtain the edit right of the file before calling this function.
+ pub async fn update_virtual_file_from_connection(
+ &self,
+ instance: &mut ConnectionInstance,
+ member: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ new_version: &VirtualFileVersion,
+ description: VirtualFileVersionDescription,
+ ) -> Result<(), std::io::Error> {
+ let new_version = snake_case!(new_version.clone());
+ let mut meta = self.virtual_file_meta(virtual_file_id).await?;
+
+ // Check if the member has edit right
+ self.check_virtual_file_edit_right(member, virtual_file_id)
+ .await?;
+
+ // Check if the new version already exists
+ if meta.version_description.contains_key(&new_version) {
+ return Err(Error::new(
+ ErrorKind::AlreadyExists,
+ format!(
+ "Version `{}` already exists for virtual file `{}`",
+ new_version, virtual_file_id
+ ),
+ ));
+ }
+
+ // Verify success
+ let receive_path = self.virtual_file_temp_path();
+ let move_path = self.virtual_file_real_path(virtual_file_id, &new_version);
+
+ match instance.read_file(receive_path.clone()).await {
+ Ok(_) => {
+ // Read success, move temp file to real path.
+ fs::rename(receive_path, move_path).await?;
+
+ // Update metadata
+ meta.current_version = new_version.clone();
+ meta.version_description
+ .insert(new_version.clone(), description);
+ meta.histories.push(new_version);
+ VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(virtual_file_id))
+ .await?;
+
+ Ok(())
+ }
+ Err(e) => {
+ // Read failed, remove temp file.
+ if receive_path.exists() {
+ fs::remove_file(receive_path).await?;
+ }
+
+ Err(Error::other(e))
+ }
+ }
+ }
+
+ /// Update virtual file from existing version
+ ///
+ /// This operation creates a new version based on the specified old version file instance.
+ /// The new version will retain the same version name as the old version, but use a different version number.
+ /// After the update, this version will be considered newer than the original version when comparing versions.
+ pub async fn update_virtual_file_from_exist_version(
+ &self,
+ member: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ old_version: &VirtualFileVersion,
+ ) -> Result<(), std::io::Error> {
+ let old_version = snake_case!(old_version.clone());
+ let mut meta = self.virtual_file_meta(virtual_file_id).await?;
+
+ // Check if the member has edit right
+ self.check_virtual_file_edit_right(member, virtual_file_id)
+ .await?;
+
+ // Ensure virtual file exist
+ let Ok(_) = self.virtual_file(virtual_file_id) else {
+ return Err(Error::new(
+ ErrorKind::NotFound,
+ format!("Virtual file `{}` not found!", virtual_file_id),
+ ));
+ };
+
+ // Ensure version exist
+ if !meta.version_exists(&old_version) {
+ return Err(Error::new(
+ ErrorKind::NotFound,
+ format!("Version `{}` not found!", old_version),
+ ));
+ }
+
+ // Ok, Create new version
+ meta.current_version = old_version.clone();
+ meta.histories.push(old_version);
+ VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(virtual_file_id)).await?;
+
+ Ok(())
+ }
+
+ /// Grant a member the edit right for a virtual file
+ /// This operation takes effect immediately upon success
+ pub async fn grant_virtual_file_edit_right(
+ &self,
+ member_id: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ ) -> Result<(), std::io::Error> {
+ let mut meta = self.virtual_file_meta(virtual_file_id).await?;
+ meta.hold_member = member_id.clone();
+ self.write_virtual_file_meta(virtual_file_id, &meta).await
+ }
+
+ /// Check if a member has the edit right for a virtual file
+ pub async fn has_virtual_file_edit_right(
+ &self,
+ member_id: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ ) -> Result<bool, std::io::Error> {
+ let meta = self.virtual_file_meta(virtual_file_id).await?;
+ Ok(meta.hold_member.eq(member_id))
+ }
+
+ /// Check if a member has the edit right for a virtual file and return Result
+ /// Returns Ok(()) if the member has edit right, otherwise returns PermissionDenied error
+ pub async fn check_virtual_file_edit_right(
+ &self,
+ member_id: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ ) -> Result<(), std::io::Error> {
+ if !self
+ .has_virtual_file_edit_right(member_id, virtual_file_id)
+ .await?
+ {
+ return Err(Error::new(
+ ErrorKind::PermissionDenied,
+ format!(
+ "Member `{}` not allowed to update virtual file `{}`",
+ member_id, virtual_file_id
+ ),
+ ));
+ }
+ Ok(())
+ }
+
+ /// Revoke the edit right for a virtual file from the current holder
+ /// This operation takes effect immediately upon success
+ pub async fn revoke_virtual_file_edit_right(
+ &self,
+ virtual_file_id: &VirtualFileId,
+ ) -> Result<(), std::io::Error> {
+ let mut meta = self.virtual_file_meta(virtual_file_id).await?;
+ meta.hold_member = String::default();
+ self.write_virtual_file_meta(virtual_file_id, &meta).await
+ }
+}
+
+impl<'a> VirtualFile<'a> {
+ /// Get id of VirtualFile
+ pub fn id(&self) -> VirtualFileId {
+ self.id.clone()
+ }
+
+ /// Read metadata of VirtualFile
+ pub async fn read_meta(&self) -> Result<VirtualFileMeta, std::io::Error> {
+ self.current_vault.virtual_file_meta(&self.id).await
+ }
+}
+
+impl VirtualFileMeta {
+ /// Get all versions of the virtual file
+ pub fn versions(&self) -> &Vec<VirtualFileVersion> {
+ &self.histories
+ }
+
+ /// Get the total number of versions for this virtual file
+ pub fn version_len(&self) -> i32 {
+ self.histories.len() as i32
+ }
+
+ /// Check if a specific version exists
+ /// Returns true if the version exists, false otherwise
+ pub fn version_exists(&self, version: &VirtualFileVersion) -> bool {
+ self.versions().iter().any(|v| v == version)
+ }
+
+ /// Get the version number (index) for a given version name
+ /// Returns None if the version doesn't exist
+ pub fn version_num(&self, version: &VirtualFileVersion) -> Option<i32> {
+ self.histories
+ .iter()
+ .rev()
+ .position(|v| v == version)
+ .map(|pos| (self.histories.len() - 1 - pos) as i32)
+ }
+
+ /// Get the version name for a given version number (index)
+ /// Returns None if the version number is out of range
+ pub fn version_name(&self, version_num: i32) -> Option<VirtualFileVersion> {
+ self.histories.get(version_num as usize).cloned()
+ }
+}