From 9e5a374164aca71ec99aa8b46e7932a1b74f68cc Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 7 Nov 2025 13:24:43 +0800 Subject: Add path formatting utility and local sheet management - Implement format_path_str function to clean and normalize file paths - Add LocalSheet struct for tracking local file metadata - Support CRUD operations on local sheet mappings - Integrate path formatting into local sheet operations --- crates/utils/string_proc/src/format_path.rs | 64 +++++++++++++++ crates/utils/string_proc/src/lib.rs | 1 + crates/vcs_data/src/data/local.rs | 58 +++++++++++++- crates/vcs_data/src/data/local/latest_info.rs | 2 - crates/vcs_data/src/data/local/local_sheet.rs | 107 +++++++++++++++++++++++--- crates/vcs_data/src/data/local/member_held.rs | 2 + 6 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 crates/utils/string_proc/src/format_path.rs (limited to 'crates') diff --git a/crates/utils/string_proc/src/format_path.rs b/crates/utils/string_proc/src/format_path.rs new file mode 100644 index 0000000..2d8927b --- /dev/null +++ b/crates/utils/string_proc/src/format_path.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; + +/// Format path str +pub fn format_path_str(path: impl Into) -> Result { + let path_str = path.into(); + + // ANSI Strip + let cleaned = strip_ansi_escapes::strip(&path_str); + let path_without_ansi = String::from_utf8(cleaned) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + let path_with_forward_slash = path_without_ansi.replace('\\', "/"); + let mut result = String::new(); + let mut prev_char = '\0'; + + for c in path_with_forward_slash.chars() { + if c == '/' && prev_char == '/' { + continue; + } + result.push(c); + prev_char = c; + } + + let unfriendly_chars = ['*', '?', '"', '<', '>', '|']; + result = result + .chars() + .filter(|c| !unfriendly_chars.contains(c)) + .collect(); + + if result.ends_with('/') { + Ok(result) + } else { + Ok(result) + } +} + +pub fn format_path(path: impl Into) -> Result { + let path_str = format_path_str(path.into().display().to_string())?; + Ok(PathBuf::from(path_str)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_path() -> Result<(), std::io::Error> { + assert_eq!(format_path_str("C:\\Users\\\\test")?, "C:/Users/test"); + + assert_eq!( + format_path_str("/path/with/*unfriendly?chars")?, + "/path/with/unfriendlychars" + ); + + assert_eq!(format_path_str("\x1b[31m/path\x1b[0m")?, "/path"); + assert_eq!(format_path_str("/home/user/dir/")?, "/home/user/dir/"); + assert_eq!( + format_path_str("/home/user/file.txt")?, + "/home/user/file.txt" + ); + + Ok(()) + } +} diff --git a/crates/utils/string_proc/src/lib.rs b/crates/utils/string_proc/src/lib.rs index e5559b9..76588c1 100644 --- a/crates/utils/string_proc/src/lib.rs +++ b/crates/utils/string_proc/src/lib.rs @@ -1,3 +1,4 @@ +pub mod format_path; pub mod format_processer; pub mod macros; pub mod simple_processer; diff --git a/crates/vcs_data/src/data/local.rs b/crates/vcs_data/src/data/local.rs index 96080b2..76711a7 100644 --- a/crates/vcs_data/src/data/local.rs +++ b/crates/vcs_data/src/data/local.rs @@ -1,13 +1,20 @@ -use std::{env::current_dir, path::PathBuf, sync::Arc}; +use std::{collections::HashMap, env::current_dir, path::PathBuf, sync::Arc}; use cfg_file::config::ConfigFile; use tokio::{fs, sync::Mutex}; use vcs_docs::docs::READMES_LOCAL_WORKSPACE_TODOLIST; use crate::{ - constants::{CLIENT_FILE_TODOLIST, CLIENT_FILE_WORKSPACE}, + constants::{CLIENT_FILE_LOCAL_SHEET, CLIENT_FILE_TODOLIST, CLIENT_FILE_WORKSPACE}, current::{current_local_path, find_local_path}, - data::local::config::LocalConfig, + data::{ + local::{ + config::LocalConfig, + local_sheet::{LocalSheet, LocalSheetData}, + }, + member::MemberId, + sheet::SheetName, + }, }; pub mod cached_sheet; @@ -16,6 +23,9 @@ pub mod latest_info; pub mod local_sheet; pub mod member_held; +const SHEET_NAME: &str = "{sheet_name}"; +const ACCOUNT_NAME: &str = "{account}"; + pub struct LocalWorkspace { config: Arc>, local_path: PathBuf, @@ -94,4 +104,46 @@ impl LocalWorkspace { Self::setup_local_workspace(current_dir()?).await?; Ok(()) } + + /// Get the path to a local sheet. + pub fn local_sheet_path(&self, member: &MemberId, sheet: &SheetName) -> PathBuf { + let result = self.local_path.join( + CLIENT_FILE_LOCAL_SHEET + .replace(ACCOUNT_NAME, member) + .replace(SHEET_NAME, sheet), + ); + result + } + + /// Read or initialize a local sheet. + pub async fn local_sheet( + &self, + member: &MemberId, + sheet: &SheetName, + ) -> Result, std::io::Error> { + let local_sheet_path = self.local_sheet_path(member, sheet); + + if !local_sheet_path.exists() { + let sheet_data = LocalSheetData { + mapping: HashMap::new(), + }; + LocalSheetData::write_to(&sheet_data, local_sheet_path).await?; + return Ok(LocalSheet { + local_workspace: self, + member: member.clone(), + sheet_name: sheet.clone(), + data: sheet_data, + }); + } + + let data = LocalSheetData::read_from(&local_sheet_path).await?; + let local_sheet = LocalSheet { + local_workspace: self, + member: member.clone(), + sheet_name: sheet.clone(), + data, + }; + + Ok(local_sheet) + } } diff --git a/crates/vcs_data/src/data/local/latest_info.rs b/crates/vcs_data/src/data/local/latest_info.rs index 28434b0..e8fa641 100644 --- a/crates/vcs_data/src/data/local/latest_info.rs +++ b/crates/vcs_data/src/data/local/latest_info.rs @@ -28,8 +28,6 @@ pub struct LatestInfo { pub vault_members: Vec, } -impl LatestInfo {} - #[derive(Default, Serialize, Deserialize)] pub struct SheetInfo { pub sheet_name: SheetName, diff --git a/crates/vcs_data/src/data/local/local_sheet.rs b/crates/vcs_data/src/data/local/local_sheet.rs index 63b73ac..bfe8d01 100644 --- a/crates/vcs_data/src/data/local/local_sheet.rs +++ b/crates/vcs_data/src/data/local/local_sheet.rs @@ -1,45 +1,61 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{collections::HashMap, io::Error, path::PathBuf}; use ::serde::{Deserialize, Serialize}; -use cfg_file::ConfigFile; +use cfg_file::{ConfigFile, config::ConfigFile}; use chrono::NaiveDate; +use string_proc::format_path::format_path; use crate::{ constants::CLIENT_FILE_LOCAL_SHEET_NOSET, - data::vault::virtual_file::{VirtualFileId, VirtualFileVersionDescription}, + data::{ + local::LocalWorkspace, + member::MemberId, + vault::virtual_file::{VirtualFileId, VirtualFileVersionDescription}, + }, }; pub type LocalFilePathBuf = PathBuf; +/// # Local Sheet +/// Local sheet information, used to record metadata of actual local files, +/// to compare with upstream information for more optimized file submission, +/// and to determine whether files need to be updated or submitted. +pub struct LocalSheet<'a> { + pub(crate) local_workspace: &'a LocalWorkspace, + pub(crate) member: MemberId, + pub(crate) sheet_name: String, + pub(crate) data: LocalSheetData, +} + #[derive(Debug, Default, Serialize, Deserialize, ConfigFile)] #[cfg_file(path = CLIENT_FILE_LOCAL_SHEET_NOSET)] // Do not use LocalSheet::write or LocalSheet::read -pub struct LocalSheet { +pub struct LocalSheetData { /// Local file path to virtual file ID mapping. #[serde(rename = "mapping")] - mapping: HashMap, // Path to VFID + pub(crate) mapping: HashMap, // Path to VFID } #[derive(Debug, Default, Serialize, Deserialize)] pub struct MappingMetaData { /// Hash value generated immediately after the file is downloaded to the local workspace #[serde(rename = "hash")] - hash_when_updated: String, + pub(crate) hash_when_updated: String, /// Time when the file was downloaded to the local workspace #[serde(rename = "date", with = "naive_date_serde")] - date_when_updated: NaiveDate, + pub(crate) date_when_updated: NaiveDate, /// Size of the file when downloaded to the local workspace #[serde(rename = "size")] - size_when_updated: u64, + pub(crate) size_when_updated: u64, /// Version description when the file was downloaded to the local workspace #[serde(rename = "version")] - version_desc_when_updated: VirtualFileVersionDescription, + pub(crate) version_desc_when_updated: VirtualFileVersionDescription, /// Virtual file ID corresponding to the local path #[serde(rename = "id")] - mapping_vfid: VirtualFileId, + pub(crate) mapping_vfid: VirtualFileId, } mod naive_date_serde { @@ -61,3 +77,74 @@ mod naive_date_serde { NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(serde::de::Error::custom) } } + +impl<'a> LocalSheet<'a> { + /// Add mapping to local sheet data + pub fn add_mapping( + &mut self, + path: LocalFilePathBuf, + mapping: MappingMetaData, + ) -> Result<(), std::io::Error> { + let path = format_path(path)?; + if self.data.mapping.contains_key(&path) { + return Err(Error::new( + std::io::ErrorKind::AlreadyExists, + "Mapping already exists", + )); + } + + self.data.mapping.insert(path, mapping); + Ok(()) + } + + /// Move mapping to other path + pub fn move_mapping( + &mut self, + from: LocalFilePathBuf, + to: LocalFilePathBuf, + ) -> Result<(), std::io::Error> { + let from = format_path(from)?; + let to = format_path(to)?; + if self.data.mapping.contains_key(&to) { + return Err(Error::new( + std::io::ErrorKind::AlreadyExists, + "To path already exists.", + )); + } + + let Some(old_value) = self.data.mapping.remove(&from) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "From path is not found.", + )); + }; + + self.data.mapping.insert(to, old_value); + + Ok(()) + } + + /// Get muttable mapping data + pub fn mapping_data_mut( + &mut self, + path: LocalFilePathBuf, + ) -> Result<&mut MappingMetaData, std::io::Error> { + let path = format_path(path)?; + let Some(data) = self.data.mapping.get_mut(&path) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "Path is not found.", + )); + }; + Ok(data) + } + + /// Persist the sheet to disk + pub async fn persist(&mut self) -> Result<(), std::io::Error> { + let _path = self + .local_workspace + .local_sheet_path(&self.member, &self.sheet_name); + LocalSheetData::write_to(&self.data, &self.sheet_name).await?; + Ok(()) + } +} diff --git a/crates/vcs_data/src/data/local/member_held.rs b/crates/vcs_data/src/data/local/member_held.rs index e9e384d..37bc18e 100644 --- a/crates/vcs_data/src/data/local/member_held.rs +++ b/crates/vcs_data/src/data/local/member_held.rs @@ -8,6 +8,8 @@ use crate::{ data::{member::MemberId, vault::virtual_file::VirtualFileId}, }; +/// # Member Held Information +/// Records the files held by the member, used for permission validation #[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigFile)] #[cfg_file(path = CLIENT_FILE_MEMBER_HELD_NOSET)] pub struct MemberHeld { -- cgit