summaryrefslogtreecommitdiff
path: root/crates/vcs_data
diff options
context:
space:
mode:
Diffstat (limited to 'crates/vcs_data')
-rw-r--r--crates/vcs_data/Cargo.toml4
-rw-r--r--crates/vcs_data/src/constants.rs12
-rw-r--r--crates/vcs_data/src/data/local.rs61
-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
-rw-r--r--crates/vcs_data/src/data/sheet.rs109
-rw-r--r--crates/vcs_data/src/data/vault/sheets.rs1
-rw-r--r--crates/vcs_data/src/data/vault/virtual_file.rs32
-rw-r--r--crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs28
13 files changed, 969 insertions, 92 deletions
diff --git a/crates/vcs_data/Cargo.toml b/crates/vcs_data/Cargo.toml
index de83b7b..3093809 100644
--- a/crates/vcs_data/Cargo.toml
+++ b/crates/vcs_data/Cargo.toml
@@ -8,6 +8,7 @@ version.workspace = true
# Utils
cfg_file = { path = "../utils/cfg_file", features = ["default"] }
data_struct = { path = "../utils/data_struct" }
+sha1_hash = { path = "../utils/sha1_hash" }
tcp_connection = { path = "../utils/tcp_connection" }
string_proc = { path = "../utils/string_proc" }
@@ -26,9 +27,10 @@ tokio = { version = "1.48.0", features = ["full"] }
# Filesystem
dirs = "6.0.0"
+walkdir = "2.5.0"
# Time
chrono = "0.4.42"
# Windows API
-winapi = { version = "0.3.9", features = ["fileapi", "winbase", "winnt"] } \ No newline at end of file
+winapi = { version = "0.3.9", features = ["fileapi", "winbase", "winnt"] }
diff --git a/crates/vcs_data/src/constants.rs b/crates/vcs_data/src/constants.rs
index e835482..a1d0ad2 100644
--- a/crates/vcs_data/src/constants.rs
+++ b/crates/vcs_data/src/constants.rs
@@ -51,17 +51,21 @@ pub const CLIENT_FILE_WORKSPACE: &str = "./.jv/workspace.toml";
pub const CLIENT_FILE_LATEST_INFO: &str = "./.jv/.latest.json";
// Client - Local
+pub const CLIENT_SUFFIX_LOCAL_SHEET_FILE: &str = ".json";
+pub const CLIENT_SUFFIX_CACHED_SHEET_FILE: &str = ".json";
pub const CLIENT_PATH_LOCAL_DRAFT: &str = "./.jv/drafts/{account}/{sheet_name}/";
-pub const CLIENT_FILE_LOCAL_SHEET: &str = "./.jv/sheets/{account}/{sheet_name}_local.toml";
-pub const CLIENT_FILE_CACHED_SHEET: &str = "./.jv/sheets/{account}/{sheet_name}.toml";
-pub const CLIENT_FILE_MEMBER_HELD: &str = "./.jv/helds/{account}_held.toml";
+pub const CLIENT_PATH_LOCAL_SHEET: &str = "./.jv/local/";
+pub const CLIENT_PATH_CACHED_SHEET: &str = "./.jv/cached/";
+pub const CLIENT_FILE_LOCAL_SHEET: &str = "./.jv/local/{account}/{sheet_name}.json";
+pub const CLIENT_FILE_CACHED_SHEET: &str = "./.jv/cached/{sheet_name}.json";
+pub const CLIENT_FILE_MEMBER_HELD: &str = "./.jv/helds/{account}.json";
pub const CLIENT_FILE_LOCAL_SHEET_NOSET: &str = "./.jv/.temp/wrong_local_sheet.toml";
pub const CLIENT_FILE_MEMBER_HELD_NOSET: &str = "./.jv/.temp/wrong_member_held.toml";
// Client - Other
pub const CLIENT_FILE_IGNOREFILES: &str = "IGNORE_RULES.toml";
-pub const CLIENT_FILE_TODOLIST: &str = "./TODO.md";
+pub const CLIENT_FILE_TODOLIST: &str = "./SETUP.md";
// -------------------------------------------------------------------------------------
diff --git a/crates/vcs_data/src/data/local.rs b/crates/vcs_data/src/data/local.rs
index cbf41ba..cbf5b73 100644
--- a/crates/vcs_data/src/data/local.rs
+++ b/crates/vcs_data/src/data/local.rs
@@ -1,16 +1,25 @@
-use std::{collections::HashMap, env::current_dir, path::PathBuf, sync::Arc};
+use std::{
+ collections::HashMap,
+ env::current_dir,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
use cfg_file::config::ConfigFile;
+use string_proc::format_path::format_path;
use tokio::{fs, sync::Mutex};
use vcs_docs::docs::READMES_LOCAL_WORKSPACE_TODOLIST;
use crate::{
- constants::{CLIENT_FILE_LOCAL_SHEET, CLIENT_FILE_TODOLIST, CLIENT_FILE_WORKSPACE},
+ constants::{
+ CLIENT_FILE_LOCAL_SHEET, CLIENT_FILE_TODOLIST, CLIENT_FILE_WORKSPACE,
+ CLIENT_PATH_LOCAL_SHEET,
+ },
current::{current_local_path, find_local_path},
data::{
local::{
config::LocalConfig,
- local_sheet::{LocalSheet, LocalSheetData},
+ local_sheet::{LocalSheet, LocalSheetData, LocalSheetPathBuf},
},
member::MemberId,
sheet::SheetName,
@@ -19,7 +28,9 @@ use crate::{
pub mod cached_sheet;
pub mod config;
+pub mod file_status;
pub mod latest_info;
+pub mod local_files;
pub mod local_sheet;
pub mod member_held;
@@ -116,6 +127,7 @@ impl LocalWorkspace {
if !local_sheet_path.exists() {
let sheet_data = LocalSheetData {
mapping: HashMap::new(),
+ vfs: HashMap::new(),
};
LocalSheetData::write_to(&sheet_data, local_sheet_path).await?;
return Ok(LocalSheet {
@@ -136,6 +148,40 @@ impl LocalWorkspace {
Ok(local_sheet)
}
+
+ /// Collect all theet names
+ pub async fn local_sheet_paths(&self) -> Result<Vec<LocalSheetPathBuf>, std::io::Error> {
+ let local_sheet_path = self.local_path.join(CLIENT_PATH_LOCAL_SHEET);
+ let mut sheet_paths = Vec::new();
+
+ async fn collect_sheet_paths(
+ dir: &Path,
+ suffix: &str,
+ paths: &mut Vec<LocalSheetPathBuf>,
+ ) -> Result<(), std::io::Error> {
+ if dir.is_dir() {
+ let mut entries = fs::read_dir(dir).await?;
+ while let Some(entry) = entries.next_entry().await? {
+ let path = entry.path();
+
+ if path.is_dir() {
+ Box::pin(collect_sheet_paths(&path, suffix, paths)).await?;
+ } else if path.is_file() {
+ if let Some(extension) = path.extension() {
+ if extension == suffix.trim_start_matches('.') {
+ let formatted_path = format_path(path)?;
+ paths.push(formatted_path);
+ }
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+
+ collect_sheet_paths(&local_sheet_path, ".json", &mut sheet_paths).await?;
+ Ok(sheet_paths)
+ }
}
mod hide_folder {
@@ -145,7 +191,7 @@ mod hide_folder {
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
#[cfg(windows)]
- use winapi::um::fileapi::{GetFileAttributesW, SetFileAttributesW, INVALID_FILE_ATTRIBUTES};
+ use winapi::um::fileapi::{GetFileAttributesW, INVALID_FILE_ATTRIBUTES, SetFileAttributesW};
pub fn hide_folder(path: &Path) -> io::Result<()> {
if !path.is_dir() {
@@ -175,10 +221,7 @@ mod hide_folder {
#[cfg(windows)]
fn hide_folder_impl(path: &Path) -> io::Result<()> {
// Convert to Windows wide string format
- let path_str: Vec<u16> = path.as_os_str()
- .encode_wide()
- .chain(Some(0))
- .collect();
+ let path_str: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
// Get current attributes
let attrs = unsafe { GetFileAttributesW(path_str.as_ptr()) };
@@ -210,4 +253,4 @@ mod hide_folder {
"Unsupported operating system",
))
}
-} \ No newline at end of file
+}
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,
+ },
+ );
+ }
+ }
+}
diff --git a/crates/vcs_data/src/data/sheet.rs b/crates/vcs_data/src/data/sheet.rs
index 09271d5..9e3a1d7 100644
--- a/crates/vcs_data/src/data/sheet.rs
+++ b/crates/vcs_data/src/data/sheet.rs
@@ -8,7 +8,10 @@ use crate::{
constants::SERVER_FILE_SHEET,
data::{
member::MemberId,
- vault::{Vault, virtual_file::VirtualFileId},
+ vault::{
+ Vault,
+ virtual_file::{VirtualFileId, VirtualFileVersion},
+ },
},
};
@@ -26,7 +29,7 @@ pub struct InputPackage {
pub from: SheetName,
/// Files in this input package with their relative paths and virtual file IDs
- pub files: Vec<(InputRelativePathBuf, VirtualFileId)>,
+ pub files: Vec<(InputRelativePathBuf, SheetMappingMetadata)>,
}
impl PartialEq for InputPackage {
@@ -60,7 +63,16 @@ pub struct SheetData {
pub(crate) inputs: Vec<InputPackage>,
/// Mapping of sheet paths to virtual file IDs
- pub(crate) mapping: HashMap<SheetPathBuf, VirtualFileId>,
+ pub(crate) mapping: HashMap<SheetPathBuf, SheetMappingMetadata>,
+
+ /// Mapping of virtual file Ids to sheet paths
+ pub(crate) id_mapping: Option<HashMap<VirtualFileId, SheetPathBuf>>,
+}
+
+#[derive(Debug, Default, Serialize, Deserialize, ConfigFile, Clone, Eq, PartialEq)]
+pub struct SheetMappingMetadata {
+ pub id: VirtualFileId,
+ pub version: VirtualFileVersion,
}
impl<'a> Sheet<'a> {
@@ -88,10 +100,15 @@ impl<'a> Sheet<'a> {
}
/// Get the mapping of this sheet
- pub fn mapping(&self) -> &HashMap<SheetPathBuf, VirtualFileId> {
+ pub fn mapping(&self) -> &HashMap<SheetPathBuf, SheetMappingMetadata> {
&self.data.mapping
}
+ /// Get the id_mapping of this sheet data
+ pub fn id_mapping(&self) -> &Option<HashMap<VirtualFileId, SheetPathBuf>> {
+ &self.data.id_mapping
+ }
+
/// Get the write count of this sheet
pub fn write_count(&self) -> i32 {
self.data.write_count
@@ -150,9 +167,13 @@ impl<'a> Sheet<'a> {
};
// Insert to sheet
- for (relative_path, virtual_file_id) in input.files {
- self.add_mapping(insert_to.join(relative_path), virtual_file_id)
- .await?;
+ for (relative_path, virtual_file_meta) in input.files {
+ self.add_mapping(
+ insert_to.join(relative_path),
+ virtual_file_meta.id,
+ virtual_file_meta.version,
+ )
+ .await?;
}
Ok(())
@@ -172,11 +193,18 @@ impl<'a> Sheet<'a> {
&mut self,
sheet_path: SheetPathBuf,
virtual_file_id: VirtualFileId,
+ version: VirtualFileVersion,
) -> Result<(), std::io::Error> {
// Check if the virtual file exists in the vault
if self.vault_reference.virtual_file(&virtual_file_id).is_err() {
// Virtual file doesn't exist, add the mapping directly
- self.data.mapping.insert(sheet_path, virtual_file_id);
+ self.data.mapping.insert(
+ sheet_path,
+ SheetMappingMetadata {
+ id: virtual_file_id,
+ version: version,
+ },
+ );
return Ok(());
}
@@ -194,16 +222,22 @@ impl<'a> Sheet<'a> {
.has_virtual_file_edit_right(holder, &virtual_file_id)
.await
{
- Ok(false) => {
- // Holder doesn't have rights, add the mapping (member is giving up the file)
- self.data.mapping.insert(sheet_path, virtual_file_id);
+ Ok(true) => {
+ // Holder has edit rights, add the mapping (member has permission to modify the file)
+ self.data.mapping.insert(
+ sheet_path,
+ SheetMappingMetadata {
+ id: virtual_file_id,
+ version,
+ },
+ );
Ok(())
}
- Ok(true) => {
- // Holder has edit rights, don't allow modifying the mapping
+ Ok(false) => {
+ // Holder doesn't have edit rights, don't allow modifying the mapping
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
- "Member has edit rights to the virtual file, cannot modify mapping",
+ "Member doesn't have edit rights to the virtual file, cannot modify mapping",
))
}
Err(_) => {
@@ -224,8 +258,11 @@ impl<'a> Sheet<'a> {
/// 4. If member has no edit rights and the file exists, returns the removed virtual file ID
///
/// Note: Full validation adds overhead - avoid frequent calls
- pub async fn remove_mapping(&mut self, sheet_path: &SheetPathBuf) -> Option<VirtualFileId> {
- let virtual_file_id = match self.data.mapping.get(sheet_path) {
+ pub async fn remove_mapping(
+ &mut self,
+ sheet_path: &SheetPathBuf,
+ ) -> Option<SheetMappingMetadata> {
+ let virtual_file_meta = match self.data.mapping.get(sheet_path) {
Some(id) => id,
None => {
// The mapping entry doesn't exist, nothing to remove
@@ -234,7 +271,11 @@ impl<'a> Sheet<'a> {
};
// Check if the virtual file exists in the vault
- if self.vault_reference.virtual_file(virtual_file_id).is_err() {
+ if self
+ .vault_reference
+ .virtual_file(&virtual_file_meta.id)
+ .is_err()
+ {
// Virtual file doesn't exist, remove the mapping and return None
self.data.mapping.remove(sheet_path);
return None;
@@ -248,7 +289,7 @@ impl<'a> Sheet<'a> {
// Check if the holder has edit rights to the virtual file
match self
.vault_reference
- .has_virtual_file_edit_right(holder, virtual_file_id)
+ .has_virtual_file_edit_right(holder, &virtual_file_meta.id)
.await
{
Ok(false) => {
@@ -273,6 +314,18 @@ impl<'a> Sheet<'a> {
/// If needed, please deserialize and reload it.
pub async fn persist(mut self) -> Result<(), std::io::Error> {
self.data.write_count += 1;
+
+ // Update id mapping
+ self.data.id_mapping = Some(HashMap::new());
+ for map in self.data.mapping.iter() {
+ self.data
+ .id_mapping
+ .as_mut()
+ .unwrap()
+ .insert(map.1.id.clone(), map.0.clone());
+ }
+
+ // Add write count
if self.data.write_count > i32::MAX {
self.data.write_count = 0;
}
@@ -402,4 +455,24 @@ impl SheetData {
pub fn write_count(&self) -> i32 {
self.write_count
}
+
+ /// Get the holder of this sheet data
+ pub fn holder(&self) -> Option<&MemberId> {
+ self.holder.as_ref()
+ }
+
+ /// Get the inputs of this sheet data
+ pub fn inputs(&self) -> &Vec<InputPackage> {
+ &self.inputs
+ }
+
+ /// Get the mapping of this sheet data
+ pub fn mapping(&self) -> &HashMap<SheetPathBuf, SheetMappingMetadata> {
+ &self.mapping
+ }
+
+ /// Get the id_mapping of this sheet data
+ pub fn id_mapping(&self) -> &Option<HashMap<VirtualFileId, SheetPathBuf>> {
+ &self.id_mapping
+ }
}
diff --git a/crates/vcs_data/src/data/vault/sheets.rs b/crates/vcs_data/src/data/vault/sheets.rs
index ba021b5..cea7271 100644
--- a/crates/vcs_data/src/data/vault/sheets.rs
+++ b/crates/vcs_data/src/data/vault/sheets.rs
@@ -133,6 +133,7 @@ impl Vault {
holder: Some(holder.clone()),
inputs: Vec::new(),
mapping: HashMap::new(),
+ id_mapping: None,
write_count: 0,
};
SheetData::write_to(&sheet_data, sheet_file_path).await?;
diff --git a/crates/vcs_data/src/data/vault/virtual_file.rs b/crates/vcs_data/src/data/vault/virtual_file.rs
index 221766f..6dd5208 100644
--- a/crates/vcs_data/src/data/vault/virtual_file.rs
+++ b/crates/vcs_data/src/data/vault/virtual_file.rs
@@ -22,7 +22,7 @@ use crate::{
pub type VirtualFileId = String;
pub type VirtualFileVersion = String;
-const VF_PREFIX: &str = "vf_";
+const VF_PREFIX: &str = "vf-";
const ID_PARAM: &str = "{vf_id}";
const ID_INDEX: &str = "{vf_index}";
const VERSION_PARAM: &str = "{vf_version}";
@@ -244,8 +244,6 @@ impl Vault {
}
fs::rename(receive_path, move_path).await?;
- //
-
Ok(new_id)
}
Err(e) => {
@@ -444,6 +442,13 @@ impl VirtualFileMeta {
&self.histories
}
+ /// Get the latest version of the virtual file
+ pub fn version_latest(&self) -> VirtualFileVersion {
+ // After creating a virtual file in `update_virtual_file_from_connection`,
+ // the Vec will never be empty, so unwrap is allowed here
+ self.histories.last().unwrap().clone()
+ }
+
/// Get the total number of versions for this virtual file
pub fn version_len(&self) -> i32 {
self.histories.len() as i32
@@ -470,4 +475,25 @@ impl VirtualFileMeta {
pub fn version_name(&self, version_num: i32) -> Option<VirtualFileVersion> {
self.histories.get(version_num as usize).cloned()
}
+
+ /// Get the member who holds the edit right of the file
+ pub fn hold_member(&self) -> &MemberId {
+ &self.hold_member
+ }
+
+ /// Get the version descriptions for all versions
+ pub fn version_descriptions(
+ &self,
+ ) -> &HashMap<VirtualFileVersion, VirtualFileVersionDescription> {
+ &self.version_description
+ }
+
+ /// Get the version description for a given version
+ pub fn version_description(
+ &self,
+ version: VirtualFileVersion,
+ ) -> Option<&VirtualFileVersionDescription> {
+ let desc = self.version_descriptions();
+ desc.get(&version)
+ }
}
diff --git a/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs b/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs
index f256436..a89fbea 100644
--- a/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs
+++ b/crates/vcs_data/vcs_data_test/src/test_sheet_creation_management_and_persistence.rs
@@ -58,10 +58,14 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io:
let lib_rs_id = VirtualFileId::new();
sheet
- .add_mapping(main_rs_path.clone(), main_rs_id.clone())
+ .add_mapping(
+ main_rs_path.clone(),
+ main_rs_id.clone(),
+ "1.0.0".to_string(),
+ )
.await?;
sheet
- .add_mapping(lib_rs_path.clone(), lib_rs_id.clone())
+ .add_mapping(lib_rs_path.clone(), lib_rs_id.clone(), "1.0.0".to_string())
.await?;
// Use output_mappings to generate the InputPackage
@@ -88,12 +92,19 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io:
let virtual_file_id = VirtualFileId::new();
sheet
- .add_mapping(mapping_path.clone(), virtual_file_id.clone())
+ .add_mapping(
+ mapping_path.clone(),
+ virtual_file_id.clone(),
+ "1.0.0".to_string(),
+ )
.await?;
// Verify mapping was added
assert_eq!(sheet.mapping().len(), 3);
- assert_eq!(sheet.mapping().get(&mapping_path), Some(&virtual_file_id));
+ assert_eq!(
+ sheet.mapping().get(&mapping_path).map(|meta| &meta.id),
+ Some(&virtual_file_id)
+ );
// Test 4: Persist sheet to disk
sheet.persist().await?;
@@ -270,10 +281,14 @@ async fn test_sheet_data_serialization() -> Result<(), std::io::Error> {
let lib_rs_id = VirtualFileId::new();
sheet
- .add_mapping(main_rs_path.clone(), main_rs_id.clone())
+ .add_mapping(
+ main_rs_path.clone(),
+ main_rs_id.clone(),
+ "1.0.0".to_string(),
+ )
.await?;
sheet
- .add_mapping(lib_rs_path.clone(), lib_rs_id.clone())
+ .add_mapping(lib_rs_path.clone(), lib_rs_id.clone(), "1.0.0".to_string())
.await?;
// Use output_mappings to generate the InputPackage
@@ -288,6 +303,7 @@ async fn test_sheet_data_serialization() -> Result<(), std::io::Error> {
.add_mapping(
vcs_data::data::sheet::SheetPathBuf::from("output/build.exe"),
build_exe_id,
+ "1.0.0".to_string(),
)
.await?;