diff options
Diffstat (limited to 'crates/vcs_data/src/data/local')
| -rw-r--r-- | crates/vcs_data/src/data/local/align.rs | 110 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/cached_sheet.rs | 94 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/config.rs | 375 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/latest_file_data.rs | 105 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/latest_info.rs | 83 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/local_files.rs | 148 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/local_sheet.rs | 377 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/vault_modified.rs | 30 | ||||
| -rw-r--r-- | crates/vcs_data/src/data/local/workspace_analyzer.rs | 327 |
9 files changed, 0 insertions, 1649 deletions
diff --git a/crates/vcs_data/src/data/local/align.rs b/crates/vcs_data/src/data/local/align.rs deleted file mode 100644 index b72804c..0000000 --- a/crates/vcs_data/src/data/local/align.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - path::PathBuf, -}; - -use data_struct::data_sort::quick_sort_with_cmp; - -use crate::data::local::workspace_analyzer::AnalyzeResult; - -pub type AlignTaskName = String; -pub type AlignPathBuf = PathBuf; -pub type AlignLostPathBuf = PathBuf; -pub type AlignCreatedPathBuf = PathBuf; - -pub struct AlignTasks { - pub created: Vec<(AlignTaskName, AlignPathBuf)>, - pub lost: Vec<(AlignTaskName, AlignPathBuf)>, - pub moved: Vec<(AlignTaskName, (AlignLostPathBuf, AlignCreatedPathBuf))>, - pub erased: Vec<(AlignTaskName, AlignPathBuf)>, -} - -impl AlignTasks { - pub fn clone_from_analyze_result(result: &AnalyzeResult) -> Self { - AlignTasks { - created: path_hash_set_sort_helper(result.created.clone(), "created"), - lost: path_hash_set_sort_helper(result.lost.clone(), "lost"), - moved: path_hash_map_sort_helper(result.moved.clone(), "moved"), - erased: path_hash_set_sort_helper(result.erased.clone(), "erased"), - } - } - - pub fn from_analyze_result(result: AnalyzeResult) -> Self { - AlignTasks { - created: path_hash_set_sort_helper(result.created, "created"), - lost: path_hash_set_sort_helper(result.lost, "lost"), - moved: path_hash_map_sort_helper(result.moved, "moved"), - erased: path_hash_set_sort_helper(result.erased, "erased"), - } - } -} - -fn path_hash_set_sort_helper( - hash_set: HashSet<PathBuf>, - prefix: impl Into<String>, -) -> Vec<(String, PathBuf)> { - let prefix_str = prefix.into(); - let mut vec: Vec<(String, PathBuf)> = hash_set - .into_iter() - .map(|path| { - let hash = sha1_hash::calc_sha1_string(path.to_string_lossy()); - let hash_prefix: String = hash.chars().take(8).collect(); - let name = format!("{}:{}", prefix_str, hash_prefix); - (name, path) - }) - .collect(); - - quick_sort_with_cmp(&mut vec, false, |a, b| { - // Compare by path depth first - let a_depth = a.1.components().count(); - let b_depth = b.1.components().count(); - - if a_depth != b_depth { - return if a_depth < b_depth { -1 } else { 1 }; - } - - // If same depth, compare lexicographically - match a.1.cmp(&b.1) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - } - }); - - vec -} - -fn path_hash_map_sort_helper( - hash_map: HashMap<String, (PathBuf, PathBuf)>, - prefix: impl Into<String>, -) -> Vec<(String, (PathBuf, PathBuf))> { - let prefix_str = prefix.into(); - let mut vec: Vec<(String, (PathBuf, PathBuf))> = hash_map - .into_values() - .map(|(path1, path2)| { - let hash = sha1_hash::calc_sha1_string(path1.to_string_lossy()); - let hash_prefix: String = hash.chars().take(8).collect(); - let name = format!("{}:{}", prefix_str, hash_prefix); - (name, (path1, path2)) - }) - .collect(); - - quick_sort_with_cmp(&mut vec, false, |a, b| { - // Compare by first PathBuf's path depth first - let a_depth = a.1.0.components().count(); - let b_depth = b.1.0.components().count(); - - if a_depth != b_depth { - return if a_depth < b_depth { -1 } else { 1 }; - } - - // If same depth, compare lexicographically by first PathBuf - match a.1.0.cmp(&b.1.0) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - } - }); - - vec -} diff --git a/crates/vcs_data/src/data/local/cached_sheet.rs b/crates/vcs_data/src/data/local/cached_sheet.rs deleted file mode 100644 index 39f9814..0000000 --- a/crates/vcs_data/src/data/local/cached_sheet.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::{io::Error, path::PathBuf}; - -use cfg_file::config::ConfigFile; -use string_proc::{format_path::format_path, snake_case}; -use tokio::fs; - -use crate::{ - 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}"; - -/// # Cached Sheet -/// The cached sheet is a read-only version cloned from the upstream repository to the local environment, -/// automatically generated during update operations, -/// which records the latest Sheet information stored locally to accelerate data access and reduce network requests. -pub struct CachedSheet; - -impl CachedSheet { - /// Read the cached sheet data. - 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!", - )); - }; - let data = SheetData::read_from(path).await?; - Ok(data) - } - - /// Get the path to the cached sheet file. - 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())), - ) - } - - /// 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() - && let Some(file_name) = path.file_name().and_then(|n| n.to_str()) - && 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() - && let Some(file_name) = path.file_name().and_then(|n| n.to_str()) - && 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/config.rs b/crates/vcs_data/src/data/local/config.rs deleted file mode 100644 index 8a89c20..0000000 --- a/crates/vcs_data/src/data/local/config.rs +++ /dev/null @@ -1,375 +0,0 @@ -use cfg_file::ConfigFile; -use cfg_file::config::ConfigFile; -use serde::{Deserialize, Serialize}; -use std::io::Error; -use std::net::SocketAddr; -use std::path::Path; -use std::path::PathBuf; -use string_proc::snake_case; - -use crate::constants::CLIENT_FILE_WORKSPACE; -use crate::constants::CLIENT_FOLDER_WORKSPACE_ROOT_NAME; -use crate::constants::CLIENT_PATH_LOCAL_DRAFT; -use crate::constants::CLIENT_PATH_WORKSPACE_ROOT; -use crate::constants::PORT; -use crate::current::current_local_path; -use crate::data::local::latest_info::LatestInfo; -use crate::data::member::MemberId; -use crate::data::sheet::SheetName; -use crate::data::vault::config::VaultUuid; - -const ACCOUNT: &str = "{account}"; -const SHEET_NAME: &str = "{sheet_name}"; - -#[derive(Serialize, Deserialize, ConfigFile, Clone)] -#[cfg_file(path = CLIENT_FILE_WORKSPACE)] -pub struct LocalConfig { - /// The upstream address, representing the upstream address of the local workspace, - /// to facilitate timely retrieval of new updates from the upstream source. - #[serde(rename = "addr")] - upstream_addr: SocketAddr, - - /// The member ID used by the current local workspace. - /// This ID will be used to verify access permissions when connecting to the upstream server. - #[serde(rename = "as")] - using_account: MemberId, - - /// Whether the current member is interacting as a host. - /// In host mode, full Vault operation permissions are available except for adding new content. - #[serde(rename = "host")] - using_host_mode: bool, - - /// Whether the local workspace is stained. - /// - /// If stained, it can only set an upstream server with the same identifier. - /// - /// If the value is None, it means not stained; - /// otherwise, it contains the stain identifier (i.e., the upstream vault's unique ID) - #[serde(rename = "up_uid")] - stained_uuid: Option<VaultUuid>, - - /// The name of the sheet currently in use. - #[serde(rename = "use")] - sheet_in_use: Option<SheetName>, -} - -impl Default for LocalConfig { - fn default() -> Self { - Self { - upstream_addr: SocketAddr::V4(std::net::SocketAddrV4::new( - std::net::Ipv4Addr::new(127, 0, 0, 1), - PORT, - )), - using_account: "unknown".to_string(), - using_host_mode: false, - stained_uuid: None, - sheet_in_use: None, - } - } -} - -impl LocalConfig { - /// Set the vault address. - pub fn set_vault_addr(&mut self, addr: SocketAddr) { - self.upstream_addr = addr; - } - - /// Get the vault address. - pub fn vault_addr(&self) -> SocketAddr { - self.upstream_addr - } - - /// Set the currently used account - pub fn set_current_account(&mut self, account: MemberId) -> Result<(), std::io::Error> { - if self.sheet_in_use().is_some() { - return Err(Error::new( - std::io::ErrorKind::DirectoryNotEmpty, - "Please exit the current sheet before switching accounts", - )); - } - self.using_account = account; - Ok(()) - } - - /// Set the host mode - pub fn set_host_mode(&mut self, host_mode: bool) { - self.using_host_mode = host_mode; - } - - /// Set the currently used sheet - pub async fn use_sheet(&mut self, sheet: SheetName) -> Result<(), std::io::Error> { - let sheet = snake_case!(sheet); - - // Check if the sheet is already in use - if self.sheet_in_use().is_some() { - return Err(std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - "Sheet already in use", - )); - }; - - // Check if the local path exists - let local_path = self.get_local_path().await?; - - // Get latest info - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_path, - &self.current_account(), - )) - .await - else { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "No latest info found", - )); - }; - - // Check if the sheet exists - if !latest_info.visible_sheets.contains(&sheet) { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Sheet not found", - )); - } - - // Check if there are any files or folders other than .jv - self.check_local_path_empty(&local_path).await?; - - // Get the draft folder path - let draft_folder = self.draft_folder(&self.using_account, &sheet, &local_path); - - if draft_folder.exists() { - // Exists - // Move the contents of the draft folder to the local path with rollback support - self.move_draft_to_local(&draft_folder, &local_path).await?; - } - - self.sheet_in_use = Some(sheet); - LocalConfig::write(self).await?; - - Ok(()) - } - - /// Exit the currently used sheet - pub async fn exit_sheet(&mut self) -> Result<(), std::io::Error> { - // Check if the sheet is already in use - if self.sheet_in_use().is_none() { - return Ok(()); - } - - // Check if the local path exists - let local_path = self.get_local_path().await?; - - // Get the current sheet name - let sheet_name = self.sheet_in_use().as_ref().unwrap().clone(); - - // Get the draft folder path - let draft_folder = self.draft_folder(&self.using_account, &sheet_name, &local_path); - - // Create the draft folder if it doesn't exist - if !draft_folder.exists() { - std::fs::create_dir_all(&draft_folder).map_err(std::io::Error::other)?; - } - - // Move all files and folders (except .jv folder) to the draft folder with rollback support - self.move_local_to_draft(&local_path, &draft_folder).await?; - - // Clear the sheet in use - self.sheet_in_use = None; - LocalConfig::write(self).await?; - - Ok(()) - } - - /// Get local path or return error - async fn get_local_path(&self) -> Result<PathBuf, std::io::Error> { - current_local_path().ok_or_else(|| { - std::io::Error::new(std::io::ErrorKind::NotFound, "Fail to get local path") - }) - } - - /// Check if local path is empty (except for .jv folder) - async fn check_local_path_empty(&self, local_path: &Path) -> Result<(), std::io::Error> { - let jv_folder = local_path.join(CLIENT_PATH_WORKSPACE_ROOT); - let mut entries = std::fs::read_dir(local_path).map_err(std::io::Error::other)?; - - if entries.any(|entry| { - if let Ok(entry) = entry { - let path = entry.path(); - path != jv_folder - && path.file_name().and_then(|s| s.to_str()) - != Some(CLIENT_FOLDER_WORKSPACE_ROOT_NAME) - } else { - false - } - }) { - return Err(std::io::Error::new( - std::io::ErrorKind::DirectoryNotEmpty, - "Local path is not empty!", - )); - } - - Ok(()) - } - - /// Move contents from draft folder to local path with rollback support - async fn move_draft_to_local( - &self, - draft_folder: &Path, - local_path: &Path, - ) -> Result<(), std::io::Error> { - let draft_entries: Vec<_> = std::fs::read_dir(draft_folder) - .map_err(std::io::Error::other)? - .collect::<Result<Vec<_>, _>>() - .map_err(std::io::Error::other)?; - - let mut moved_items: Vec<MovedItem> = Vec::new(); - - for entry in &draft_entries { - let entry_path = entry.path(); - let target_path = local_path.join(entry_path.file_name().unwrap()); - - // Move each file/directory from draft folder to local path - std::fs::rename(&entry_path, &target_path).map_err(|e| { - // Rollback all previously moved items - for moved_item in &moved_items { - let _ = std::fs::rename(&moved_item.target, &moved_item.source); - } - std::io::Error::other(e) - })?; - - moved_items.push(MovedItem { - source: entry_path.clone(), - target: target_path.clone(), - }); - } - - // Remove the now-empty draft folder - std::fs::remove_dir(draft_folder).map_err(|e| { - // Rollback all moved items if folder removal fails - for moved_item in &moved_items { - let _ = std::fs::rename(&moved_item.target, &moved_item.source); - } - std::io::Error::other(e) - })?; - - Ok(()) - } - - /// Move contents from local path to draft folder with rollback support (except .jv folder) - async fn move_local_to_draft( - &self, - local_path: &Path, - draft_folder: &Path, - ) -> Result<(), std::io::Error> { - let jv_folder = local_path.join(CLIENT_PATH_WORKSPACE_ROOT); - let entries: Vec<_> = std::fs::read_dir(local_path) - .map_err(std::io::Error::other)? - .collect::<Result<Vec<_>, _>>() - .map_err(std::io::Error::other)?; - - let mut moved_items: Vec<MovedItem> = Vec::new(); - - for entry in &entries { - let entry_path = entry.path(); - - // Skip the .jv folder - if entry_path == jv_folder - || entry_path.file_name().and_then(|s| s.to_str()) - == Some(CLIENT_FOLDER_WORKSPACE_ROOT_NAME) - { - continue; - } - - let target_path = draft_folder.join(entry_path.file_name().unwrap()); - - // Move each file/directory from local path to draft folder - std::fs::rename(&entry_path, &target_path).map_err(|e| { - // Rollback all previously moved items - for moved_item in &moved_items { - let _ = std::fs::rename(&moved_item.target, &moved_item.source); - } - std::io::Error::other(e) - })?; - - moved_items.push(MovedItem { - source: entry_path.clone(), - target: target_path.clone(), - }); - } - - Ok(()) - } - - /// Get the currently used account - pub fn current_account(&self) -> MemberId { - self.using_account.clone() - } - - /// Check if the current member is interacting as a host. - pub fn is_host_mode(&self) -> bool { - self.using_host_mode - } - - /// Check if the local workspace is stained. - pub fn stained(&self) -> bool { - self.stained_uuid.is_some() - } - - /// Get the UUID of the vault that the local workspace is stained with. - pub fn stained_uuid(&self) -> Option<VaultUuid> { - self.stained_uuid - } - - /// Stain the local workspace with the given UUID. - pub fn stain(&mut self, uuid: VaultUuid) { - self.stained_uuid = Some(uuid); - } - - /// Unstain the local workspace. - pub fn unstain(&mut self) { - self.stained_uuid = None; - } - - /// Get the upstream address. - pub fn upstream_addr(&self) -> SocketAddr { - self.upstream_addr - } - - /// Get the currently used sheet - pub fn sheet_in_use(&self) -> &Option<SheetName> { - &self.sheet_in_use - } - - /// Get draft folder - pub fn draft_folder( - &self, - account: &MemberId, - sheet_name: &SheetName, - local_workspace_path: impl Into<PathBuf>, - ) -> PathBuf { - let account_str = snake_case!(account.as_str()); - let sheet_name_str = snake_case!(sheet_name.as_str()); - let draft_path = CLIENT_PATH_LOCAL_DRAFT - .replace(ACCOUNT, &account_str) - .replace(SHEET_NAME, &sheet_name_str); - local_workspace_path.into().join(draft_path) - } - - /// Get current draft folder - pub fn current_draft_folder(&self) -> Option<PathBuf> { - let Some(sheet_name) = self.sheet_in_use() else { - return None; - }; - - let current_dir = current_local_path()?; - - Some(self.draft_folder(&self.using_account, sheet_name, current_dir)) - } -} - -#[derive(Clone)] -struct MovedItem { - source: PathBuf, - target: PathBuf, -} diff --git a/crates/vcs_data/src/data/local/latest_file_data.rs b/crates/vcs_data/src/data/local/latest_file_data.rs deleted file mode 100644 index 21c647c..0000000 --- a/crates/vcs_data/src/data/local/latest_file_data.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::{collections::HashMap, io::Error, path::PathBuf}; - -use cfg_file::ConfigFile; -use serde::{Deserialize, Serialize}; - -use crate::{ - constants::{CLIENT_FILE_LATEST_DATA, CLIENT_FILE_MEMBER_HELD_NOSET}, - current::current_local_path, - data::{ - member::MemberId, - vault::virtual_file::{VirtualFileId, VirtualFileVersion, VirtualFileVersionDescription}, - }, -}; - -const ACCOUNT: &str = "{account}"; - -/// # Latest file data -/// Records the file holder and the latest version for permission and update checks -#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigFile)] -#[cfg_file(path = CLIENT_FILE_MEMBER_HELD_NOSET)] -pub struct LatestFileData { - /// File holding status - #[serde(rename = "held")] - held_status: HashMap<VirtualFileId, HeldStatus>, - - /// File version - #[serde(rename = "ver")] - versions: HashMap<VirtualFileId, VirtualFileVersion>, - - /// File histories and descriptions - #[serde(rename = "his")] - histories: HashMap<VirtualFileId, Vec<(VirtualFileVersion, VirtualFileVersionDescription)>>, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub enum HeldStatus { - #[serde(rename = "Hold")] - HeldWith(MemberId), // Held, status changes are sync to the client - - #[serde(rename = "None")] - NotHeld, // Not held, status changes are sync to the client - - #[default] - #[serde(rename = "Unknown")] - WantedToKnow, // Holding status is unknown, notify server must inform client -} - -impl LatestFileData { - /// Get the path to the file holding the held status information for the given member. - pub fn data_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_LATEST_DATA.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, - }) - } - - /// Get the version of the file with the given ID. - pub fn file_version(&self, vfid: &VirtualFileId) -> Option<&VirtualFileVersion> { - self.versions.get(vfid) - } - - /// Get the version of the file with the given ID. - pub fn file_histories( - &self, - vfid: &VirtualFileId, - ) -> Option<&Vec<(VirtualFileVersion, VirtualFileVersionDescription)>> { - self.histories.get(vfid) - } - - /// Update the held status of the files. - pub fn update_info( - &mut self, - map: HashMap< - VirtualFileId, - ( - Option<MemberId>, - VirtualFileVersion, - Vec<(VirtualFileVersion, VirtualFileVersionDescription)>, - ), - >, - ) { - for (vfid, (member_id, version, desc)) in map { - self.held_status.insert( - vfid.clone(), - match member_id { - Some(member_id) => HeldStatus::HeldWith(member_id), - None => HeldStatus::NotHeld, - }, - ); - self.versions.insert(vfid.clone(), version); - self.histories.insert(vfid, desc); - } - } -} diff --git a/crates/vcs_data/src/data/local/latest_info.rs b/crates/vcs_data/src/data/local/latest_info.rs deleted file mode 100644 index e11836b..0000000 --- a/crates/vcs_data/src/data/local/latest_info.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - path::{Path, PathBuf}, - time::SystemTime, -}; - -use cfg_file::ConfigFile; -use serde::{Deserialize, Serialize}; - -use crate::{ - constants::{CLIENT_FILE_LATEST_INFO, CLIENT_FILE_LATEST_INFO_NOSET}, - data::{ - member::{Member, MemberId}, - sheet::{SheetData, SheetName, SheetPathBuf}, - vault::{ - sheet_share::{Share, SheetShareId}, - virtual_file::VirtualFileId, - }, - }, -}; - -const ACCOUNT: &str = "{account}"; - -/// # Latest Info -/// Locally cached latest information, -/// used to cache personal information from upstream for querying and quickly retrieving member information. -#[derive(Default, Serialize, Deserialize, ConfigFile)] -#[cfg_file(path = CLIENT_FILE_LATEST_INFO_NOSET)] -pub struct LatestInfo { - // Sheets - /// Visible sheets, - /// indicating which sheets I can edit - #[serde(rename = "my")] - pub visible_sheets: Vec<SheetName>, - - /// Invisible sheets, - /// indicating which sheets I can export files to (these sheets are not readable to me) - #[serde(rename = "others")] - pub invisible_sheets: Vec<SheetInfo>, - - /// Reference sheets, - /// indicating sheets owned by the host, visible to everyone, - /// but only the host can modify or add mappings within them - #[serde(rename = "refsheets")] - pub reference_sheets: HashSet<SheetName>, - - /// Reference sheet data, indicating what files I can get from the reference sheet - #[serde(rename = "ref")] - pub ref_sheet_content: SheetData, - - /// Reverse mapping from virtual file IDs to actual paths in reference sheets - #[serde(rename = "ref_vfs")] - pub ref_sheet_vfs_mapping: HashMap<VirtualFileId, SheetPathBuf>, - - /// Shares in my sheets, indicating which external merge requests have entries that I can view - #[serde(rename = "shares")] - pub shares_in_my_sheets: HashMap<SheetName, HashMap<SheetShareId, Share>>, - - /// Update instant - #[serde(rename = "update")] - pub update_instant: Option<SystemTime>, - - // Members - /// All member information of the vault, allowing me to contact them more conveniently - #[serde(rename = "members")] - pub vault_members: Vec<Member>, -} - -impl LatestInfo { - /// Get the path to the latest info file for a given workspace and member ID - pub fn latest_info_path(local_workspace_path: &Path, member_id: &MemberId) -> PathBuf { - local_workspace_path.join(CLIENT_FILE_LATEST_INFO.replace(ACCOUNT, member_id)) - } -} - -#[derive(Default, Serialize, Deserialize)] -pub struct SheetInfo { - #[serde(rename = "name")] - pub sheet_name: SheetName, - - #[serde(rename = "holder")] - pub holder_name: Option<MemberId>, -} diff --git a/crates/vcs_data/src/data/local/local_files.rs b/crates/vcs_data/src/data/local/local_files.rs deleted file mode 100644 index 9cc244f..0000000 --- a/crates/vcs_data/src/data/local/local_files.rs +++ /dev/null @@ -1,148 +0,0 @@ -use std::path::{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: &[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: &Path, - 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() == Some(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(); - - result -} - -/// 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 deleted file mode 100644 index 6f9924c..0000000 --- a/crates/vcs_data/src/data/local/local_sheet.rs +++ /dev/null @@ -1,377 +0,0 @@ -use std::{collections::HashMap, io::Error, path::PathBuf, time::SystemTime}; - -use ::serde::{Deserialize, Serialize}; -use cfg_file::{ConfigFile, config::ConfigFile}; -use string_proc::format_path::format_path; - -use crate::{ - constants::CLIENT_FILE_LOCAL_SHEET_NOSET, - data::{ - local::LocalWorkspace, - member::MemberId, - 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, -/// 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, Clone)] -#[cfg_file(path = CLIENT_FILE_LOCAL_SHEET_NOSET)] // Do not use LocalSheet::write or LocalSheet::read -pub struct LocalSheetData { - /// Local file path to metadata mapping. - #[serde(rename = "map")] - pub(crate) mapping: HashMap<LocalFilePathBuf, LocalMappingMetadata>, - - #[serde(rename = "vfs")] - pub(crate) vfs: HashMap<VirtualFileId, LocalFilePathBuf>, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct LocalMappingMetadata { - /// Hash value generated immediately after the file is downloaded to the local workspace - #[serde(rename = "base_hash")] - pub(crate) hash_when_updated: String, - - /// Time when the file was downloaded to the local workspace - #[serde(rename = "time")] - 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 = "desc")] - pub(crate) version_desc_when_updated: VirtualFileVersionDescription, - - /// Version when the file was downloaded to the local workspace - #[serde(rename = "ver")] - 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 - #[serde(rename = "check_time")] - pub(crate) last_modify_check_time: SystemTime, - - /// Latest modifiy check result - #[serde(rename = "modified")] - pub(crate) last_modify_check_result: bool, - - /// Latest modifiy check hash result - #[serde(rename = "current_hash")] - pub(crate) last_modify_check_hash: Option<String>, -} - -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 - #[allow(clippy::too_many_arguments)] - 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_modify_check_time: last_modifiy_check_time, - last_modify_check_result: last_modifiy_check_result, - last_modify_check_hash: None, - } - } - - /// Getter for hash_when_updated - pub fn hash_when_updated(&self) -> &String { - &self.hash_when_updated - } - - /// Setter for hash_when_updated - pub fn set_hash_when_updated(&mut self, hash: String) { - self.hash_when_updated = hash; - } - - /// Getter for date_when_updated - pub fn time_when_updated(&self) -> &SystemTime { - &self.time_when_updated - } - - /// Setter for time_when_updated - pub fn set_time_when_updated(&mut self, time: SystemTime) { - self.time_when_updated = time; - } - - /// Getter for size_when_updated - pub fn size_when_updated(&self) -> u64 { - self.size_when_updated - } - - /// Setter for size_when_updated - pub fn set_size_when_updated(&mut self, size: u64) { - self.size_when_updated = size; - } - - /// Getter for version_desc_when_updated - pub fn version_desc_when_updated(&self) -> &VirtualFileVersionDescription { - &self.version_desc_when_updated - } - - /// Setter for version_desc_when_updated - pub fn set_version_desc_when_updated(&mut self, version_desc: VirtualFileVersionDescription) { - self.version_desc_when_updated = version_desc; - } - - /// Getter for version_when_updated - pub fn version_when_updated(&self) -> &VirtualFileVersion { - &self.version_when_updated - } - - /// Setter for version_when_updated - pub fn set_version_when_updated(&mut self, version: VirtualFileVersion) { - self.version_when_updated = version; - } - - /// Getter for mapping_vfid - pub fn mapping_vfid(&self) -> &VirtualFileId { - &self.mapping_vfid - } - - /// Setter for mapping_vfid - pub fn set_mapping_vfid(&mut self, vfid: VirtualFileId) { - self.mapping_vfid = vfid; - } - - /// Getter for last_modifiy_check_time - pub fn last_modifiy_check_time(&self) -> &SystemTime { - &self.last_modify_check_time - } - - /// Setter for last_modifiy_check_time - pub fn set_last_modifiy_check_time(&mut self, time: SystemTime) { - self.last_modify_check_time = time; - } - - /// Getter for last_modifiy_check_result - pub fn last_modifiy_check_result(&self) -> bool { - self.last_modify_check_result - } - - /// Setter for last_modifiy_check_result - pub fn set_last_modifiy_check_result(&mut self, result: bool) { - self.last_modify_check_result = result; - } - - /// Getter for last_modifiy_check_hash - pub fn last_modifiy_check_hash(&self) -> &Option<String> { - &self.last_modify_check_hash - } - - /// Setter for last_modifiy_check_hash - pub fn set_last_modifiy_check_hash(&mut self, hash: Option<String>) { - self.last_modify_check_hash = hash; - } -} - -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_modify_check_time: SystemTime::now(), - last_modify_check_result: false, - last_modify_check_hash: None, - } - } -} - -mod instant_serde { - use serde::{self, Deserialize, Deserializer, Serializer}; - use tokio::time::Instant; - - pub fn serialize<S>(instant: &Instant, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - serializer.serialize_u64(instant.elapsed().as_secs()) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result<Instant, D::Error> - where - D: Deserializer<'de>, - { - 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 - } -} - -impl<'a> LocalSheet<'a> { - /// Add mapping to local sheet data - pub fn add_mapping( - &mut self, - path: &LocalFilePathBuf, - mapping: LocalMappingMetadata, - ) -> Result<(), std::io::Error> { - let path = format_path(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", - )); - } - - 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(()) - } - - /// Remove mapping from local sheet - pub fn remove_mapping( - &mut self, - path: &LocalFilePathBuf, - ) -> Result<LocalMappingMetadata, std::io::Error> { - let path = format_path(path)?; - match self.data.mapping.remove(&path) { - Some(mapping) => Ok(mapping), - None => Err(Error::new( - std::io::ErrorKind::NotFound, - "Path is not found.", - )), - } - } - - /// 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 LocalMappingMetadata, 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) - } - - /// 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); - 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/vault_modified.rs b/crates/vcs_data/src/data/local/vault_modified.rs deleted file mode 100644 index 563d11f..0000000 --- a/crates/vcs_data/src/data/local/vault_modified.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{constants::CLIENT_FILE_VAULT_MODIFIED, current::current_local_path}; - -pub async fn check_vault_modified() -> bool { - let Some(current_dir) = current_local_path() else { - return false; - }; - - let record_file = current_dir.join(CLIENT_FILE_VAULT_MODIFIED); - if !record_file.exists() { - return false; - } - - let Ok(contents) = tokio::fs::read_to_string(&record_file).await else { - return false; - }; - - matches!(contents.trim().to_lowercase().as_str(), "true") -} - -pub async fn sign_vault_modified(modified: bool) { - let Some(current_dir) = current_local_path() else { - return; - }; - - let record_file = current_dir.join(CLIENT_FILE_VAULT_MODIFIED); - - let contents = if modified { "true" } else { "false" }; - - let _ = tokio::fs::write(&record_file, contents).await; -} diff --git a/crates/vcs_data/src/data/local/workspace_analyzer.rs b/crates/vcs_data/src/data/local/workspace_analyzer.rs deleted file mode 100644 index f2d83ff..0000000 --- a/crates/vcs_data/src/data/local/workspace_analyzer.rs +++ /dev/null @@ -1,327 +0,0 @@ -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>, - - /// Erased local files - pub erased: 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 mut_workspace = workspace.config.lock().await; - let member = mut_workspace.current_account(); - let Some(sheet) = mut_workspace.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() - && 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 = (workspace.local_sheet(&member, &sheet_name).await).ok(); - - // 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, workspace).await?; - Self::analyze_modified( - &mut result, - &file_relative_paths, - &mut analyze_ctx, - workspace, - ) - .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>, - workspace: &LocalWorkspace, - ) -> 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(); - - // Files that exist locally but not in remote - let mut erased_files: HashSet<PathBuf> = HashSet::new(); - - if let Some(cached_data) = &analyze_ctx.cached_sheet_data { - if let Some(local_sheet) = &analyze_ctx.local_sheet { - let cached_sheet_mapping = cached_data.mapping(); - let local_sheet_mapping = &local_sheet.data.mapping; - - // Find paths that exist in local sheet but not in cached sheet - for local_path in local_sheet_mapping.keys() { - if !cached_sheet_mapping.contains_key(local_path) { - erased_files.insert(local_path.clone()); - } - } - } - } - - // Files that exist in the local sheet but not in reality are considered lost - let mut lost_files: HashSet<&PathBuf> = local_sheet_paths - .difference(&file_relative_paths_ref) - .filter(|&&path| !erased_files.contains(path)) - .cloned() - .collect(); - - // Files that exist in reality but not in the local sheet are recorded as newly created - let mut new_files: HashSet<&PathBuf> = file_relative_paths_ref - .difference(&local_sheet_paths) - .cloned() - .collect(); - - // Calculate hashes for new files - let new_files_for_hash: Vec<PathBuf> = new_files - .iter() - .map(|p| workspace.local_path.join(p)) - .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::other(e)), - } - .iter() - .map(|r| (r.file_path.clone(), r.hash.to_string())) - .collect(); - - // Build hash mapping table for lost files - 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| { - ( - // Using the most recently recorded Hash can more accurately identify moved items, - // but if it doesn't exist, fall back to the initially recorded Hash - mapping_data - .last_modify_check_hash - .as_ref() - .cloned() - .unwrap_or(mapping_data.hash_when_updated.clone()), - (*f).clone(), - ) - }) - }) - .collect(), - None => HashMap::new(), - }; - - // If these hashes correspond to the hashes of missing files, then this pair of new and lost items will be merged into moved items - let mut moved_files: HashSet<(FromRelativePathBuf, ToRelativePathBuf)> = HashSet::new(); - for (new_path, new_hash) in file_hashes { - let new_path = new_path - .strip_prefix(&workspace.local_path) - .map(|p| p.to_path_buf()) - .unwrap_or(new_path); - - // If the new hash value hits the mapping, add a moved item - if let Some(lost_path) = lost_files_hash_mapping.remove(&new_hash) { - // Remove this new item and lost item - lost_files.remove(&lost_path); - new_files.remove(&new_path); - - // Create moved item - moved_files.insert((lost_path.clone(), new_path)); - } - } - - // Enter fuzzy matching to match other potentially moved items that haven't been matched - // If the total number of new and lost files is divisible by 2, it indicates there might still be files that have been moved, consider trying fuzzy matching - if new_files.len() + lost_files.len() % 2 == 0 { - // Try fuzzy matching - // ... - } - - // Collect results and set the result - 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()); - vfid.map(|vfid| (vfid, (from.clone(), to.clone()))) - }) - .collect(); - result.erased = erased_files; - - 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>, - workspace: &LocalWorkspace, - ) -> 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(workspace.local_path.join(path), 2048).await - { - Ok(hash) => hash, - Err(e) => return Err(Error::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_modify_check_time = modified_time; - mapping_data.last_modify_check_result = true; - } else { - // Update last modified check time to modified time - mapping_data.last_modify_check_time = modified_time; - mapping_data.last_modify_check_result = false; - } - - // Record latest hash - mapping_data.last_modify_check_hash = Some(hash_calc.hash) - } - - // 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, - moved: HashMap::new(), - created: HashSet::new(), - lost: HashSet::new(), - modified: HashSet::new(), - erased: HashSet::new(), - } - } -} |
