diff options
Diffstat (limited to 'crates/vcs_data/src/data/local')
| -rw-r--r-- | crates/vcs_data/src/data/local/cached_sheet.rs | 84 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/file_status.rs | 282 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/latest_info.rs | 48 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/local_files.rs | 152 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/local_sheet.rs | 207 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/member_held.rs | 41 |
6 files changed, 763 insertions, 51 deletions
diff --git a/crates/vcs_data/src/data/local/cached_sheet.rs b/crates/vcs_data/src/data/local/cached_sheet.rs index 0f4eee9..e617922 100644 --- a/crates/vcs_data/src/data/local/cached_sheet.rs +++ b/crates/vcs_data/src/data/local/cached_sheet.rs @@ -1,17 +1,19 @@ use std::{io::Error, path::PathBuf}; use cfg_file::config::ConfigFile; -use string_proc::snake_case; +use string_proc::{format_path::format_path, snake_case}; +use tokio::fs; use crate::{ - constants::CLIENT_FILE_CACHED_SHEET, - current::current_local_path, - data::{ - member::MemberId, - sheet::{SheetData, SheetName}, + constants::{ + CLIENT_FILE_CACHED_SHEET, CLIENT_PATH_CACHED_SHEET, CLIENT_SUFFIX_CACHED_SHEET_FILE, }, + current::current_local_path, + data::sheet::{SheetData, SheetName}, }; +pub type CachedSheetPathBuf = PathBuf; + const SHEET_NAME: &str = "{sheet_name}"; const ACCOUNT_NAME: &str = "{account}"; @@ -23,14 +25,10 @@ pub struct CachedSheet; impl CachedSheet { /// Read the cached sheet data. - pub async fn cached_sheet_data( - account_name: MemberId, - sheet_name: SheetName, - ) -> Result<SheetData, std::io::Error> { - let account_name = snake_case!(account_name); - let sheet_name = snake_case!(sheet_name); - - let Some(path) = Self::cached_sheet_path(account_name, sheet_name) else { + pub async fn cached_sheet_data(sheet_name: &SheetName) -> Result<SheetData, std::io::Error> { + let sheet_name = snake_case!(sheet_name.clone()); + + let Some(path) = Self::cached_sheet_path(sheet_name) else { return Err(Error::new( std::io::ErrorKind::NotFound, "Local workspace not found!", @@ -41,14 +39,60 @@ impl CachedSheet { } /// Get the path to the cached sheet file. - pub fn cached_sheet_path(account_name: MemberId, sheet_name: SheetName) -> Option<PathBuf> { + pub fn cached_sheet_path(sheet_name: SheetName) -> Option<PathBuf> { let current_workspace = current_local_path()?; Some( - current_workspace.join( - CLIENT_FILE_CACHED_SHEET - .replace(SHEET_NAME, &sheet_name.to_string()) - .replace(ACCOUNT_NAME, &account_name.to_string()), - ), + current_workspace + .join(CLIENT_FILE_CACHED_SHEET.replace(SHEET_NAME, &sheet_name.to_string())), ) } + + /// Get all cached sheet names + pub async fn cached_sheet_names() -> Result<Vec<SheetName>, std::io::Error> { + let mut dir = fs::read_dir(CLIENT_PATH_CACHED_SHEET).await?; + let mut sheet_names = Vec::new(); + + while let Some(entry) = dir.next_entry().await? { + let path = entry.path(); + + if path.is_file() { + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + if file_name.ends_with(CLIENT_SUFFIX_CACHED_SHEET_FILE) { + let name_without_ext = file_name + .trim_end_matches(CLIENT_SUFFIX_CACHED_SHEET_FILE) + .to_string(); + sheet_names.push(name_without_ext); + } + } + } + } + + Ok(sheet_names) + } + + /// Get all cached sheet paths + pub async fn cached_sheet_paths() -> Result<Vec<CachedSheetPathBuf>, std::io::Error> { + let mut dir = fs::read_dir(CLIENT_PATH_CACHED_SHEET).await?; + let mut sheet_paths = Vec::new(); + let Some(workspace_path) = current_local_path() else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "Local workspace not found!", + )); + }; + + while let Some(entry) = dir.next_entry().await? { + let path = entry.path(); + + if path.is_file() { + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + if file_name.ends_with(CLIENT_SUFFIX_CACHED_SHEET_FILE) { + sheet_paths.push(format_path(workspace_path.join(path))?); + } + } + } + } + + Ok(sheet_paths) + } } diff --git a/crates/vcs_data/src/data/local/file_status.rs b/crates/vcs_data/src/data/local/file_status.rs new file mode 100644 index 0000000..c37c21b --- /dev/null +++ b/crates/vcs_data/src/data/local/file_status.rs @@ -0,0 +1,282 @@ +use std::{ + collections::{HashMap, HashSet}, + io::Error, + path::PathBuf, +}; + +use sha1_hash::calc_sha1_multi; +use string_proc::format_path::format_path; +use walkdir::WalkDir; + +use crate::data::{ + local::{LocalWorkspace, cached_sheet::CachedSheet, local_sheet::LocalSheet}, + member::MemberId, + sheet::{SheetData, SheetName}, + vault::virtual_file::VirtualFileId, +}; + +pub type FromRelativePathBuf = PathBuf; +pub type ToRelativePathBuf = PathBuf; +pub type CreatedRelativePathBuf = PathBuf; +pub type LostRelativePathBuf = PathBuf; +pub type ModifiedRelativePathBuf = PathBuf; + +pub struct AnalyzeResult<'a> { + local_workspace: &'a LocalWorkspace, + + /// Moved local files + pub moved: HashMap<VirtualFileId, (FromRelativePathBuf, ToRelativePathBuf)>, + + /// Newly created local files + pub created: HashSet<CreatedRelativePathBuf>, + + /// Lost local files + pub lost: HashSet<LostRelativePathBuf>, + + /// Modified local files (excluding moved files) + /// For files that were both moved and modified, changes can only be detected after LocalSheet mapping is aligned with actual files + pub modified: HashSet<ModifiedRelativePathBuf>, +} + +struct AnalyzeContext<'a> { + member: MemberId, + sheet_name: SheetName, + local_sheet: Option<LocalSheet<'a>>, + cached_sheet_data: Option<SheetData>, +} + +impl<'a> AnalyzeResult<'a> { + /// Analyze all files, calculate the file information provided + pub async fn analyze_local_status( + local_workspace: &'a LocalWorkspace, + ) -> Result<AnalyzeResult<'a>, std::io::Error> { + // Workspace + let workspace = local_workspace; + + // Current member, sheet + let (member, sheet_name) = { + let lock = workspace.config.lock().await; + let member = lock.current_account(); + let Some(sheet) = lock.sheet_in_use().clone() else { + return Err(Error::new(std::io::ErrorKind::NotFound, "Sheet not found")); + }; + (member, sheet) + }; + + // Local files (RelativePaths) + let local_path = workspace.local_path(); + let file_relative_paths = { + let mut paths = HashSet::new(); + for entry in WalkDir::new(&local_path) { + let entry = match entry { + Ok(entry) => entry, + Err(_) => continue, + }; + + // Skip entries that contain ".jv" in their path + if entry.path().to_string_lossy().contains(".jv") { + continue; + } + + if entry.file_type().is_file() { + if let Ok(relative_path) = entry.path().strip_prefix(&local_path) { + let format = format_path(relative_path.to_path_buf()); + let Ok(format) = format else { + continue; + }; + paths.insert(format); + } + } + } + + paths + }; + + // Read local sheet + let local_sheet = match workspace.local_sheet(&member, &sheet_name).await { + Ok(v) => Some(v), + Err(_) => None, + }; + + // Read cached sheet + let cached_sheet_data = match CachedSheet::cached_sheet_data(&sheet_name).await { + Ok(v) => Some(v), + Err(_) => { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "Cached sheet not found", + )); + } + }; + + // Create new result + let mut result = Self::none_result(&workspace); + + // Analyze entry + let mut analyze_ctx = AnalyzeContext { + member, + sheet_name, + local_sheet, + cached_sheet_data, + }; + Self::analyze_moved(&mut result, &file_relative_paths, &analyze_ctx).await?; + Self::analyze_modified(&mut result, &file_relative_paths, &mut analyze_ctx).await?; + + Ok(result) + } + + /// Track file moves by comparing recorded SHA1 hashes with actual file SHA1 hashes + /// For files that cannot be directly matched, continue searching using fuzzy matching algorithms + async fn analyze_moved( + result: &mut AnalyzeResult<'_>, + file_relative_paths: &HashSet<PathBuf>, + analyze_ctx: &AnalyzeContext<'a>, + ) -> Result<(), std::io::Error> { + let local_sheet_paths: HashSet<&PathBuf> = match &analyze_ctx.local_sheet { + Some(local_sheet) => local_sheet.data.mapping.keys().collect(), + None => HashSet::new(), + }; + let file_relative_paths_ref: HashSet<&PathBuf> = file_relative_paths.iter().collect(); + + // 在本地表存在但实际不存在的文件,为丢失 + let mut lost_files: HashSet<&PathBuf> = local_sheet_paths + .difference(&file_relative_paths_ref) + .cloned() + .collect(); + + // 在本地表不存在但实际存在的文件,记录为新建 + let mut new_files: HashSet<&PathBuf> = file_relative_paths_ref + .difference(&local_sheet_paths) + .cloned() + .collect(); + + // 计算新增的文件 Hash + let new_files_for_hash: Vec<PathBuf> = new_files.iter().map(|p| (*p).clone()).collect(); + let file_hashes: HashSet<(PathBuf, String)> = + match calc_sha1_multi::<PathBuf, Vec<PathBuf>>(new_files_for_hash, 8192).await { + Ok(hash) => hash, + Err(e) => return Err(Error::new(std::io::ErrorKind::Other, e)), + } + .iter() + .map(|r| (r.file_path.clone(), r.hash.to_string())) + .collect(); + + // 建立丢失文件的 Hash 映射表 + let mut lost_files_hash_mapping: HashMap<String, FromRelativePathBuf> = + match &analyze_ctx.local_sheet { + Some(local_sheet) => lost_files + .iter() + .filter_map(|f| { + local_sheet.mapping_data(f).ok().map(|mapping_data| { + (mapping_data.hash_when_updated.clone(), (*f).clone()) + }) + }) + .collect(), + None => HashMap::new(), + }; + + // 如果这些 Hash 能对应缺失文件的 Hash,那么这对新增和丢失项将被合并为移动项 + let mut moved_files: HashSet<(FromRelativePathBuf, ToRelativePathBuf)> = HashSet::new(); + for (new_path, new_hash) in file_hashes { + // 如果新的 Hash 值命中映射,则添加移动项 + if let Some(lost_path) = lost_files_hash_mapping.remove(&new_hash) { + // 移除该新增项和丢失项 + lost_files.remove(&lost_path); + new_files.remove(&new_path); + + // 建立移动项 + moved_files.insert((lost_path.clone(), new_path)); + } + } + + // 进入模糊匹配,将其他未匹配的可能移动项进行匹配 + // 如果 新增 和 缺失 数量总和能被 2 整除,则说明还存在文件被移动的可能,考虑尝试模糊匹配 + if new_files.len() + lost_files.len() % 2 == 0 { + // 尝试模糊匹配 + // ... + } + + // 将结果收集,并设置结果 + result.created = new_files.iter().map(|p| (*p).clone()).collect(); + result.lost = lost_files.iter().map(|p| (*p).clone()).collect(); + result.moved = moved_files + .iter() + .filter_map(|(from, to)| { + let vfid = analyze_ctx + .local_sheet + .as_ref() + .and_then(|local_sheet| local_sheet.mapping_data(from).ok()) + .map(|mapping_data| mapping_data.mapping_vfid.clone()); + if let Some(vfid) = vfid { + Some((vfid, (from.clone(), to.clone()))) + } else { + None + } + }) + .collect(); + + Ok(()) + } + + /// Compare using file modification time and SHA1 hash values. + /// Note: For files that have been both moved and modified, they can only be recognized as modified after their location is matched. + async fn analyze_modified( + result: &mut AnalyzeResult<'_>, + file_relative_paths: &HashSet<PathBuf>, + analyze_ctx: &mut AnalyzeContext<'a>, + ) -> Result<(), std::io::Error> { + let local_sheet = &mut analyze_ctx.local_sheet.as_mut().unwrap(); + let local_path = local_sheet.local_workspace.local_path().clone(); + + for path in file_relative_paths { + // Get mapping data + let Ok(mapping_data) = local_sheet.mapping_data_mut(&path) else { + continue; + }; + + // If modified time not changed, skip + let modified_time = std::fs::metadata(local_path.join(path))?.modified()?; + if &modified_time == mapping_data.last_modifiy_check_time() { + if mapping_data.last_modifiy_check_result() { + result.modified.insert(path.clone()); + } + continue; + } + + // Calculate hash + let hash_calc = match sha1_hash::calc_sha1(path, 2048).await { + Ok(hash) => hash, + Err(e) => return Err(Error::new(std::io::ErrorKind::Other, e)), + }; + + // If hash not match, mark as modified + if &hash_calc.hash != mapping_data.hash_when_updated() { + result.modified.insert(path.clone()); + + // Update last modified check time to modified time + mapping_data.last_modifiy_check_time = modified_time; + mapping_data.last_modifiy_check_result = true; + } else { + // Update last modified check time to modified time + mapping_data.last_modifiy_check_time = modified_time; + mapping_data.last_modifiy_check_result = false; + } + } + + // Persist the local sheet data + LocalSheet::write(local_sheet).await?; + + Ok(()) + } + + /// Generate a empty AnalyzeResult + fn none_result(local_workspace: &'a LocalWorkspace) -> AnalyzeResult<'a> { + AnalyzeResult { + local_workspace: local_workspace, + moved: HashMap::new(), + created: HashSet::new(), + lost: HashSet::new(), + modified: HashSet::new(), + } + } +} diff --git a/crates/vcs_data/src/data/local/latest_info.rs b/crates/vcs_data/src/data/local/latest_info.rs index e8fa641..e4f45b1 100644 --- a/crates/vcs_data/src/data/local/latest_info.rs +++ b/crates/vcs_data/src/data/local/latest_info.rs @@ -1,5 +1,8 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + use cfg_file::ConfigFile; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use tokio::time::Instant; use crate::{ constants::CLIENT_FILE_LATEST_INFO, @@ -23,6 +26,13 @@ pub struct LatestInfo { /// Reference sheet data, indicating what files I can get from the reference sheet pub ref_sheet_content: SheetData, + /// Update instant + #[serde( + serialize_with = "serialize_instant", + deserialize_with = "deserialize_instant" + )] + pub update_instant: Option<Instant>, + // Members /// All member information of the vault, allowing me to contact them more conveniently pub vault_members: Vec<Member>, @@ -33,3 +43,39 @@ pub struct SheetInfo { pub sheet_name: SheetName, pub holder_name: Option<MemberId>, } + +fn serialize_instant<S>(instant: &Option<Instant>, serializer: S) -> Result<S::Ok, S::Error> +where + S: Serializer, +{ + let system_now = SystemTime::now(); + let instant_now = Instant::now(); + let duration_since_epoch = instant + .as_ref() + .and_then(|i| i.checked_duration_since(instant_now)) + .map(|d| system_now.checked_add(d)) + .unwrap_or(Some(system_now)) + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .unwrap_or_else(|| SystemTime::now().duration_since(UNIX_EPOCH).unwrap()); + + serializer.serialize_u64(duration_since_epoch.as_millis() as u64) +} + +fn deserialize_instant<'de, D>(deserializer: D) -> Result<Option<Instant>, D::Error> +where + D: Deserializer<'de>, +{ + let millis = u64::deserialize(deserializer)?; + let duration_since_epoch = std::time::Duration::from_millis(millis); + let system_time = UNIX_EPOCH + duration_since_epoch; + let now_system = SystemTime::now(); + let now_instant = Instant::now(); + + if let Ok(elapsed) = system_time.elapsed() { + Ok(Some(now_instant - elapsed)) + } else if let Ok(duration_until) = system_time.duration_since(now_system) { + Ok(Some(now_instant + duration_until)) + } else { + Ok(Some(now_instant)) + } +} diff --git a/crates/vcs_data/src/data/local/local_files.rs b/crates/vcs_data/src/data/local/local_files.rs new file mode 100644 index 0000000..1599e34 --- /dev/null +++ b/crates/vcs_data/src/data/local/local_files.rs @@ -0,0 +1,152 @@ +use std::path::PathBuf; + +use string_proc::format_path::format_path; +use tokio::fs; + +use crate::constants::CLIENT_FOLDER_WORKSPACE_ROOT_NAME; + +pub struct RelativeFiles { + pub(crate) files: Vec<PathBuf>, +} + +impl IntoIterator for RelativeFiles { + type Item = PathBuf; + type IntoIter = std::vec::IntoIter<Self::Item>; + + fn into_iter(self) -> Self::IntoIter { + self.files.into_iter() + } +} + +impl RelativeFiles { + pub fn iter(&self) -> std::slice::Iter<'_, PathBuf> { + self.files.iter() + } +} + +/// Read the relative paths within the project from the input file list +pub async fn get_relative_paths(local_path: PathBuf, paths: Vec<PathBuf>) -> Option<RelativeFiles> { + // Get Relative Paths + let Ok(paths) = format_input_paths_and_ignore_outside_paths(&local_path, &paths).await else { + return None; + }; + let files: Vec<PathBuf> = abs_paths_to_abs_files(paths).await; + let Ok(files) = parse_to_relative(&local_path, files) else { + return None; + }; + Some(RelativeFiles { files }) +} + +/// Normalize the input paths +async fn format_input_paths( + local_path: &PathBuf, + track_files: &[PathBuf], +) -> Result<Vec<PathBuf>, std::io::Error> { + let current_dir = local_path; + + let mut real_paths = Vec::new(); + for file in track_files { + let path = current_dir.join(file); + + // Skip paths that contain .jv directories + if path.components().any(|component| { + if let std::path::Component::Normal(name) = component { + name.to_str() + .map_or(false, |s| s == CLIENT_FOLDER_WORKSPACE_ROOT_NAME) + } else { + false + } + }) { + continue; + } + + match format_path(path) { + Ok(path) => real_paths.push(path), + Err(e) => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to format path: {}", e), + )); + } + } + } + + Ok(real_paths) +} + +/// Ignore files outside the workspace +async fn format_input_paths_and_ignore_outside_paths( + local_path: &PathBuf, + files: &[PathBuf], +) -> Result<Vec<PathBuf>, std::io::Error> { + let result = format_input_paths(local_path, files).await?; + let result: Vec<PathBuf> = result + .into_iter() + .filter(|path| path.starts_with(local_path)) + .collect(); + Ok(result) +} + +/// Normalize the input paths to relative paths +fn parse_to_relative( + local_dir: &PathBuf, + files: Vec<PathBuf>, +) -> Result<Vec<PathBuf>, std::io::Error> { + let result: Result<Vec<PathBuf>, _> = files + .iter() + .map(|p| { + p.strip_prefix(local_dir) + .map(|relative| relative.to_path_buf()) + .map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Path prefix stripping failed", + ) + }) + }) + .collect(); + + match result { + Ok(paths) => Ok(paths), + Err(e) => Err(e), + } +} + +/// Convert absolute paths to absolute file paths, expanding directories to their contained files +async fn abs_paths_to_abs_files(paths: Vec<PathBuf>) -> Vec<PathBuf> { + let mut files = Vec::new(); + + for path in paths { + if !path.exists() { + continue; + } + + let metadata = match fs::metadata(&path).await { + Ok(meta) => meta, + Err(_) => continue, + }; + + if metadata.is_file() { + files.push(path); + } else if metadata.is_dir() { + let walker = walkdir::WalkDir::new(&path); + for entry in walker.into_iter().filter_map(|e| e.ok()) { + if entry.path().components().any(|component| { + if let std::path::Component::Normal(name) = component { + name == CLIENT_FOLDER_WORKSPACE_ROOT_NAME + } else { + false + } + }) { + continue; + } + + if entry.file_type().is_file() { + files.push(entry.path().to_path_buf()); + } + } + } + } + + files +} diff --git a/crates/vcs_data/src/data/local/local_sheet.rs b/crates/vcs_data/src/data/local/local_sheet.rs index bfe8d01..980fa49 100644 --- a/crates/vcs_data/src/data/local/local_sheet.rs +++ b/crates/vcs_data/src/data/local/local_sheet.rs @@ -1,8 +1,7 @@ -use std::{collections::HashMap, io::Error, path::PathBuf}; +use std::{collections::HashMap, io::Error, path::PathBuf, time::SystemTime}; use ::serde::{Deserialize, Serialize}; use cfg_file::{ConfigFile, config::ConfigFile}; -use chrono::NaiveDate; use string_proc::format_path::format_path; use crate::{ @@ -10,11 +9,13 @@ use crate::{ data::{ local::LocalWorkspace, member::MemberId, - vault::virtual_file::{VirtualFileId, VirtualFileVersionDescription}, + sheet::SheetName, + vault::virtual_file::{VirtualFileId, VirtualFileVersion, VirtualFileVersionDescription}, }, }; pub type LocalFilePathBuf = PathBuf; +pub type LocalSheetPathBuf = PathBuf; /// # Local Sheet /// Local sheet information, used to record metadata of actual local files, @@ -27,54 +28,168 @@ pub struct LocalSheet<'a> { pub(crate) data: LocalSheetData, } -#[derive(Debug, Default, Serialize, Deserialize, ConfigFile)] +#[derive(Debug, Default, Serialize, Deserialize, ConfigFile, Clone)] #[cfg_file(path = CLIENT_FILE_LOCAL_SHEET_NOSET)] // Do not use LocalSheet::write or LocalSheet::read pub struct LocalSheetData { - /// Local file path to virtual file ID mapping. + /// Local file path to metadata mapping. #[serde(rename = "mapping")] - pub(crate) mapping: HashMap<LocalFilePathBuf, MappingMetaData>, // Path to VFID + pub(crate) mapping: HashMap<LocalFilePathBuf, LocalMappingMetadata>, + + pub(crate) vfs: HashMap<VirtualFileId, LocalFilePathBuf>, } -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct MappingMetaData { +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LocalMappingMetadata { /// Hash value generated immediately after the file is downloaded to the local workspace #[serde(rename = "hash")] pub(crate) hash_when_updated: String, /// Time when the file was downloaded to the local workspace - #[serde(rename = "date", with = "naive_date_serde")] - pub(crate) date_when_updated: NaiveDate, + pub(crate) time_when_updated: SystemTime, /// Size of the file when downloaded to the local workspace #[serde(rename = "size")] pub(crate) size_when_updated: u64, /// Version description when the file was downloaded to the local workspace - #[serde(rename = "version")] + #[serde(rename = "version_desc")] pub(crate) version_desc_when_updated: VirtualFileVersionDescription, + /// Version when the file was downloaded to the local workspace + #[serde(rename = "version")] + pub(crate) version_when_updated: VirtualFileVersion, + /// Virtual file ID corresponding to the local path #[serde(rename = "id")] pub(crate) mapping_vfid: VirtualFileId, + + /// Latest modifiy check time + pub(crate) last_modifiy_check_time: SystemTime, + + /// Latest modifiy check result + pub(crate) last_modifiy_check_result: bool, +} + +impl LocalSheetData { + /// Wrap LocalSheetData into LocalSheet with workspace, member, and sheet name + pub fn wrap_to_local_sheet<'a>( + self, + workspace: &'a LocalWorkspace, + member: MemberId, + sheet_name: SheetName, + ) -> LocalSheet<'a> { + LocalSheet { + local_workspace: workspace, + member, + sheet_name, + data: self, + } + } +} + +impl LocalMappingMetadata { + /// Create a new MappingMetaData instance + pub fn new( + hash_when_updated: String, + time_when_updated: SystemTime, + size_when_updated: u64, + version_desc_when_updated: VirtualFileVersionDescription, + version_when_updated: VirtualFileVersion, + mapping_vfid: VirtualFileId, + last_modifiy_check_time: SystemTime, + last_modifiy_check_result: bool, + ) -> Self { + Self { + hash_when_updated, + time_when_updated, + size_when_updated, + version_desc_when_updated, + version_when_updated, + mapping_vfid, + last_modifiy_check_time, + last_modifiy_check_result, + } + } + + /// Getter for hash_when_updated + pub fn hash_when_updated(&self) -> &String { + &self.hash_when_updated + } + + /// Getter for date_when_updated + pub fn time_when_updated(&self) -> &SystemTime { + &self.time_when_updated + } + + /// Getter for size_when_updated + pub fn size_when_updated(&self) -> u64 { + self.size_when_updated + } + + /// Getter for version_desc_when_updated + pub fn version_desc_when_updated(&self) -> &VirtualFileVersionDescription { + &self.version_desc_when_updated + } + + /// Getter for version_when_updated + pub fn version_when_updated(&self) -> &VirtualFileVersion { + &self.version_when_updated + } + + /// Getter for mapping_vfid + pub fn mapping_vfid(&self) -> &VirtualFileId { + &self.mapping_vfid + } + + /// Getter for last_modifiy_check_time + pub fn last_modifiy_check_time(&self) -> &SystemTime { + &self.last_modifiy_check_time + } + + /// Getter for last_modifiy_check_result + pub fn last_modifiy_check_result(&self) -> bool { + self.last_modifiy_check_result + } +} + +impl Default for LocalMappingMetadata { + fn default() -> Self { + Self { + hash_when_updated: Default::default(), + time_when_updated: SystemTime::now(), + size_when_updated: Default::default(), + version_desc_when_updated: Default::default(), + version_when_updated: Default::default(), + mapping_vfid: Default::default(), + last_modifiy_check_time: SystemTime::now(), + last_modifiy_check_result: false, + } + } } -mod naive_date_serde { - use chrono::NaiveDate; +mod instant_serde { use serde::{self, Deserialize, Deserializer, Serializer}; + use tokio::time::Instant; - pub fn serialize<S>(date: &NaiveDate, serializer: S) -> Result<S::Ok, S::Error> + pub fn serialize<S>(instant: &Instant, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { - serializer.serialize_str(&date.format("%Y-%m-%d").to_string()) + serializer.serialize_u64(instant.elapsed().as_secs()) } - pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error> + pub fn deserialize<'de, D>(deserializer: D) -> Result<Instant, D::Error> where D: Deserializer<'de>, { - let s = String::deserialize(deserializer)?; - NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(serde::de::Error::custom) + let secs = u64::deserialize(deserializer)?; + Ok(Instant::now() - std::time::Duration::from_secs(secs)) + } +} + +impl<'a> From<&'a LocalSheet<'a>> for &'a LocalSheetData { + fn from(sheet: &'a LocalSheet<'a>) -> Self { + &sheet.data } } @@ -83,10 +198,12 @@ impl<'a> LocalSheet<'a> { pub fn add_mapping( &mut self, path: LocalFilePathBuf, - mapping: MappingMetaData, + mapping: LocalMappingMetadata, ) -> Result<(), std::io::Error> { let path = format_path(path)?; - if self.data.mapping.contains_key(&path) { + if self.data.mapping.contains_key(&path) + || self.data.vfs.contains_key(&mapping.mapping_vfid) + { return Err(Error::new( std::io::ErrorKind::AlreadyExists, "Mapping already exists", @@ -100,8 +217,8 @@ impl<'a> LocalSheet<'a> { /// Move mapping to other path pub fn move_mapping( &mut self, - from: LocalFilePathBuf, - to: LocalFilePathBuf, + from: &LocalFilePathBuf, + to: &LocalFilePathBuf, ) -> Result<(), std::io::Error> { let from = format_path(from)?; let to = format_path(to)?; @@ -124,11 +241,26 @@ impl<'a> LocalSheet<'a> { Ok(()) } + /// Get immutable mapping data + pub fn mapping_data( + &self, + path: &LocalFilePathBuf, + ) -> Result<&LocalMappingMetadata, std::io::Error> { + let path = format_path(path)?; + let Some(data) = self.data.mapping.get(&path) else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "Path is not found.", + )); + }; + Ok(data) + } + /// Get muttable mapping data pub fn mapping_data_mut( &mut self, - path: LocalFilePathBuf, - ) -> Result<&mut MappingMetaData, std::io::Error> { + path: &LocalFilePathBuf, + ) -> Result<&mut LocalMappingMetadata, std::io::Error> { let path = format_path(path)?; let Some(data) = self.data.mapping.get_mut(&path) else { return Err(Error::new( @@ -139,12 +271,31 @@ impl<'a> LocalSheet<'a> { Ok(data) } - /// Persist the sheet to disk - pub async fn persist(&mut self) -> Result<(), std::io::Error> { - let _path = self + /// Write the sheet to disk + pub async fn write(&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?; + self.write_to_path(path).await + } + + /// Write the sheet to custom path + pub async fn write_to_path(&mut self, path: impl Into<PathBuf>) -> Result<(), std::io::Error> { + let path = path.into(); + + self.data.vfs = HashMap::new(); + for (path, mapping) in self.data.mapping.iter() { + self.data + .vfs + .insert(mapping.mapping_vfid.clone(), path.clone()); + } + + LocalSheetData::write_to(&self.data, path).await?; Ok(()) } + + /// Get path by VirtualFileId + pub fn path_by_id(&self, vfid: &VirtualFileId) -> Option<&PathBuf> { + self.data.vfs.get(vfid) + } } diff --git a/crates/vcs_data/src/data/local/member_held.rs b/crates/vcs_data/src/data/local/member_held.rs index 37bc18e..3f07232 100644 --- a/crates/vcs_data/src/data/local/member_held.rs +++ b/crates/vcs_data/src/data/local/member_held.rs @@ -1,13 +1,16 @@ -use std::collections::HashMap; +use std::{collections::HashMap, io::Error, path::PathBuf}; use cfg_file::ConfigFile; use serde::{Deserialize, Serialize}; use crate::{ - constants::CLIENT_FILE_MEMBER_HELD_NOSET, + constants::{CLIENT_FILE_MEMBER_HELD, CLIENT_FILE_MEMBER_HELD_NOSET}, + current::current_local_path, data::{member::MemberId, vault::virtual_file::VirtualFileId}, }; +const ACCOUNT: &str = "{account}"; + /// # Member Held Information /// Records the files held by the member, used for permission validation #[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigFile)] @@ -25,3 +28,37 @@ pub enum HeldStatus { #[default] WantedToKnow, // Holding status is unknown, notify server must inform client } + +impl MemberHeld { + /// Get the path to the file holding the held status information for the given member. + pub fn held_file_path(account: &MemberId) -> Result<PathBuf, std::io::Error> { + let Some(local_path) = current_local_path() else { + return Err(Error::new( + std::io::ErrorKind::NotFound, + "Workspace not found.", + )); + }; + Ok(local_path.join(CLIENT_FILE_MEMBER_HELD.replace(ACCOUNT, account))) + } + + /// Get the member who holds the file with the given ID. + pub fn file_holder(&self, vfid: &VirtualFileId) -> Option<&MemberId> { + self.held_status.get(vfid).and_then(|status| match status { + HeldStatus::HeldWith(id) => Some(id), + _ => None, + }) + } + + /// Update the held status of the files. + pub fn update_held_status(&mut self, map: HashMap<VirtualFileId, Option<MemberId>>) { + for (vfid, member_id) in map { + self.held_status.insert( + vfid, + match member_id { + Some(member_id) => HeldStatus::HeldWith(member_id), + None => HeldStatus::NotHeld, + }, + ); + } + } +} |
