summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-03-15 18:19:59 +0800
committer魏曹先生 <1992414357@qq.com>2026-03-15 18:19:59 +0800
commitcc9bf0e13d75b9938b67da5df8135369374dc36d (patch)
tree37cb07d05ddfdfe7333de59e41d08c83351c92e3
parentd942ec50ff68f36c2641becdd6f32a95ab3f4325 (diff)
Add ID mapping system for local/remote index source conversion
-rw-r--r--systems/_constants/src/lib.rs1
-rw-r--r--systems/sheet/src/index_source.rs3
-rw-r--r--systems/sheet/src/index_source/alias.rs419
-rw-r--r--systems/sheet/src/index_source/error.rs19
-rw-r--r--systems/workspace/Cargo.toml1
-rw-r--r--systems/workspace/src/workspace/error.rs4
-rw-r--r--systems/workspace/src/workspace/manager.rs51
7 files changed, 497 insertions, 1 deletions
diff --git a/systems/_constants/src/lib.rs b/systems/_constants/src/lib.rs
index bea8fa8..b9d313c 100644
--- a/systems/_constants/src/lib.rs
+++ b/systems/_constants/src/lib.rs
@@ -133,6 +133,7 @@ pub mod workspace {
c! { VAULT_MIRROR = ".jv/UPSTREAM/" }
c! { LOCAL_SHEETS = ".jv/sheets/{account}/" }
c! { DRAFT_AREA = ".jv/drafts/{account}_{sheet}/" }
+ c! { ID_MAPPING = ".jv/idmp/" }
c! { WORKING_AREA = "" }
}
}
diff --git a/systems/sheet/src/index_source.rs b/systems/sheet/src/index_source.rs
index e322670..43508f2 100644
--- a/systems/sheet/src/index_source.rs
+++ b/systems/sheet/src/index_source.rs
@@ -1,5 +1,8 @@
use serde::{Deserialize, Serialize};
+pub mod alias;
+pub mod error;
+
/// IndexSource
/// Points to a unique resource address in Vault
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
diff --git a/systems/sheet/src/index_source/alias.rs b/systems/sheet/src/index_source/alias.rs
new file mode 100644
index 0000000..4a8f0ad
--- /dev/null
+++ b/systems/sheet/src/index_source/alias.rs
@@ -0,0 +1,419 @@
+use std::path::Path;
+use std::path::PathBuf;
+use tokio::fs;
+use tokio::fs::OpenOptions;
+use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
+
+use crate::index_source::IndexSource;
+use crate::index_source::error::IDAliasError;
+
+const MAX_ENTRIES_PER_FILE: u32 = 65536;
+const ENTRY_SIZE: usize = 4;
+const FILE_SIZE: u64 = (MAX_ENTRIES_PER_FILE as u64) * (ENTRY_SIZE as u64);
+
+impl IndexSource {
+ /// Convert local index source to remote index source
+ ///
+ /// This method converts the local index source's ID to the corresponding remote ID,
+ /// and updates the index source's state to remote.
+ /// If the index source is already in remote state, it returns directly.
+ ///
+ /// - `alias_dir` - Directory path where alias files are stored
+ /// - `Result<IndexSource, (IndexSource, aliasError)>` - Returns the converted index source on success,
+ /// or returns the original index source along with the error if conversion fails
+ pub async fn to_remote_namespace(
+ mut self,
+ alias_dir: PathBuf,
+ ) -> Result<IndexSource, (IndexSource, IDAliasError)> {
+ if self.remote {
+ return Ok(self);
+ }
+
+ let new_id = match convert_to_remote(alias_dir, self.id).await {
+ Ok(id) => id,
+ Err(e) => return Err((self, e)),
+ };
+ self.id = new_id;
+ self.set_is_remote(true);
+ Ok(self)
+ }
+}
+
+pub struct IndexSourceAliasesManager;
+impl IndexSourceAliasesManager {
+ /// Write a alias between local and remote IDs
+ pub async fn write_alias(
+ aliases_dir: impl Into<PathBuf>,
+ local_id: u32,
+ remote_id: u32,
+ ) -> Result<(), IDAliasError> {
+ write_alias(aliases_dir.into(), local_id, remote_id).await
+ }
+
+ /// Delete a alias between local and remote IDs
+ pub async fn delete_alias(
+ aliases_dir: impl Into<PathBuf>,
+ local_id: u32,
+ ) -> Result<(), IDAliasError> {
+ delete_alias(aliases_dir.into(), local_id).await
+ }
+
+ /// Check if a alias exists between local and remote IDs
+ pub async fn alias_exists(
+ aliases_dir: impl Into<PathBuf>,
+ local_id: u32,
+ ) -> Result<bool, IDAliasError> {
+ alias_exists(aliases_dir.into(), local_id).await
+ }
+}
+
+async fn write_alias(
+ aliases_dir: PathBuf,
+ local_id: u32,
+ remote_id: u32,
+) -> Result<(), IDAliasError> {
+ ensure_aliases_dir(&aliases_dir).await?;
+
+ let (file_index, offset) = get_file_path_and_offset(local_id);
+ let file = get_or_create_alias_file(&aliases_dir, file_index).await?;
+ write_entry_to_file(file, offset, remote_id).await?;
+
+ Ok(())
+}
+
+async fn delete_alias(aliases_dir: PathBuf, local_id: u32) -> Result<(), IDAliasError> {
+ ensure_aliases_dir(&aliases_dir).await?;
+
+ let (file_index, offset) = get_file_path_and_offset(local_id);
+ let file = get_or_create_alias_file(&aliases_dir, file_index).await?;
+ write_entry_to_file(file, offset, 0).await?;
+
+ Ok(())
+}
+
+async fn alias_exists(aliases_dir: PathBuf, local_id: u32) -> Result<bool, IDAliasError> {
+ ensure_aliases_dir(&aliases_dir).await?;
+
+ let (file_index, offset) = get_file_path_and_offset(local_id);
+ let file = get_or_create_alias_file(&aliases_dir, file_index).await?;
+ let remote_id = read_entry_from_file(file, offset).await?;
+
+ Ok(remote_id != 0)
+}
+
+async fn convert_to_remote(aliases_dir: PathBuf, local_id: u32) -> Result<u32, IDAliasError> {
+ ensure_aliases_dir(&aliases_dir).await?;
+
+ let (file_index, offset) = get_file_path_and_offset(local_id);
+ let file = get_or_create_alias_file(&aliases_dir, file_index).await?;
+ let remote_id = read_entry_from_file(file, offset).await?;
+
+ if remote_id == 0 {
+ return Err(IDAliasError::AliasNotFound(local_id));
+ }
+
+ Ok(remote_id)
+}
+
+fn get_file_path_and_offset(id: u32) -> (u32, u64) {
+ let file_index = id / MAX_ENTRIES_PER_FILE;
+ let offset_within_file = id % MAX_ENTRIES_PER_FILE;
+ let byte_offset = (offset_within_file as u64) * (ENTRY_SIZE as u64);
+ (file_index, byte_offset)
+}
+
+async fn ensure_aliases_dir(aliases_dir: &Path) -> Result<(), IDAliasError> {
+ if !aliases_dir.exists() {
+ fs::create_dir_all(aliases_dir).await?;
+ }
+ Ok(())
+}
+
+async fn get_or_create_alias_file(
+ aliases_dir: &Path,
+ file_index: u32,
+) -> Result<fs::File, IDAliasError> {
+ let file_path = aliases_dir.join(file_index.to_string());
+
+ let file = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .create(true)
+ .open(&file_path)
+ .await
+ .map_err(|e| IDAliasError::Io(e))?;
+
+ let metadata = file.metadata().await.map_err(|e| IDAliasError::Io(e))?;
+ if metadata.len() != FILE_SIZE {
+ drop(file);
+ let file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(&file_path)
+ .await
+ .map_err(|e| IDAliasError::Io(e))?;
+
+ file.set_len(FILE_SIZE)
+ .await
+ .map_err(|e| IDAliasError::Io(e))?;
+
+ let file = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(&file_path)
+ .await
+ .map_err(|e| IDAliasError::Io(e))?;
+
+ Ok(file)
+ } else {
+ Ok(file)
+ }
+}
+
+async fn write_entry_to_file(
+ mut file: fs::File,
+ offset: u64,
+ remote_id: u32,
+) -> Result<(), IDAliasError> {
+ file.seek(std::io::SeekFrom::Start(offset)).await?;
+ file.write_all(&remote_id.to_le_bytes()).await?;
+
+ Ok(())
+}
+
+async fn read_entry_from_file(mut file: fs::File, offset: u64) -> Result<u32, IDAliasError> {
+ file.seek(std::io::SeekFrom::Start(offset)).await?;
+
+ let mut remote_buf = [0u8; 4];
+ file.read_exact(&mut remote_buf).await?;
+
+ let remote_id = u32::from_le_bytes(remote_buf);
+ Ok(remote_id)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::env;
+
+ fn get_test_dir(test_name: &str) -> PathBuf {
+ let current_dir = env::current_dir().expect("Failed to get current directory");
+ let test_dir = current_dir
+ .join(".temp")
+ .join("index_source")
+ .join(test_name);
+ test_dir
+ }
+
+ async fn cleanup_test_dir(test_name: &str) {
+ let test_dir = get_test_dir(test_name);
+ if test_dir.exists() {
+ tokio::fs::remove_dir_all(&test_dir)
+ .await
+ .expect("Failed to cleanup test directory");
+ }
+ }
+
+ async fn setup_test(test_name: &str) -> PathBuf {
+ cleanup_test_dir(test_name).await;
+ let test_dir = get_test_dir(test_name);
+ tokio::fs::create_dir_all(&test_dir)
+ .await
+ .expect("Failed to create test directory");
+ test_dir
+ }
+
+ #[test]
+ fn test_get_file_path_and_offset() {
+ assert_eq!(get_file_path_and_offset(0), (0, 0));
+ assert_eq!(get_file_path_and_offset(1), (0, 4));
+ assert_eq!(get_file_path_and_offset(65535), (0, 262140)); // 65535 * 4
+ assert_eq!(get_file_path_and_offset(65536), (1, 0));
+ assert_eq!(get_file_path_and_offset(65537), (1, 4));
+ assert_eq!(get_file_path_and_offset(131071), (1, 262140)); // (131071-65536) * 4
+ assert_eq!(get_file_path_and_offset(131072), (2, 0));
+ }
+
+ #[tokio::test]
+ async fn test_write_and_read_alias() {
+ let test_dir = setup_test("test_write_and_read_alias").await;
+
+ write_alias(test_dir.clone(), 100, 200)
+ .await
+ .expect("Failed to write alias");
+
+ let exists = alias_exists(test_dir.clone(), 100)
+ .await
+ .expect("Failed to check alias");
+ assert!(exists, "alias should exist");
+
+ let remote_id = convert_to_remote(test_dir.clone(), 100)
+ .await
+ .expect("Failed to convert to remote");
+ assert_eq!(remote_id, 200, "Remote ID should be 200");
+
+ let result = convert_to_remote(test_dir.clone(), 999).await;
+ assert!(
+ result.is_err(),
+ "Should return error for non-existent alias"
+ );
+
+ cleanup_test_dir("test_write_and_read_alias").await;
+ }
+
+ #[tokio::test]
+ async fn test_delete_alias() {
+ let test_dir = setup_test("test_delete_alias").await;
+
+ write_alias(test_dir.clone(), 300, 400)
+ .await
+ .expect("Failed to write alias");
+
+ let exists = alias_exists(test_dir.clone(), 300)
+ .await
+ .expect("Failed to check alias");
+ assert!(exists, "alias should exist before deletion");
+
+ delete_alias(test_dir.clone(), 300)
+ .await
+ .expect("Failed to delete alias");
+
+ let exists = alias_exists(test_dir.clone(), 300)
+ .await
+ .expect("Failed to check alias");
+ assert!(!exists, "alias should not exist after deletion");
+
+ let result = convert_to_remote(test_dir.clone(), 300).await;
+ assert!(result.is_err(), "Should return error after deletion");
+
+ cleanup_test_dir("test_delete_alias").await;
+ }
+
+ #[tokio::test]
+ async fn test_large_id_alias() {
+ let test_dir = setup_test("test_large_id_alias").await;
+
+ let large_local_id = 70000;
+ let large_remote_id = 80000;
+
+ write_alias(test_dir.clone(), large_local_id, large_remote_id)
+ .await
+ .expect("Failed to write large alias");
+
+ let exists = alias_exists(test_dir.clone(), large_local_id)
+ .await
+ .expect("Failed to check alias");
+ assert!(exists, "Large alias should exist");
+
+ let remote_id = convert_to_remote(test_dir.clone(), large_local_id)
+ .await
+ .expect("Failed to convert large to remote");
+ assert_eq!(remote_id, large_remote_id, "Remote ID should match");
+
+ cleanup_test_dir("test_large_id_alias").await;
+ }
+
+ #[tokio::test]
+ async fn test_multiple_aliass() {
+ let test_dir = setup_test("test_multiple_aliass").await;
+
+ let aliass = vec![(10, 20), (30, 40), (50, 60), (1000, 2000), (70000, 80000)];
+
+ for (local, remote) in &aliass {
+ write_alias(test_dir.clone(), *local, *remote)
+ .await
+ .expect("Failed to write alias");
+ }
+
+ for (local, remote) in &aliass {
+ let exists = alias_exists(test_dir.clone(), *local)
+ .await
+ .expect("Failed to check alias");
+ assert!(exists, "alias for local {} should exist", local);
+
+ let converted_remote = convert_to_remote(test_dir.clone(), *local)
+ .await
+ .expect("Failed to convert to remote");
+ assert_eq!(
+ converted_remote, *remote,
+ "Remote ID should match for local {}",
+ local
+ );
+ }
+
+ let result = alias_exists(test_dir.clone(), 9999)
+ .await
+ .expect("Failed to check non-existent alias");
+ assert!(!result, "Non-existent alias should return false");
+
+ cleanup_test_dir("test_multiple_aliass").await;
+ }
+
+ #[tokio::test]
+ async fn test_file_creation_and_size() {
+ let test_dir = setup_test("test_file_creation_and_size").await;
+
+ write_alias(test_dir.clone(), 500, 600)
+ .await
+ .expect("Failed to write alias");
+
+ let file_path = test_dir.join("0");
+ assert!(file_path.exists(), "File should exist");
+
+ let metadata = tokio::fs::metadata(&file_path)
+ .await
+ .expect("Failed to get file metadata");
+ assert_eq!(
+ metadata.len(),
+ FILE_SIZE,
+ "File size should be {}",
+ FILE_SIZE
+ );
+
+ write_alias(test_dir.clone(), 70000, 80000)
+ .await
+ .expect("Failed to write alias");
+
+ let file_path_1 = test_dir.join("1");
+ assert!(file_path_1.exists(), "File 1 should exist");
+
+ let metadata_1 = tokio::fs::metadata(&file_path_1)
+ .await
+ .expect("Failed to get file metadata");
+ assert_eq!(
+ metadata_1.len(),
+ FILE_SIZE,
+ "File 1 size should be {}",
+ FILE_SIZE
+ );
+
+ cleanup_test_dir("test_file_creation_and_size").await;
+ }
+
+ #[tokio::test]
+ async fn test_error_handling() {
+ let test_dir = setup_test("test_error_handling").await;
+
+ let result = convert_to_remote(test_dir.clone(), 100).await;
+ assert!(
+ matches!(result, Err(IDAliasError::AliasNotFound(100))),
+ "Should return aliasNotFound error"
+ );
+
+ write_alias(test_dir.clone(), 200, 300)
+ .await
+ .expect("Failed to write alias");
+ delete_alias(test_dir.clone(), 200)
+ .await
+ .expect("Failed to delete alias");
+
+ let result = convert_to_remote(test_dir.clone(), 200).await;
+ assert!(
+ matches!(result, Err(IDAliasError::AliasNotFound(200))),
+ "Should return aliasNotFound after deletion"
+ );
+
+ cleanup_test_dir("test_error_handling").await;
+ }
+}
diff --git a/systems/sheet/src/index_source/error.rs b/systems/sheet/src/index_source/error.rs
new file mode 100644
index 0000000..b8e98fd
--- /dev/null
+++ b/systems/sheet/src/index_source/error.rs
@@ -0,0 +1,19 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum IDAliasError {
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("Invalid alias file: {0}")]
+ InvalidAliasFile(String),
+
+ #[error("Alias not found for ID: {0}")]
+ AliasNotFound(u32),
+
+ #[error("Invalid file offset: {0}")]
+ InvalidOffset(u64),
+
+ #[error("File size mismatch: expected {0}, got {1}")]
+ FileSizeMismatch(u64, u64),
+}
diff --git a/systems/workspace/Cargo.toml b/systems/workspace/Cargo.toml
index c394bb3..c601370 100644
--- a/systems/workspace/Cargo.toml
+++ b/systems/workspace/Cargo.toml
@@ -8,6 +8,7 @@ asset_system = { path = "../_asset" }
config_system = { path = "../_config" }
constants = { path = "../_constants" }
framework = { path = "../_framework" }
+sheet_system = { path = "../sheet" }
serde.workspace = true
thiserror.workspace = true
diff --git a/systems/workspace/src/workspace/error.rs b/systems/workspace/src/workspace/error.rs
index 495558b..0910392 100644
--- a/systems/workspace/src/workspace/error.rs
+++ b/systems/workspace/src/workspace/error.rs
@@ -1,5 +1,6 @@
use asset_system::error::{DataApplyError, DataReadError, DataWriteError, HandleLockError};
use framework::space::error::SpaceError;
+use sheet_system::index_source::error::IDAliasError;
#[derive(thiserror::Error, Debug)]
pub enum WorkspaceOperationError {
@@ -26,6 +27,9 @@ pub enum WorkspaceOperationError {
#[error("Data apply error: {0}")]
DataApply(#[from] DataApplyError),
+
+ #[error("ID alias error: {0}")]
+ IDAliasError(#[from] IDAliasError),
}
impl From<SpaceError> for WorkspaceOperationError {
diff --git a/systems/workspace/src/workspace/manager.rs b/systems/workspace/src/workspace/manager.rs
index 9e319a1..528f266 100644
--- a/systems/workspace/src/workspace/manager.rs
+++ b/systems/workspace/src/workspace/manager.rs
@@ -1,7 +1,8 @@
use crate::workspace::{Workspace, config::WorkspaceConfig, error::WorkspaceOperationError};
use asset_system::asset::ReadOnlyAsset;
-use constants::workspace::files::workspace_file_config;
+use constants::workspace::{dirs::workspace_dir_id_mapping, files::workspace_file_config};
use framework::space::Space;
+use sheet_system::index_source::{IndexSource, alias::IndexSourceAliasesManager};
pub struct WorkspaceManager {
space: Space<Workspace>,
@@ -35,4 +36,52 @@ impl WorkspaceManager {
let asset = ReadOnlyAsset::from(config_path);
Ok(asset)
}
+
+ /// Attempt to convert an index source to a remote namespace.
+ /// This method takes an `IndexSource` and tries to map it to a remote namespace
+ /// using the workspace's ID alias directory. If not found, the original
+ /// `IndexSource` is returned as a fallback.
+ ///
+ /// - `index_source` - The index source to convert
+ /// - `Result<IndexSource, WorkspaceOperationError>` - The converted index source on success,
+ /// or the original index source if alias fails. Returns an error if there's
+ /// a problem accessing the workspace directory.
+ pub async fn try_to_remote_index(
+ &self,
+ index_source: IndexSource,
+ ) -> Result<IndexSource, WorkspaceOperationError> {
+ let aliases_dir = self.get_space().local_path(workspace_dir_id_mapping())?;
+ Ok(match index_source.to_remote_namespace(aliases_dir).await {
+ Ok(index_source) => index_source,
+ Err((index_source, _)) => index_source,
+ })
+ }
+
+ /// Write a alias between local and remote IDs
+ pub async fn write_id_alias(
+ &self,
+ local_id: u32,
+ remote_id: u32,
+ ) -> Result<(), WorkspaceOperationError> {
+ let aliases_dir = self.get_space().local_path(workspace_dir_id_mapping())?;
+ IndexSourceAliasesManager::write_alias(aliases_dir, local_id, remote_id)
+ .await
+ .map_err(|e| WorkspaceOperationError::IDAliasError(e))
+ }
+
+ /// Delete a alias between local and remote IDs
+ pub async fn delete_id_alias(&self, local_id: u32) -> Result<(), WorkspaceOperationError> {
+ let aliases_dir = self.get_space().local_path(workspace_dir_id_mapping())?;
+ IndexSourceAliasesManager::delete_alias(aliases_dir, local_id)
+ .await
+ .map_err(|e| WorkspaceOperationError::IDAliasError(e))
+ }
+
+ /// Check if a alias exists between local and remote IDs
+ pub async fn id_aliases_exists(&self, local_id: u32) -> Result<bool, WorkspaceOperationError> {
+ let aliases_dir = self.get_space().local_path(workspace_dir_id_mapping())?;
+ IndexSourceAliasesManager::alias_exists(aliases_dir, local_id)
+ .await
+ .map_err(|e| WorkspaceOperationError::IDAliasError(e))
+ }
}