diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-03-15 18:19:59 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-03-15 18:19:59 +0800 |
| commit | cc9bf0e13d75b9938b67da5df8135369374dc36d (patch) | |
| tree | 37cb07d05ddfdfe7333de59e41d08c83351c92e3 /systems | |
| parent | d942ec50ff68f36c2641becdd6f32a95ab3f4325 (diff) | |
Add ID mapping system for local/remote index source conversion
Diffstat (limited to 'systems')
| -rw-r--r-- | systems/_constants/src/lib.rs | 1 | ||||
| -rw-r--r-- | systems/sheet/src/index_source.rs | 3 | ||||
| -rw-r--r-- | systems/sheet/src/index_source/alias.rs | 419 | ||||
| -rw-r--r-- | systems/sheet/src/index_source/error.rs | 19 | ||||
| -rw-r--r-- | systems/workspace/Cargo.toml | 1 | ||||
| -rw-r--r-- | systems/workspace/src/workspace/error.rs | 4 | ||||
| -rw-r--r-- | systems/workspace/src/workspace/manager.rs | 51 |
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)) + } } |
