summaryrefslogtreecommitdiff
path: root/crates/vcs_data/src/data/local
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2025-11-17 11:49:49 +0800
committer魏曹先生 <1992414357@qq.com>2025-11-17 11:49:49 +0800
commit7b97b52af021500d8085c875d20215e8dc0f53cc (patch)
tree9b8219363380db3330bda75e28e364154224eca8 /crates/vcs_data/src/data/local
parente190d90594b17fb16849a13198af3f5152414e4c (diff)
feat: Add file status tracking and SHA1 hash system
- Implement SHA1 hash calculation module with async support - Add file status analysis for tracking moves, creates, and modifications - Enhance local file management with relative path handling - Update virtual file actions with improved tracking capabilities
Diffstat (limited to 'crates/vcs_data/src/data/local')
-rw-r--r--crates/vcs_data/src/data/local/cached_sheet.rs84
-rw-r--r--crates/vcs_data/src/data/local/file_status.rs282
-rw-r--r--crates/vcs_data/src/data/local/latest_info.rs48
-rw-r--r--crates/vcs_data/src/data/local/local_files.rs152
-rw-r--r--crates/vcs_data/src/data/local/local_sheet.rs207
-rw-r--r--crates/vcs_data/src/data/local/member_held.rs41
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,
+ },
+ );
+ }
+ }
+}