From a6b38c2918ece9d763a1eb1d581268d94b2bdcf0 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 11 Mar 2026 14:49:20 +0800 Subject: Add framework crate --- systems/_constants/src/lib.rs | 82 +++---- systems/_framework/Cargo.toml | 10 + systems/_framework/src/lib.rs | 1 + systems/_framework/src/space.rs | 414 ++++++++++++++++++++++++++++++++++ systems/_framework/src/space/error.rs | 20 ++ 5 files changed, 486 insertions(+), 41 deletions(-) create mode 100644 systems/_framework/Cargo.toml create mode 100644 systems/_framework/src/lib.rs create mode 100644 systems/_framework/src/space.rs create mode 100644 systems/_framework/src/space/error.rs (limited to 'systems') diff --git a/systems/_constants/src/lib.rs b/systems/_constants/src/lib.rs index c9dee11..1fac6f8 100644 --- a/systems/_constants/src/lib.rs +++ b/systems/_constants/src/lib.rs @@ -14,14 +14,14 @@ pub mod server { /// File constants #[constants_macros::constants("server_file")] pub mod files { - c! { CONFIG = "./config.toml" } + c! { CONFIG = "config.toml" } // Storage location for keys and passwords - c! { KEY = "./auth/key/{member_name}.pem" } - c! { PASSWORD = "./auth/pswd/{member_name}.pswd" } + c! { KEY = "auth/key/{member_name}.pem" } + c! { PASSWORD = "auth/pswd/{member_name}.pswd" } // Only Key holders are allowed to initiate Join Request - c! { JOIN_REQUEST_KEY = "./.temp/join_requests/{member_name}.pem" } + c! { JOIN_REQUEST_KEY = ".temp/join_requests/{member_name}.pem" } // User metadata is stored here // Typically includes: @@ -30,17 +30,17 @@ pub mod server { // - Other information // // Facilitates queries by other members - c! { MEMBER_METADATA = "./meta/{member_name}.toml" } + c! { MEMBER_METADATA = "meta/{member_name}.toml" } } /// Directory path constants #[constants_macros::constants("server_dir")] pub mod dirs { - c! { KEYS = "./auth/key/" } - c! { PASSWORDS = "./auth/pswd/" } - c! { JOIN_REQUEST_KEYS = "./.temp/join_requests/" } - c! { MEMBER_METADATAS = "./meta/" } - c! { VAULTS = "./v/" } + c! { KEYS = "auth/key/" } + c! { PASSWORDS = "auth/pswd/" } + c! { JOIN_REQUEST_KEYS = ".temp/join_requests/" } + c! { MEMBER_METADATAS = "meta/" } + c! { VAULTS = "v/" } } } @@ -61,44 +61,44 @@ pub mod vault { #[constants_macros::constants("vault_file")] pub mod files { // ### Configs ### - c! { CONFIG = "./vault.toml" } + c! { CONFIG = "vault.toml" } // ### Sheets ### // Reference sheet - c! { REFSHEET = "./ref/{ref_sheet_name}.sheet" } + c! { REFSHEET = "ref/{ref_sheet_name}.sheet" } // Member sheet backup, only used to temporarily store a member's local workspace file structure, fully controlled by the corresponding member - c! { MEMBER_SHEET_BACKUP = "./_member/{member_name}/backups/{sheet_name}.sheet" } + c! { MEMBER_SHEET_BACKUP = "_member/{member_name}/backups/{sheet_name}.sheet" } // ### Rules ### - c! { IGNORE_RULE_SCRIPT = "./rules/ignore/{script_name}.lua" } + c! { IGNORE_RULE_SCRIPT = "rules/ignore/{script_name}.lua" } // ### Storages ### - c! { CHANGE_FILE = "./changes/CURRENT" } - c! { CHANGE_FILE_BACKUP = "./changes/backup.{index}" } + c! { CHANGE_FILE = "changes/CURRENT" } + c! { CHANGE_FILE_BACKUP = "changes/backup.{index}" } // Whether an index is locked; if the file exists, the member name written inside is the current holder - c! { INDEX_LOCK = "./index/{index_slice_1}/{index_slice_2}/{index_name}/LOCK" } + c! { INDEX_LOCK = "index/{index_slice_1}/{index_slice_2}/{index_name}/LOCK" } // Index version sequence - c! { INDEX_VER = "./index/{index_slice_1}/{index_slice_2}/{index_name}/ver" } + c! { INDEX_VER = "index/{index_slice_1}/{index_slice_2}/{index_name}/ver" } // Index - c! { INDEX = "./index/{index_slice_1}/{index_slice_2}/{index_name}/{version}.index" } + c! { INDEX = "index/{index_slice_1}/{index_slice_2}/{index_name}/{version}.index" } // Object, file chunk - c! { OBJ = "./obj/{obj_hash_slice_1}/{obj_hash_slice_2}/{obj_hash}" } + c! { OBJ = "obj/{obj_hash_slice_1}/{obj_hash_slice_2}/{obj_hash}" } } /// Directory path constants #[constants_macros::constants("vault_dir")] pub mod dirs { - c! { REFSHEETS = "./ref/" } - c! { MEMBER = "./_member/{member_name}/" } - c! { MEMBER_SHEET_BACKUPS = "./_member/{member_name}/backups/" } - c! { IGNORE_RULES = "./rules/ignore/" } - c! { CHANGES = "./changes/" } + c! { REFSHEETS = "ref/" } + c! { MEMBER = "_member/{member_name}/" } + c! { MEMBER_SHEET_BACKUPS = "_member/{member_name}/backups/" } + c! { IGNORE_RULES = "rules/ignore/" } + c! { CHANGES = "changes/" } } } @@ -109,30 +109,30 @@ pub mod workspace { #[constants_macros::constants("workspace_file")] pub mod files { // ### Config ### - c! { CONFIG = "./.jv/workspace.toml" } + c! { CONFIG = ".jv/workspace.toml" } // ### Sheets ### // Records the latest state of local physical files, used to calculate deviations - c! { LOCAL_STATUS = "./.jv/sheets/{account}/{sheet}.local" } + c! { LOCAL_STATUS = ".jv/sheets/{account}/{sheet}.local" } // Personal sheet, represents the desired file structure, held only by the member, can be backed up to the vault - c! { SHEET = "./.jv/sheets/{account}/{sheet}.sheet" } + c! { SHEET = ".jv/sheets/{account}/{sheet}.sheet" } // Draft file, when switching to another sheet, fully records modified but untracked files - c! { DRAFTED_FILE = "./.jv/drafts/{account}_{sheet}/{mapping}" } + c! { DRAFTED_FILE = ".jv/drafts/{account}_{sheet}/{mapping}" } // Working file - c! { WORKING_FILE = "./{mapping}" } + c! { WORKING_FILE = "{mapping}" } } /// Directory path constants #[constants_macros::constants("workspace_dir")] pub mod dirs { - c! { WORKSPACE = "./.jv" } - c! { VAULT_MIRROR = "./.jv/UPSTREAM/" } - c! { LOCAL_SHEETS = "./.jv/sheets/{account}/" } - c! { DRAFT_AREA = "./.jv/drafts/{account}_{sheet}/" } - c! { WORKING_AREA = "./" } + c! { WORKSPACE = ".jv" } + c! { VAULT_MIRROR = ".jv/UPSTREAM/" } + c! { LOCAL_SHEETS = ".jv/sheets/{account}/" } + c! { DRAFT_AREA = ".jv/drafts/{account}_{sheet}/" } + c! { WORKING_AREA = "" } } } @@ -143,22 +143,22 @@ pub mod user { #[constants_macros::constants("user_file")] pub mod files { // Upstream public key, stored after initial login, used to verify the trustworthiness of that upstream - c! { UPSTREAM_PUB = "./upstreams/{upstream_addr}.pem" } + c! { UPSTREAM_PUB = "upstreams/{upstream_addr}.pem" } // Account private key, stored only locally, used for login authentication - c! { PRIVATE_KEY = "./private/{account}.pem" } + c! { PRIVATE_KEY = "private/{account}.pem" } // Account public key, automatically generated from the private key and stored, // will be placed into the server's "join request list" upon initial login (if server.toml permits this action) // The server administrator can optionally approve this request - c! { PUBLIC_KEY = "./public/{account}.pem" } + c! { PUBLIC_KEY = "public/{account}.pem" } } /// Directory path constants #[constants_macros::constants("user_dir")] pub mod dirs { - c! { UPSTREAM_PUBS = "./upstreams/" } - c! { PRIVATE_KEYS = "./private/" } - c! { PUBLIC_KEYS = "./public/" } + c! { UPSTREAM_PUBS = "upstreams/" } + c! { PRIVATE_KEYS = "private/" } + c! { PUBLIC_KEYS = "public/" } } } diff --git a/systems/_framework/Cargo.toml b/systems/_framework/Cargo.toml new file mode 100644 index 0000000..286aec8 --- /dev/null +++ b/systems/_framework/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "framework" +edition = "2024" +version.workspace = true + +[dependencies] +tokio = { version = "1.50", features = ["fs"] } +thiserror = "2" + +just_fmt = "0.1.2" diff --git a/systems/_framework/src/lib.rs b/systems/_framework/src/lib.rs new file mode 100644 index 0000000..9ffc73e --- /dev/null +++ b/systems/_framework/src/lib.rs @@ -0,0 +1 @@ +pub mod space; diff --git a/systems/_framework/src/space.rs b/systems/_framework/src/space.rs new file mode 100644 index 0000000..8a82467 --- /dev/null +++ b/systems/_framework/src/space.rs @@ -0,0 +1,414 @@ +use crate::space::error::SpaceError; +use just_fmt::fmt_path::{PathFormatConfig, PathFormatError, fmt_path, fmt_path_custom}; +use std::{ + cell::Cell, + env::current_dir, + ffi::OsString, + ops::Deref, + path::{Path, PathBuf}, +}; + +pub mod error; + +pub struct Space { + path_format_cfg: PathFormatConfig, + + content: T, + space_dir: Cell>, + current_dir: Option, +} + +impl Space { + /// Create a new `Space` instance with the given content. + pub fn new(content: T) -> Self { + Space { + path_format_cfg: PathFormatConfig { + resolve_parent_dirs: true, + ..Default::default() + }, + content, + space_dir: Cell::new(None), + current_dir: None, + } + } + + /// Consume the `Space`, returning the inner content. + pub fn into_inner(self) -> T { + self.content + } + + /// Get the space directory for the given current directory. + /// + /// If the space directory has already been found, it is returned from cache. + /// Otherwise, it is found using the pattern from `T::get_pattern()`. + pub fn space_dir(&self, current_dir: impl Into) -> Result { + let pattern = T::get_pattern(); + match self.space_dir.take() { + Some(dir) => { + self.update_space_dir(Some(dir.clone())); + Ok(dir) + } + None => { + let result = find_space_root_with(current_dir.into(), pattern); + match result { + Ok(r) => { + self.update_space_dir(Some(r.clone())); + Ok(r) + } + Err(e) => Err(e), + } + } + } + } + + /// Get the space directory using the current directory. + /// + /// The current directory is either the explicitly set directory or the process's current directory. + pub fn space_dir_current(&self) -> Result { + self.space_dir(self.current_dir()?) + } + + /// Set the current directory explicitly. + /// + /// This clears any cached space directory. + pub fn set_current_dir(&mut self, path: PathBuf) -> Result<(), PathFormatError> { + self.update_space_dir(None); + self.current_dir = Some(fmt_path(path)?); + Ok(()) + } + + /// Reset the current directory to the process's current directory. + /// + /// This clears any cached space directory. + pub fn reset_current_dir(&mut self) { + self.update_space_dir(None); + self.current_dir = None + } + + /// Get the current directory. + /// + /// Returns the explicitly set directory if any, otherwise the process's current directory. + fn current_dir(&self) -> Result { + match &self.current_dir { + Some(d) => Ok(d.clone()), + None => Ok(fmt_path(current_dir()?)?), + } + } + + /// Update the cached space directory. + fn update_space_dir(&self, space_dir: Option) { + self.space_dir.set(space_dir); + } +} + +impl Space { + /// Convert a relative path to an absolute path within the space. + /// + /// The path is formatted according to the space's path format configuration. + pub fn local_path(&self, relative_path: impl AsRef) -> Result { + let path = fmt_path_custom(relative_path.as_ref().to_path_buf(), &self.path_format_cfg)?; + let raw_path = self.space_dir_current()?.join(path); + Ok(fmt_path(raw_path)?) + } + + /// Convert an absolute path to a relative path within the space, if possible. + /// + /// Returns `None` if the absolute path is not under the space directory. + pub fn to_local_path( + &self, + absolute_path: impl AsRef, + ) -> Result, SpaceError> { + let path = fmt_path(absolute_path.as_ref())?; + let current = self.space_dir_current()?; + match path.strip_prefix(current) { + Ok(result) => Ok(Some(result.to_path_buf())), + Err(_) => Ok(None), + } + } + + /// Canonicalize a relative path within the space. + pub async fn canonicalize( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::canonicalize(path).await?) + } + + /// Copy a file from one relative path to another within the space. + pub async fn copy( + &self, + from: impl AsRef, + to: impl AsRef, + ) -> Result { + let from_path = self.local_path(from)?; + let to_path = self.local_path(to)?; + Ok(tokio::fs::copy(from_path, to_path).await?) + } + + /// Create a directory at the given relative path within the space. + pub async fn create_dir(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::create_dir(path).await?) + } + + /// Recursively create a directory and all its parents at the given relative path within the space. + pub async fn create_dir_all(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::create_dir_all(path).await?) + } + + /// Create a hard link from `src` to `dst` within the space. + pub async fn hard_link( + &self, + src: impl AsRef, + dst: impl AsRef, + ) -> Result<(), SpaceError> { + let src_path = self.local_path(src)?; + let dst_path = self.local_path(dst)?; + Ok(tokio::fs::hard_link(src_path, dst_path).await?) + } + + /// Get metadata for a file or directory at the given relative path within the space. + pub async fn metadata( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::metadata(path).await?) + } + + /// Read the entire contents of a file at the given relative path within the space. + pub async fn read(&self, relative_path: impl AsRef) -> Result, SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::read(path).await?) + } + + /// Read the directory entries at the given relative path within the space. + pub async fn read_dir( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::read_dir(path).await?) + } + + /// Read the target of a symbolic link at the given relative path within the space. + pub async fn read_link(&self, relative_path: impl AsRef) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::read_link(path).await?) + } + + /// Read the entire contents of a file as a string at the given relative path within the space. + pub async fn read_to_string( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::read_to_string(path).await?) + } + + /// Remove an empty directory at the given relative path within the space. + pub async fn remove_dir(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::remove_dir(path).await?) + } + + /// Remove a directory and all its contents at the given relative path within the space. + pub async fn remove_dir_all(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::remove_dir_all(path).await?) + } + + /// Remove a file at the given relative path within the space. + pub async fn remove_file(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::remove_file(path).await?) + } + + /// Rename a file or directory from one relative path to another within the space. + pub async fn rename( + &self, + from: impl AsRef, + to: impl AsRef, + ) -> Result<(), SpaceError> { + let from_path = self.local_path(from)?; + let to_path = self.local_path(to)?; + Ok(tokio::fs::rename(from_path, to_path).await?) + } + + /// Set permissions for a file or directory at the given relative path within the space. + pub async fn set_permissions( + &self, + relative_path: impl AsRef, + perm: std::fs::Permissions, + ) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::set_permissions(path, perm).await?) + } + + /// Create a symbolic link from `src` to `dst` within the space (Unix only). + #[cfg(unix)] + pub async fn symlink( + &self, + src: impl AsRef, + dst: impl AsRef, + ) -> Result<(), SpaceError> { + let src_path = self.local_path(src)?; + let dst_path = self.local_path(dst)?; + Ok(tokio::fs::symlink(src_path, dst_path).await?) + } + + /// Create a directory symbolic link from `src` to `dst` within the space (Windows only). + #[cfg(windows)] + pub async fn symlink_dir( + &self, + src: impl AsRef, + dst: impl AsRef, + ) -> Result<(), SpaceError> { + let src_path = self.local_path(src)?; + let dst_path = self.local_path(dst)?; + Ok(tokio::fs::symlink_dir(src_path, dst_path).await?) + } + + /// Create a file symbolic link from `src` to `dst` within the space (Windows only). + #[cfg(windows)] + pub async fn symlink_file( + &self, + src: impl AsRef, + dst: impl AsRef, + ) -> Result<(), SpaceError> { + let src_path = self.local_path(src)?; + let dst_path = self.local_path(dst)?; + Ok(tokio::fs::symlink_file(src_path, dst_path).await?) + } + + /// Get metadata for a file or directory without following symbolic links. + pub async fn symlink_metadata( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::symlink_metadata(path).await?) + } + + /// Check if a file or directory exists at the given relative path within the space. + pub async fn try_exists(&self, relative_path: impl AsRef) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::try_exists(path).await?) + } + + /// Write data to a file at the given relative path within the space. + pub async fn write( + &self, + relative_path: impl AsRef, + contents: impl AsRef<[u8]>, + ) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::write(path, contents).await?) + } +} + +impl From for Space { + fn from(content: T) -> Self { + Space::::new(content) + } +} + +impl AsRef for Space { + fn as_ref(&self) -> &T { + &self.content + } +} + +impl Deref for Space { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.as_ref() + } +} + +pub trait SpaceContent { + fn get_pattern() -> SpaceRootFindPattern; +} + +pub enum SpaceRootFindPattern { + IncludeDotDir(OsString), + IncludeFile(OsString), +} + +/// Find the space directory containing the current directory, +/// Use Pattern to specify the search method +/// +/// For the full implementation, see `find_space_root_with` +pub fn find_space_root(pattern: SpaceRootFindPattern) -> Result { + find_space_root_with(current_dir()?, pattern) +} + +/// Find the space directory containing the specified directory, +/// Use Pattern to specify the search method +/// +/// IncludeDotDir(OsString) +/// - Contains a specific directory, e.g., to find `.git`, use `IncludeDotDir("git".into())` +/// +/// IncludeFile(OsString) +/// - Contains a specific file, e.g., to find `Cargo.toml`, use `IncludeFile("Cargo.toml".into())` +/// +/// ```rust +/// # use std::env::current_dir; +/// # use std::path::PathBuf; +/// # use framework::space::SpaceRootFindPattern; +/// # use framework::space::find_space_root_with; +/// // Find the `.cargo` directory +/// let path = find_space_root_with( +/// current_dir().unwrap(), +/// SpaceRootFindPattern::IncludeDotDir( +/// "cargo".into() +/// ) +/// ); +/// assert!(path.is_ok()); +/// assert!(path.unwrap().join(".cargo").is_dir()) +/// ``` +/// ```rust +/// # use std::env::current_dir; +/// # use std::path::PathBuf; +/// # use framework::space::SpaceRootFindPattern; +/// # use framework::space::find_space_root_with; +/// // Find the `Cargo.toml` file +/// let path = find_space_root_with( +/// current_dir().unwrap(), +/// SpaceRootFindPattern::IncludeFile( +/// "Cargo.toml".into() +/// ) +/// ); +/// assert!(path.is_ok()); +/// assert!(path.unwrap().join("Cargo.toml").is_file()) +/// ``` +pub fn find_space_root_with( + current_dir: PathBuf, + pattern: SpaceRootFindPattern, +) -> Result { + // Get the pattern used for matching + let match_pattern: Box bool> = match pattern { + SpaceRootFindPattern::IncludeDotDir(dot_dir_name) => Box::new(move |path| { + path.join(format!(".{}", dot_dir_name.to_string_lossy())) + .is_dir() + }), + SpaceRootFindPattern::IncludeFile(file_name) => { + Box::new(move |path| path.join(&file_name).is_file()) + } + }; + // Match parent directories + let mut current = current_dir; + loop { + if match_pattern(current.as_path()) { + return Ok(current); + } + if let Some(parent) = current.parent() { + current = parent.to_path_buf(); + } else { + break; + } + } + Err(SpaceError::SpaceNotFound) +} diff --git a/systems/_framework/src/space/error.rs b/systems/_framework/src/space/error.rs new file mode 100644 index 0000000..50e673f --- /dev/null +++ b/systems/_framework/src/space/error.rs @@ -0,0 +1,20 @@ +#[derive(thiserror::Error, Debug)] +pub enum SpaceError { + #[error("Space not found")] + SpaceNotFound, + + #[error("Path format error: {0}")] + PathFormatError(#[from] just_fmt::fmt_path::PathFormatError), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), +} + +impl PartialEq for SpaceError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Io(_), Self::Io(_)) => true, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} -- cgit