summaryrefslogtreecommitdiff
path: root/crates/vcs/src/workspace
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2025-09-25 13:22:50 +0800
committer魏曹先生 <1992414357@qq.com>2025-09-25 13:22:50 +0800
commit07b1ce2c3398c69b021ea2418a057eb7b6cacc40 (patch)
tree73d6c8f35df425b15c905819fb0d37aec02e6563 /crates/vcs/src/workspace
parent28828776b7aaa6f2bf723837eab1cae859582be5 (diff)
Rename `env` to `vcs`
Diffstat (limited to 'crates/vcs/src/workspace')
-rw-r--r--crates/vcs/src/workspace/local.rs106
-rw-r--r--crates/vcs/src/workspace/local/config.rs53
-rw-r--r--crates/vcs/src/workspace/member.rs67
-rw-r--r--crates/vcs/src/workspace/user.rs1
-rw-r--r--crates/vcs/src/workspace/user/accounts.rs1
-rw-r--r--crates/vcs/src/workspace/vault.rs137
-rw-r--r--crates/vcs/src/workspace/vault/config.rs46
-rw-r--r--crates/vcs/src/workspace/vault/member.rs105
-rw-r--r--crates/vcs/src/workspace/vault/virtual_file.rs422
9 files changed, 938 insertions, 0 deletions
diff --git a/crates/vcs/src/workspace/local.rs b/crates/vcs/src/workspace/local.rs
new file mode 100644
index 0000000..0119952
--- /dev/null
+++ b/crates/vcs/src/workspace/local.rs
@@ -0,0 +1,106 @@
+use std::{env::current_dir, path::PathBuf};
+
+use cfg_file::config::ConfigFile;
+use tokio::fs;
+
+use crate::{
+ constants::{CLIENT_FILE_README, CLIENT_FILE_WORKSPACE},
+ current::{current_local_path, find_local_path},
+ workspace::local::config::LocalConfig,
+};
+
+pub mod config;
+
+pub struct LocalWorkspace {
+ config: LocalConfig,
+ local_path: PathBuf,
+}
+
+impl LocalWorkspace {
+ /// Get the path of the local workspace.
+ pub fn local_path(&self) -> &PathBuf {
+ &self.local_path
+ }
+
+ /// Initialize local workspace.
+ pub fn init(config: LocalConfig, local_path: impl Into<PathBuf>) -> Option<Self> {
+ let Some(local_path) = find_local_path(local_path) else {
+ return None;
+ };
+ Some(Self { config, local_path })
+ }
+
+ /// Initialize local workspace in the current directory.
+ pub fn init_current_dir(config: LocalConfig) -> Option<Self> {
+ let Some(local_path) = current_local_path() else {
+ return None;
+ };
+ Some(Self { config, local_path })
+ }
+
+ /// Setup local workspace
+ pub async fn setup_local_workspace(
+ local_path: impl Into<PathBuf>,
+ ) -> Result<(), std::io::Error> {
+ let local_path: PathBuf = local_path.into();
+
+ // Ensure directory is empty
+ if local_path.exists() && local_path.read_dir()?.next().is_some() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::DirectoryNotEmpty,
+ "DirectoryNotEmpty",
+ ));
+ }
+
+ // 1. Setup config
+ let config = LocalConfig::default();
+ LocalConfig::write_to(&config, local_path.join(CLIENT_FILE_WORKSPACE)).await?;
+
+ // 2. Setup README.md
+ let readme_content = format!(
+ "\
+# JustEnoughVCS Local Workspace
+
+This directory is a **Local Workspace** managed by `JustEnoughVCS`. All files and subdirectories within this scope can be version-controlled using the `JustEnoughVCS` CLI or GUI tools, with the following exceptions:
+
+- The `.jv` directory
+- Any files or directories excluded via `.jgnore` or `.gitignore`
+
+> ⚠️ **Warning**
+>
+> Files in this workspace will be uploaded to the upstream server. Please ensure you fully trust this server before proceeding.
+
+## Access Requirements
+
+To use `JustEnoughVCS` with this workspace, you must have:
+
+- **A registered user ID** with the upstream server
+- **Your private key** properly configured locally
+- **Your public key** stored in the server's public key directory
+
+Without these credentials, the server will reject all access requests.
+
+## Support
+
+- **Permission or access issues?** → Contact your server administrator
+- **Tooling problems or bugs?** → Reach out to the development team via [GitHub Issues](https://github.com/JustEnoughVCS/VersionControl/issues)
+- **Documentation**: Visit our repository for full documentation
+
+------
+
+*Thank you for using JustEnoughVCS!*
+"
+ )
+ .trim()
+ .to_string();
+ fs::write(local_path.join(CLIENT_FILE_README), readme_content).await?;
+
+ Ok(())
+ }
+
+ /// Setup local workspace in current directory
+ pub async fn setup_local_workspacecurrent_dir() -> Result<(), std::io::Error> {
+ Self::setup_local_workspace(current_dir()?).await?;
+ Ok(())
+ }
+}
diff --git a/crates/vcs/src/workspace/local/config.rs b/crates/vcs/src/workspace/local/config.rs
new file mode 100644
index 0000000..d641880
--- /dev/null
+++ b/crates/vcs/src/workspace/local/config.rs
@@ -0,0 +1,53 @@
+use cfg_file::ConfigFile;
+use serde::{Deserialize, Serialize};
+use std::net::SocketAddr;
+
+use crate::constants::CLIENT_FILE_WORKSPACE;
+use crate::constants::PORT;
+use crate::workspace::vault::MemberId;
+
+#[derive(Serialize, Deserialize, ConfigFile)]
+#[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.
+ 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.
+ using_account: MemberId,
+}
+
+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(),
+ }
+ }
+}
+
+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) {
+ self.using_account = account;
+ }
+
+ /// Get the currently used account
+ pub fn current_account(&self) -> MemberId {
+ self.using_account.clone()
+ }
+}
diff --git a/crates/vcs/src/workspace/member.rs b/crates/vcs/src/workspace/member.rs
new file mode 100644
index 0000000..208c78c
--- /dev/null
+++ b/crates/vcs/src/workspace/member.rs
@@ -0,0 +1,67 @@
+use std::collections::HashMap;
+
+use cfg_file::ConfigFile;
+use serde::{Deserialize, Serialize};
+use string_proc::snake_case;
+
+#[derive(Debug, Eq, Clone, ConfigFile, Serialize, Deserialize)]
+pub struct Member {
+ /// Member ID, the unique identifier of the member
+ id: String,
+
+ /// Member metadata
+ metadata: HashMap<String, String>,
+}
+
+impl Default for Member {
+ fn default() -> Self {
+ Self::new("default_user")
+ }
+}
+
+impl PartialEq for Member {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ }
+}
+
+impl std::fmt::Display for Member {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.id)
+ }
+}
+
+impl std::convert::AsRef<str> for Member {
+ fn as_ref(&self) -> &str {
+ &self.id
+ }
+}
+
+impl Member {
+ /// Create member struct by id
+ pub fn new(new_id: impl Into<String>) -> Self {
+ Self {
+ id: snake_case!(new_id.into()),
+ metadata: HashMap::new(),
+ }
+ }
+
+ /// Get member id
+ pub fn id(&self) -> String {
+ self.id.clone()
+ }
+
+ /// Get metadata
+ pub fn metadata(&self, key: impl Into<String>) -> Option<&String> {
+ self.metadata.get(&key.into())
+ }
+
+ /// Set metadata
+ pub fn set_metadata(
+ &mut self,
+ key: impl AsRef<str>,
+ value: impl Into<String>,
+ ) -> Option<String> {
+ self.metadata.insert(key.as_ref().to_string(), value.into())
+ }
+}
diff --git a/crates/vcs/src/workspace/user.rs b/crates/vcs/src/workspace/user.rs
new file mode 100644
index 0000000..9bb4894
--- /dev/null
+++ b/crates/vcs/src/workspace/user.rs
@@ -0,0 +1 @@
+pub mod accounts;
diff --git a/crates/vcs/src/workspace/user/accounts.rs b/crates/vcs/src/workspace/user/accounts.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/crates/vcs/src/workspace/user/accounts.rs
@@ -0,0 +1 @@
+
diff --git a/crates/vcs/src/workspace/vault.rs b/crates/vcs/src/workspace/vault.rs
new file mode 100644
index 0000000..caac662
--- /dev/null
+++ b/crates/vcs/src/workspace/vault.rs
@@ -0,0 +1,137 @@
+use std::{
+ env::current_dir,
+ fs::{self, create_dir_all},
+ path::PathBuf,
+};
+
+use cfg_file::config::ConfigFile;
+
+use crate::{
+ constants::{
+ SERVER_FILE_README, SERVER_FILE_VAULT, SERVER_PATH_MEMBER_PUB, SERVER_PATH_MEMBERS,
+ SERVER_PATH_SHEETS, SERVER_PATH_VIRTUAL_FILE_ROOT,
+ },
+ current::{current_vault_path, find_vault_path},
+ workspace::vault::config::VaultConfig,
+};
+
+pub mod config;
+pub mod member;
+pub mod virtual_file;
+
+pub type MemberId = String;
+
+pub struct Vault {
+ config: VaultConfig,
+ vault_path: PathBuf,
+}
+
+impl Vault {
+ /// Get vault path
+ pub fn vault_path(&self) -> &PathBuf {
+ &self.vault_path
+ }
+
+ /// Initialize vault
+ pub fn init(config: VaultConfig, vault_path: impl Into<PathBuf>) -> Option<Self> {
+ let Some(vault_path) = find_vault_path(vault_path) else {
+ return None;
+ };
+ Some(Self { config, vault_path })
+ }
+
+ /// Initialize vault
+ pub fn init_current_dir(config: VaultConfig) -> Option<Self> {
+ let Some(vault_path) = current_vault_path() else {
+ return None;
+ };
+ Some(Self { config, vault_path })
+ }
+
+ /// Setup vault
+ pub async fn setup_vault(vault_path: impl Into<PathBuf>) -> Result<(), std::io::Error> {
+ let vault_path: PathBuf = vault_path.into();
+
+ // Ensure directory is empty
+ if vault_path.exists() && vault_path.read_dir()?.next().is_some() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::DirectoryNotEmpty,
+ "DirectoryNotEmpty",
+ ));
+ }
+
+ // 1. Setup main config
+ let config = VaultConfig::default();
+ VaultConfig::write_to(&config, vault_path.join(SERVER_FILE_VAULT)).await?;
+
+ // 2. Setup sheets directory
+ create_dir_all(vault_path.join(SERVER_PATH_SHEETS))?;
+
+ // 3. Setup key directory
+ create_dir_all(vault_path.join(SERVER_PATH_MEMBER_PUB))?;
+
+ // 4. Setup member directory
+ create_dir_all(vault_path.join(SERVER_PATH_MEMBERS))?;
+
+ // 5. Setup storage directory
+ create_dir_all(vault_path.join(SERVER_PATH_VIRTUAL_FILE_ROOT))?;
+
+ // Final, generate README.md
+ let readme_content = format!(
+ "\
+# JustEnoughVCS Server Setup
+
+This directory contains the server configuration and data for `JustEnoughVCS`.
+
+## User Authentication
+To allow users to connect to this server, place their public keys in the `{}` directory.
+Each public key file should be named `{{member_id}}.pem` (e.g., `juliet.pem`), and contain the user's public key in PEM format.
+
+**ECDSA:**
+```bash
+openssl genpkey -algorithm ed25519 -out your_name_private.pem
+openssl pkey -in your_name_private.pem -pubout -out your_name.pem
+```
+
+**RSA:**
+```bash
+openssl genpkey -algorithm RSA -out your_name_private.pem -pkeyopt rsa_keygen_bits:2048
+openssl pkey -in your_name_private.pem -pubout -out your_name.pem
+```
+
+**DSA:**
+```bash
+openssl genpkey -algorithm DSA -out your_name_private.pem -pkeyopt dsa_paramgen_bits:2048
+openssl pkey -in your_name_private.pem -pubout -out your_name.pem
+```
+
+Place only the `your_name.pem` file in the server's `./key/` directory, renamed to match the user's member ID.
+
+## File Storage
+All version-controlled files (Virtual File) are stored in the `{}` directory.
+
+## License
+This software is distributed under the MIT License. For complete license details, please see the main repository homepage.
+
+## Support
+Repository: `https://github.com/JustEnoughVCS/VersionControl`
+Please report any issues or questions on the GitHub issue tracker.
+
+## Thanks :)
+Thank you for using `JustEnoughVCS!`
+ ",
+ SERVER_PATH_MEMBER_PUB, SERVER_PATH_VIRTUAL_FILE_ROOT
+ )
+ .trim()
+ .to_string();
+ fs::write(vault_path.join(SERVER_FILE_README), readme_content)?;
+
+ Ok(())
+ }
+
+ /// Setup vault in current directory
+ pub async fn setup_vault_current_dir() -> Result<(), std::io::Error> {
+ Self::setup_vault(current_dir()?).await?;
+ Ok(())
+ }
+}
diff --git a/crates/vcs/src/workspace/vault/config.rs b/crates/vcs/src/workspace/vault/config.rs
new file mode 100644
index 0000000..5414e4d
--- /dev/null
+++ b/crates/vcs/src/workspace/vault/config.rs
@@ -0,0 +1,46 @@
+use cfg_file::ConfigFile;
+use serde::{Deserialize, Serialize};
+
+use crate::constants::SERVER_FILE_VAULT;
+use crate::workspace::member::Member;
+use crate::workspace::vault::MemberId;
+
+#[derive(Serialize, Deserialize, ConfigFile)]
+#[cfg_file(path = SERVER_FILE_VAULT)]
+pub struct VaultConfig {
+ /// Vault name, which can be used as the project name and generally serves as a hint
+ vault_name: String,
+
+ /// Vault admin id, a list of member id representing administrator identities
+ vault_admin_list: Vec<MemberId>,
+}
+
+impl Default for VaultConfig {
+ fn default() -> Self {
+ Self {
+ vault_name: "JustEnoughVault".to_string(),
+ vault_admin_list: Vec::new(),
+ }
+ }
+}
+
+impl VaultConfig {
+ // Change name of the vault.
+ pub fn change_name(&mut self, name: impl Into<String>) {
+ self.vault_name = name.into()
+ }
+
+ // Add admin
+ pub fn add_admin(&mut self, member: &Member) {
+ let uuid = member.id();
+ if !self.vault_admin_list.contains(&uuid) {
+ self.vault_admin_list.push(uuid);
+ }
+ }
+
+ // Remove admin
+ pub fn remove_admin(&mut self, member: &Member) {
+ let id = member.id();
+ self.vault_admin_list.retain(|x| x != &id);
+ }
+}
diff --git a/crates/vcs/src/workspace/vault/member.rs b/crates/vcs/src/workspace/vault/member.rs
new file mode 100644
index 0000000..793ba2a
--- /dev/null
+++ b/crates/vcs/src/workspace/vault/member.rs
@@ -0,0 +1,105 @@
+use std::{
+ fs,
+ io::{Error, ErrorKind},
+ path::PathBuf,
+};
+
+use cfg_file::config::ConfigFile;
+
+use crate::{
+ constants::{SERVER_FILE_MEMBER_INFO, SERVER_FILE_MEMBER_PUB},
+ workspace::{
+ member::Member,
+ vault::{MemberId, Vault},
+ },
+};
+
+const ID_PARAM: &str = "{member_id}";
+
+/// Member Manage
+impl Vault {
+ /// Read member from configuration file
+ pub async fn member(&self, id: &MemberId) -> Result<Member, std::io::Error> {
+ if let Some(cfg_file) = self.member_cfg(id) {
+ let member = Member::read_from(cfg_file).await?;
+ return Ok(member);
+ }
+
+ Err(Error::new(ErrorKind::NotFound, "Member not found!"))
+ }
+
+ /// Update member info
+ pub async fn update_member(&self, member: Member) -> Result<(), std::io::Error> {
+ // Ensure member exist
+ if let Some(_) = self.member_cfg(&member.id()) {
+ let member_cfg_path = self.member_cfg_path(&member.id());
+ Member::write_to(&member, member_cfg_path).await?;
+ return Ok(());
+ }
+
+ Err(Error::new(ErrorKind::NotFound, "Member not found!"))
+ }
+
+ /// Register a member to vault
+ pub async fn register_member_to_vault(&self, member: Member) -> Result<(), std::io::Error> {
+ // Ensure member not exist
+ if let Some(_) = self.member_cfg(&member.id()) {
+ return Err(Error::new(
+ ErrorKind::DirectoryNotEmpty,
+ format!("Member `{}` already registered!", member.id()),
+ ));
+ }
+
+ // Wrtie config file to member dir
+ let member_cfg_path = self.member_cfg_path(&member.id());
+ Member::write_to(&member, member_cfg_path).await?;
+
+ Ok(())
+ }
+
+ /// Remove member from vault
+ pub fn remove_member_from_vault(&self, id: &MemberId) -> Result<(), std::io::Error> {
+ // Ensure member exist
+ if let Some(member_cfg_path) = self.member_cfg(id) {
+ fs::remove_file(member_cfg_path)?;
+ }
+
+ Ok(())
+ }
+
+ /// Try to get the member's configuration file to determine if the member exists
+ pub fn member_cfg(&self, id: &MemberId) -> Option<PathBuf> {
+ let cfg_file = self.member_cfg_path(id);
+ if cfg_file.exists() {
+ Some(cfg_file)
+ } else {
+ None
+ }
+ }
+
+ /// Try to get the member's public key file to determine if the member has login permission
+ pub fn member_key(&self, id: &MemberId) -> Option<PathBuf> {
+ let key_file = self.member_key_path(id);
+ if key_file.exists() {
+ Some(key_file)
+ } else {
+ None
+ }
+ }
+
+ /// Get the member's configuration file path, but do not check if the file exists
+ pub fn member_cfg_path(&self, id: &MemberId) -> PathBuf {
+ let path = self
+ .vault_path
+ .join(SERVER_FILE_MEMBER_INFO.replace(ID_PARAM, id.to_string().as_str()));
+ path
+ }
+
+ /// Get the member's public key file path, but do not check if the file exists
+ pub fn member_key_path(&self, id: &MemberId) -> PathBuf {
+ let path = self
+ .vault_path
+ .join(SERVER_FILE_MEMBER_PUB.replace(ID_PARAM, id.to_string().as_str()));
+ path
+ }
+}
diff --git a/crates/vcs/src/workspace/vault/virtual_file.rs b/crates/vcs/src/workspace/vault/virtual_file.rs
new file mode 100644
index 0000000..321f0e1
--- /dev/null
+++ b/crates/vcs/src/workspace/vault/virtual_file.rs
@@ -0,0 +1,422 @@
+use std::{
+ collections::HashMap,
+ io::{Error, ErrorKind},
+ path::PathBuf,
+};
+
+use cfg_file::{ConfigFile, config::ConfigFile};
+use serde::{Deserialize, Serialize};
+use string_proc::snake_case;
+use tcp_connection::instance::ConnectionInstance;
+use tokio::fs;
+use uuid::Uuid;
+
+use crate::{
+ constants::{
+ SERVER_FILE_VIRTUAL_FILE_META, SERVER_FILE_VIRTUAL_FILE_VERSION_INSTANCE,
+ SERVER_PATH_VIRTUAL_FILE_ROOT, SERVER_PATH_VIRTUAL_FILE_STORAGE,
+ SERVER_PATH_VIRTUAL_FILE_TEMP,
+ },
+ workspace::vault::{MemberId, Vault},
+};
+
+pub type VirtualFileId = String;
+pub type VirtualFileVersion = String;
+
+const ID_PARAM: &str = "{vf_id}";
+const VERSION_PARAM: &str = "{vf_version}";
+const TEMP_NAME: &str = "{temp_name}";
+
+pub struct VirtualFile<'a> {
+ /// Unique identifier for the virtual file
+ id: VirtualFileId,
+
+ /// Reference of Vault
+ current_vault: &'a Vault,
+}
+
+#[derive(Default, Clone, Serialize, Deserialize, ConfigFile)]
+pub struct VirtualFileMeta {
+ /// Current version of the virtual file
+ current_version: VirtualFileVersion,
+
+ /// The member who holds the edit right of the file
+ hold_member: MemberId,
+
+ /// Description of each version
+ version_description: HashMap<VirtualFileVersion, VirtualFileVersionDescription>,
+
+ /// Histories
+ histories: Vec<VirtualFileVersion>,
+}
+
+#[derive(Default, Clone, Serialize, Deserialize)]
+pub struct VirtualFileVersionDescription {
+ /// The member who created this version
+ pub creator: MemberId,
+
+ /// The description of this version
+ pub description: String,
+}
+
+impl VirtualFileVersionDescription {
+ /// Create a new version description
+ pub fn new(creator: MemberId, description: String) -> Self {
+ Self {
+ creator,
+ description,
+ }
+ }
+}
+
+impl Vault {
+ /// Generate a temporary path for receiving
+ pub fn virtual_file_temp_path(&self) -> PathBuf {
+ let random_receive_name = format!("{}", uuid::Uuid::new_v4());
+ self.vault_path
+ .join(SERVER_PATH_VIRTUAL_FILE_TEMP.replace(TEMP_NAME, &random_receive_name))
+ }
+
+ /// Get the directory where virtual files are stored
+ pub fn virtual_file_storage_dir(&self) -> PathBuf {
+ self.vault_path().join(SERVER_PATH_VIRTUAL_FILE_ROOT)
+ }
+
+ /// Get the directory where a specific virtual file is stored
+ pub fn virtual_file_dir(&self, id: VirtualFileId) -> PathBuf {
+ self.vault_path()
+ .join(SERVER_PATH_VIRTUAL_FILE_STORAGE.replace(ID_PARAM, &id.to_string()))
+ }
+
+ /// Get the directory where a specific virtual file's metadata is stored
+ pub fn virtual_file_real_path(
+ &self,
+ id: &VirtualFileId,
+ version: &VirtualFileVersion,
+ ) -> PathBuf {
+ self.vault_path().join(
+ SERVER_FILE_VIRTUAL_FILE_VERSION_INSTANCE
+ .replace(ID_PARAM, &id.to_string())
+ .replace(VERSION_PARAM, &version.to_string()),
+ )
+ }
+
+ /// Get the directory where a specific virtual file's metadata is stored
+ pub fn virtual_file_meta_path(&self, id: &VirtualFileId) -> PathBuf {
+ self.vault_path()
+ .join(SERVER_FILE_VIRTUAL_FILE_META.replace(ID_PARAM, &id.to_string()))
+ }
+
+ /// Get the virtual file with the given ID
+ pub fn virtual_file(&self, id: &VirtualFileId) -> Option<VirtualFile<'_>> {
+ let dir = self.virtual_file_dir(id.clone());
+ if dir.exists() {
+ Some(VirtualFile {
+ id: id.clone(),
+ current_vault: self,
+ })
+ } else {
+ None
+ }
+ }
+
+ /// Get the meta data of the virtual file with the given ID
+ pub async fn virtual_file_meta(
+ &self,
+ id: &VirtualFileId,
+ ) -> Result<VirtualFileMeta, std::io::Error> {
+ let dir = self.virtual_file_meta_path(id);
+ let metadata = VirtualFileMeta::read_from(dir).await?;
+ Ok(metadata)
+ }
+
+ /// Write the meta data of the virtual file with the given ID
+ pub async fn write_virtual_file_meta(
+ &self,
+ id: &VirtualFileId,
+ meta: &VirtualFileMeta,
+ ) -> Result<(), std::io::Error> {
+ let dir = self.virtual_file_meta_path(id);
+ VirtualFileMeta::write_to(meta, dir).await?;
+ Ok(())
+ }
+
+ /// Create a virtual file from a connection instance
+ ///
+ /// It's the only way to create virtual files!
+ ///
+ /// When the target machine executes `write_file`, use this function instead of `read_file`,
+ /// and provide the member ID of the transmitting member.
+ ///
+ /// The system will automatically receive the file and
+ /// create the virtual file.
+ pub async fn create_virtual_file_from_connection(
+ &self,
+ instance: &mut ConnectionInstance,
+ member_id: &MemberId,
+ ) -> Result<VirtualFileId, std::io::Error> {
+ const FIRST_VERSION: &str = "0";
+ let receive_path = self.virtual_file_temp_path();
+ let new_id = format!("vf_{}", Uuid::new_v4());
+ let move_path = self.virtual_file_real_path(&new_id, &FIRST_VERSION.to_string());
+
+ match instance.read_file(receive_path.clone()).await {
+ Ok(_) => {
+ // Read successful, create virtual file
+ // Create default version description
+ let mut version_description =
+ HashMap::<VirtualFileVersion, VirtualFileVersionDescription>::new();
+ version_description.insert(
+ FIRST_VERSION.to_string(),
+ VirtualFileVersionDescription {
+ creator: member_id.clone(),
+ description: "Track".to_string(),
+ },
+ );
+ // Create metadata
+ let mut meta = VirtualFileMeta {
+ current_version: FIRST_VERSION.to_string(),
+ hold_member: String::default(),
+ version_description,
+ histories: Vec::default(),
+ };
+
+ // Add first version
+ meta.histories.push(FIRST_VERSION.to_string());
+
+ // Write metadata to file
+ VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(&new_id)).await?;
+
+ // Move temp file to virtual file directory
+ if let Some(parent) = move_path.parent() {
+ if !parent.exists() {
+ fs::create_dir_all(parent).await?;
+ }
+ }
+ fs::rename(receive_path, move_path).await?;
+
+ Ok(new_id)
+ }
+ Err(e) => {
+ // Read failed, remove temp file.
+ if receive_path.exists() {
+ fs::remove_file(receive_path).await?;
+ }
+
+ Err(Error::new(ErrorKind::Other, e))
+ }
+ }
+ }
+
+ /// Update a virtual file from a connection instance
+ ///
+ /// It's the only way to update virtual files!
+ /// When the target machine executes `write_file`, use this function instead of `read_file`,
+ /// and provide the member ID of the transmitting member.
+ ///
+ /// The system will automatically receive the file and
+ /// update the virtual file.
+ ///
+ /// Note: The specified member must hold the edit right of the file,
+ /// otherwise the file reception will not be allowed.
+ ///
+ /// Make sure to obtain the edit right of the file before calling this function.
+ pub async fn update_virtual_file_from_connection(
+ &self,
+ instance: &mut ConnectionInstance,
+ member: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ new_version: &VirtualFileVersion,
+ description: VirtualFileVersionDescription,
+ ) -> Result<(), std::io::Error> {
+ let new_version = snake_case!(new_version.clone());
+ let mut meta = self.virtual_file_meta(virtual_file_id).await?;
+
+ // Check if the member has edit right
+ self.check_virtual_file_edit_right(member, virtual_file_id)
+ .await?;
+
+ // Check if the new version already exists
+ if meta.version_description.contains_key(&new_version) {
+ return Err(Error::new(
+ ErrorKind::AlreadyExists,
+ format!(
+ "Version `{}` already exists for virtual file `{}`",
+ new_version, virtual_file_id
+ ),
+ ));
+ }
+
+ // Verify success
+ let receive_path = self.virtual_file_temp_path();
+ let move_path = self.virtual_file_real_path(virtual_file_id, &new_version);
+
+ match instance.read_file(receive_path.clone()).await {
+ Ok(_) => {
+ // Read success, move temp file to real path.
+ fs::rename(receive_path, move_path).await?;
+
+ // Update metadata
+ meta.current_version = new_version.clone();
+ meta.version_description
+ .insert(new_version.clone(), description);
+ meta.histories.push(new_version);
+ VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(virtual_file_id))
+ .await?;
+
+ return Ok(());
+ }
+ Err(e) => {
+ // Read failed, remove temp file.
+ if receive_path.exists() {
+ fs::remove_file(receive_path).await?;
+ }
+
+ return Err(Error::new(ErrorKind::Other, e));
+ }
+ }
+ }
+
+ /// Update virtual file from existing version
+ ///
+ /// This operation creates a new version based on the specified old version file instance.
+ /// The new version will retain the same version name as the old version, but use a different version number.
+ /// After the update, this version will be considered newer than the original version when comparing versions.
+ pub async fn update_virtual_file_from_exist_version(
+ &self,
+ member: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ old_version: &VirtualFileVersion,
+ ) -> Result<(), std::io::Error> {
+ let old_version = snake_case!(old_version.clone());
+ let mut meta = self.virtual_file_meta(virtual_file_id).await?;
+
+ // Check if the member has edit right
+ self.check_virtual_file_edit_right(member, virtual_file_id)
+ .await?;
+
+ // Ensure virtual file exist
+ let Some(_) = self.virtual_file(virtual_file_id) else {
+ return Err(Error::new(
+ ErrorKind::NotFound,
+ format!("Virtual file `{}` not found!", virtual_file_id),
+ ));
+ };
+
+ // Ensure version exist
+ if !meta.version_exists(&old_version) {
+ return Err(Error::new(
+ ErrorKind::NotFound,
+ format!("Version `{}` not found!", old_version),
+ ));
+ }
+
+ // Ok, Create new version
+ meta.current_version = old_version.clone();
+ meta.histories.push(old_version);
+ VirtualFileMeta::write_to(&meta, self.virtual_file_meta_path(virtual_file_id)).await?;
+
+ Ok(())
+ }
+
+ /// Grant a member the edit right for a virtual file
+ /// This operation takes effect immediately upon success
+ pub async fn grant_virtual_file_edit_right(
+ &self,
+ member_id: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ ) -> Result<(), std::io::Error> {
+ let mut meta = self.virtual_file_meta(virtual_file_id).await?;
+ meta.hold_member = member_id.clone();
+ self.write_virtual_file_meta(virtual_file_id, &meta).await
+ }
+
+ /// Check if a member has the edit right for a virtual file
+ pub async fn has_virtual_file_edit_right(
+ &self,
+ member_id: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ ) -> Result<bool, std::io::Error> {
+ let meta = self.virtual_file_meta(virtual_file_id).await?;
+ Ok(meta.hold_member.eq(member_id))
+ }
+
+ /// Check if a member has the edit right for a virtual file and return Result
+ /// Returns Ok(()) if the member has edit right, otherwise returns PermissionDenied error
+ pub async fn check_virtual_file_edit_right(
+ &self,
+ member_id: &MemberId,
+ virtual_file_id: &VirtualFileId,
+ ) -> Result<(), std::io::Error> {
+ if !self
+ .has_virtual_file_edit_right(member_id, virtual_file_id)
+ .await?
+ {
+ return Err(Error::new(
+ ErrorKind::PermissionDenied,
+ format!(
+ "Member `{}` not allowed to update virtual file `{}`",
+ member_id, virtual_file_id
+ ),
+ ));
+ }
+ Ok(())
+ }
+
+ /// Revoke the edit right for a virtual file from the current holder
+ /// This operation takes effect immediately upon success
+ pub async fn revoke_virtual_file_edit_right(
+ &self,
+ virtual_file_id: &VirtualFileId,
+ ) -> Result<(), std::io::Error> {
+ let mut meta = self.virtual_file_meta(virtual_file_id).await?;
+ meta.hold_member = String::default();
+ self.write_virtual_file_meta(virtual_file_id, &meta).await
+ }
+}
+
+impl<'a> VirtualFile<'a> {
+ /// Get id of VirtualFile
+ pub fn id(&self) -> VirtualFileId {
+ self.id.clone()
+ }
+
+ /// Read metadata of VirtualFile
+ pub async fn read_meta(&self) -> Result<VirtualFileMeta, std::io::Error> {
+ self.current_vault.virtual_file_meta(&self.id).await
+ }
+}
+
+impl VirtualFileMeta {
+ /// Get all versions of the virtual file
+ pub fn versions(&self) -> &Vec<VirtualFileVersion> {
+ &self.histories
+ }
+
+ /// Get the total number of versions for this virtual file
+ pub fn version_len(&self) -> i32 {
+ self.histories.len() as i32
+ }
+
+ /// Check if a specific version exists
+ /// Returns true if the version exists, false otherwise
+ pub fn version_exists(&self, version: &VirtualFileVersion) -> bool {
+ self.versions().iter().any(|v| v == version)
+ }
+
+ /// Get the version number (index) for a given version name
+ /// Returns None if the version doesn't exist
+ pub fn version_num(&self, version: &VirtualFileVersion) -> Option<i32> {
+ self.histories
+ .iter()
+ .rev()
+ .position(|v| v == version)
+ .map(|pos| (self.histories.len() - 1 - pos) as i32)
+ }
+
+ /// Get the version name for a given version number (index)
+ /// Returns None if the version number is out of range
+ pub fn version_name(&self, version_num: i32) -> Option<VirtualFileVersion> {
+ self.histories.get(version_num as usize).cloned()
+ }
+}