summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2025-11-03 20:01:02 +0800
committerGitHub <noreply@github.com>2025-11-03 20:01:02 +0800
commitf58fcaed4d7da4aa23e8500fb4720836378c3440 (patch)
tree0fd0dae1bdc1602a82395b2c11660f80c95f1b20
parentffb5805291343ba1cd4bb4f38788d9ce6e3e2ba6 (diff)
parentcf2402fff1632fd673a1865d83cefc80b75ed85f (diff)
Merge pull request #34 from JustEnoughVCS/jvcs_dev_actions
Jvcs dev actions
-rw-r--r--crates/utils/tcp_connection/src/instance_challenge.rs16
-rw-r--r--crates/vcs_actions/src/actions/local_actions.rs45
-rw-r--r--crates/vcs_actions/src/actions/sheet_actions.rs79
-rw-r--r--crates/vcs_actions/src/registry/client_registry.rs12
-rw-r--r--crates/vcs_actions/src/registry/server_registry.rs11
-rw-r--r--crates/vcs_data/src/constants.rs4
-rw-r--r--crates/vcs_data/src/data/local/config.rs263
-rw-r--r--crates/vcs_data/src/data/local/latest_info.rs10
-rw-r--r--crates/vcs_data/src/data/sheet.rs39
-rw-r--r--crates/vcs_data/src/data/vault/sheets.rs2
-rw-r--r--crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs18
11 files changed, 465 insertions, 34 deletions
diff --git a/crates/utils/tcp_connection/src/instance_challenge.rs b/crates/utils/tcp_connection/src/instance_challenge.rs
index c4ea6a8..3a7f6a3 100644
--- a/crates/utils/tcp_connection/src/instance_challenge.rs
+++ b/crates/utils/tcp_connection/src/instance_challenge.rs
@@ -132,7 +132,21 @@ impl ConnectionInstance {
self.stream.read_exact(&mut challenge).await?;
// Load private key
- let private_key_pem = tokio::fs::read_to_string(&private_key_file).await?;
+ let private_key_pem = tokio::fs::read_to_string(&private_key_file)
+ .await
+ .map_err(|e| {
+ TcpTargetError::NotFound(format!(
+ "Read private key \"{}\" failed: \"{}\"",
+ private_key_file
+ .as_ref()
+ .display()
+ .to_string()
+ .split("/")
+ .last()
+ .unwrap_or("UNKNOWN"),
+ e
+ ))
+ })?;
// Sign the challenge with supported key types
let signature = if let Ok(rsa_key) = RsaPrivateKey::from_pkcs1_pem(&private_key_pem) {
diff --git a/crates/vcs_actions/src/actions/local_actions.rs b/crates/vcs_actions/src/actions/local_actions.rs
index 87eafb8..59dd972 100644
--- a/crates/vcs_actions/src/actions/local_actions.rs
+++ b/crates/vcs_actions/src/actions/local_actions.rs
@@ -2,11 +2,14 @@ use std::net::SocketAddr;
use action_system::{action::ActionContext, macros::action_gen};
use cfg_file::config::ConfigFile;
-use log::{info, warn};
+use log::info;
use serde::{Deserialize, Serialize};
use tcp_connection::error::TcpTargetError;
use vcs_data::data::{
- local::{config::LocalConfig, latest_info::LatestInfo},
+ local::{
+ config::LocalConfig,
+ latest_info::{LatestInfo, SheetInfo},
+ },
vault::config::VaultUuid,
};
@@ -18,10 +21,15 @@ use crate::actions::{
pub enum SetUpstreamVaultActionResult {
// Success
DirectedAndStained,
+ Redirected,
// Fail
AlreadyStained,
AuthorizeFailed(String),
+ RedirectFailed(String),
+ SameUpstream,
+
+ Done,
}
#[action_gen]
@@ -44,6 +52,7 @@ pub async fn set_upstream_vault_action(
.await
.write(*vault.config().vault_uuid())
.await?;
+ return Ok(SetUpstreamVaultActionResult::Done);
}
if ctx.is_proc_on_local() {
@@ -69,7 +78,27 @@ pub async fn set_upstream_vault_action(
info!("Workspace stained!");
return Ok(SetUpstreamVaultActionResult::DirectedAndStained);
} else {
- warn!("Workspace already stained!");
+ // Local workspace is already stained, redirecting
+ let Some(stained_uuid) = mut_local_config.stained_uuid() else {
+ return Ok(SetUpstreamVaultActionResult::RedirectFailed(
+ "Stained uuid not found".to_string(),
+ ));
+ };
+ let local_upstream = mut_local_config.upstream_addr();
+
+ // Address changed, but same UUID.
+ if vault_uuid == stained_uuid {
+ if local_upstream != upstream {
+ // Set the upstream address
+ mut_local_config.set_vault_addr(upstream);
+
+ // Store the updated config
+ LocalConfig::write(&mut_local_config).await?;
+ return Ok(SetUpstreamVaultActionResult::Redirected);
+ } else {
+ return Ok(SetUpstreamVaultActionResult::SameUpstream);
+ }
+ }
return Ok(SetUpstreamVaultActionResult::AlreadyStained);
}
}
@@ -108,10 +137,16 @@ pub async fn update_to_latest_info_action(
let mut member_visible = Vec::new();
for sheet in vault.sheets().await? {
- if sheet.holder() == &member_id {
+ if sheet.holder().is_some() && sheet.holder().unwrap() == &member_id {
member_owned.push(sheet.name().clone());
} else {
- member_visible.push(sheet.name().clone());
+ member_visible.push(SheetInfo {
+ sheet_name: sheet.name().clone(),
+ holder_name: match sheet.holder() {
+ Some(holder) => Some(holder.clone()),
+ None => None,
+ },
+ });
}
}
diff --git a/crates/vcs_actions/src/actions/sheet_actions.rs b/crates/vcs_actions/src/actions/sheet_actions.rs
index e69de29..b6ea51d 100644
--- a/crates/vcs_actions/src/actions/sheet_actions.rs
+++ b/crates/vcs_actions/src/actions/sheet_actions.rs
@@ -0,0 +1,79 @@
+use action_system::{action::ActionContext, macros::action_gen};
+use serde::{Deserialize, Serialize};
+use tcp_connection::error::TcpTargetError;
+use vcs_data::data::sheet::SheetName;
+
+use crate::actions::{auth_member, check_connection_instance, try_get_vault};
+
+#[derive(Default, Serialize, Deserialize)]
+pub enum MakeSheetActionResult {
+ Success,
+
+ // Fail
+ AuthorizeFailed(String),
+ SheetAlreadyExists,
+ SheetCreationFailed(String),
+
+ #[default]
+ Unknown,
+}
+
+/// Build a sheet with context
+#[action_gen]
+pub async fn make_sheet_action(
+ ctx: ActionContext,
+ sheet_name: SheetName,
+) -> Result<MakeSheetActionResult, TcpTargetError> {
+ let instance = check_connection_instance(&ctx)?;
+
+ // Auth Member
+ let member_id = match auth_member(&ctx, instance).await {
+ Ok(id) => id,
+ Err(e) => return Ok(MakeSheetActionResult::AuthorizeFailed(e.to_string())),
+ };
+
+ if ctx.is_proc_on_remote() {
+ let vault = try_get_vault(&ctx)?;
+
+ // Check if the sheet already exists
+ if vault.sheet(&sheet_name).await.is_ok() {
+ instance
+ .lock()
+ .await
+ .write(MakeSheetActionResult::SheetAlreadyExists)
+ .await?;
+ return Ok(MakeSheetActionResult::SheetAlreadyExists);
+ } else {
+ // Create the sheet
+ match vault.create_sheet(&sheet_name, &member_id).await {
+ Ok(_) => {
+ instance
+ .lock()
+ .await
+ .write(MakeSheetActionResult::Success)
+ .await?;
+ return Ok(MakeSheetActionResult::Success);
+ }
+ Err(e) => {
+ instance
+ .lock()
+ .await
+ .write(MakeSheetActionResult::SheetCreationFailed(e.to_string()))
+ .await?;
+ return Ok(MakeSheetActionResult::SheetCreationFailed(e.to_string()));
+ }
+ }
+ }
+ }
+
+ if ctx.is_proc_on_local() {
+ let result = instance
+ .lock()
+ .await
+ .read::<MakeSheetActionResult>()
+ .await?;
+ return Ok(result);
+ }
+
+ Err(TcpTargetError::NoResult("No result.".to_string()))
+}
diff --git a/crates/vcs_actions/src/registry/client_registry.rs b/crates/vcs_actions/src/registry/client_registry.rs
index c7d6eb9..a0b87a6 100644
--- a/crates/vcs_actions/src/registry/client_registry.rs
+++ b/crates/vcs_actions/src/registry/client_registry.rs
@@ -9,16 +9,24 @@ use vcs_data::data::{
};
use crate::{
- actions::local_actions::{
- register_set_upstream_vault_action, register_update_to_latest_info_action,
+ actions::{
+ local_actions::{
+ register_set_upstream_vault_action, register_update_to_latest_info_action,
+ },
+ sheet_actions::register_make_sheet_action,
},
connection::protocol::RemoteActionInvoke,
};
fn register_actions(pool: &mut ActionPool) {
// Pool register here
+
+ // Local Actions
register_set_upstream_vault_action(pool);
register_update_to_latest_info_action(pool);
+
+ // Sheet Actions
+ register_make_sheet_action(pool);
}
pub fn client_action_pool() -> ActionPool {
diff --git a/crates/vcs_actions/src/registry/server_registry.rs b/crates/vcs_actions/src/registry/server_registry.rs
index 3b6ab17..eade391 100644
--- a/crates/vcs_actions/src/registry/server_registry.rs
+++ b/crates/vcs_actions/src/registry/server_registry.rs
@@ -1,12 +1,19 @@
use action_system::action_pool::ActionPool;
-use crate::actions::local_actions::{
- register_set_upstream_vault_action, register_update_to_latest_info_action,
+use crate::actions::{
+ local_actions::{register_set_upstream_vault_action, register_update_to_latest_info_action},
+ sheet_actions::register_make_sheet_action,
};
pub fn server_action_pool() -> ActionPool {
let mut pool = ActionPool::new();
+
+ // Local Actions
register_set_upstream_vault_action(&mut pool);
register_update_to_latest_info_action(&mut pool);
+
+ // Sheet Actions
+ register_make_sheet_action(&mut pool);
+
pool
}
diff --git a/crates/vcs_data/src/constants.rs b/crates/vcs_data/src/constants.rs
index cd6eaa3..1d17927 100644
--- a/crates/vcs_data/src/constants.rs
+++ b/crates/vcs_data/src/constants.rs
@@ -42,6 +42,7 @@ pub const SERVER_FILE_README: &str = "./README.md";
// Client
pub const CLIENT_PATH_WORKSPACE_ROOT: &str = "./.jv/";
+pub const CLIENT_FOLDER_WORKSPACE_ROOT_NAME: &str = ".jv";
// Client - Workspace (Main)
pub const CLIENT_FILE_WORKSPACE: &str = "./.jv/workspace.toml";
@@ -52,6 +53,9 @@ pub const CLIENT_FILE_LATEST_INFO: &str = "./.jv/latest.json";
// Client - Sheets
pub const CLIENT_FILE_SHEET_COPY: &str = "./.jv/sheets/{sheet_name}.copy.json";
+// Client - Local Draft
+pub const CLIENT_PATH_LOCAL_DRAFT: &str = "./.jv/drafts/{sheet_name}/";
+
// Client - Other
pub const CLIENT_FILE_IGNOREFILES: &str = "IGNORE_RULES.toml";
pub const CLIENT_FILE_README: &str = "./README.md";
diff --git a/crates/vcs_data/src/data/local/config.rs b/crates/vcs_data/src/data/local/config.rs
index 338d01b..3dc5248 100644
--- a/crates/vcs_data/src/data/local/config.rs
+++ b/crates/vcs_data/src/data/local/config.rs
@@ -1,12 +1,24 @@
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::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::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 +37,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<VaultUuid>,
+
+ /// The name of the sheet currently in use.
+ sheet_in_use: Option<SheetName>,
}
impl Default for LocalConfig {
@@ -36,6 +51,7 @@ impl Default for LocalConfig {
)),
using_account: "unknown".to_string(),
stained_uuid: None,
+ sheet_in_use: None,
}
}
}
@@ -56,6 +72,208 @@ 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 with rollback support
+ 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 with rollback support
+ 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<PathBuf, std::io::Error> {
+ 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 with rollback support
+ async fn move_draft_to_local(
+ &self,
+ draft_folder: &Path,
+ local_path: &Path,
+ ) -> Result<(), std::io::Error> {
+ let draft_entries: Vec<_> = std::fs::read_dir(draft_folder)
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
+
+ let mut moved_items: Vec<MovedItem> = 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| {
+ // 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| {
+ // 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 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: Vec<_> = std::fs::read_dir(local_path)
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
+
+ let mut moved_items: Vec<MovedItem> = Vec::new();
+
+ for entry in &entries {
+ 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| {
+ // 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(())
+ }
+
/// Get the currently used account
pub fn current_account(&self) -> MemberId {
self.using_account.clone()
@@ -66,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<VaultUuid> {
+ self.stained_uuid
+ }
+
/// Stain the local workspace with the given UUID.
pub fn stain(&mut self, uuid: VaultUuid) {
self.stained_uuid = Some(uuid);
@@ -75,4 +298,44 @@ 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<SheetName> {
+ &self.sheet_in_use
+ }
+
+ /// Get draft folder
+ pub fn draft_folder(
+ &self,
+ sheet_name: &SheetName,
+ local_workspace_path: impl Into<PathBuf>,
+ ) -> 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<PathBuf> {
+ 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))
+ }
+}
+
+#[derive(Clone)]
+struct MovedItem {
+ source: PathBuf,
+ target: PathBuf,
}
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<SheetName>,
/// Other sheets, indicating which sheets I can export files to (these sheets are not readable to me)
- pub other_sheets: Vec<SheetName>,
+ pub other_sheets: Vec<SheetInfo>,
/// 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<MemberId>,
+}
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<MemberId>,
/// Inputs
pub(crate) inputs: Vec<InputPackage>,
@@ -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<VirtualFileId> {
@@ -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) => {
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(),
};
diff --git a/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs b/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs
index 7484e4b..f256436 100644
--- a/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs
+++ b/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs
@@ -36,8 +36,8 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io:
let sheet = vault.create_sheet(&sheet_name, &member_id).await?;
// Verify sheet properties
- assert_eq!(sheet.holder(), &member_id);
- assert_eq!(sheet.holder(), &member_id);
+ assert_eq!(sheet.holder(), Some(&member_id));
+ assert_eq!(sheet.holder(), Some(&member_id));
assert!(sheet.inputs().is_empty());
assert!(sheet.mapping().is_empty());
@@ -100,7 +100,7 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io:
// Verify persistence by reloading the sheet
let reloaded_sheet = vault.sheet(&sheet_name).await?;
- assert_eq!(reloaded_sheet.holder(), &member_id);
+ assert_eq!(reloaded_sheet.holder(), Some(&member_id));
assert_eq!(reloaded_sheet.inputs().len(), 1);
assert_eq!(reloaded_sheet.mapping().len(), 3);
@@ -129,16 +129,16 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io:
// One sheet should be the test sheet, the other should be the ref sheet with host as holder
let test_sheet_holder = all_sheets
.iter()
- .find(|s| s.holder() == &member_id)
+ .find(|s| s.holder() == Some(&member_id))
.map(|s| s.holder())
.unwrap();
let ref_sheet_holder = all_sheets
.iter()
- .find(|s| s.holder() == &"host".to_string())
+ .find(|s| s.holder() == Some(&"host".to_string()))
.map(|s| s.holder())
.unwrap();
- assert_eq!(test_sheet_holder, &member_id);
- assert_eq!(ref_sheet_holder, &"host".to_string());
+ assert_eq!(test_sheet_holder, Some(&member_id));
+ assert_eq!(ref_sheet_holder, Some(&"host".to_string()));
// Test 8: Safe deletion (move to trash)
vault.delete_sheet_safely(&sheet_name).await?;
@@ -150,8 +150,8 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io:
// Test 9: Restore sheet from trash
let restored_sheet = vault.sheet(&sheet_name).await?;
- assert_eq!(restored_sheet.holder(), &member_id);
- assert_eq!(restored_sheet.holder(), &member_id);
+ assert_eq!(restored_sheet.holder(), Some(&member_id));
+ assert_eq!(restored_sheet.holder(), Some(&member_id));
// Verify sheet is back in normal listing
let sheet_names_after_restore = vault.sheet_names()?;