aboutsummaryrefslogtreecommitdiff
path: root/legacy_data/src/data/local/workspace_config.rs
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-02-05 22:35:05 +0800
committer魏曹先生 <1992414357@qq.com>2026-02-05 22:35:05 +0800
commit27f6414ad1ff451feb0044af62f37dc2a6255ffa (patch)
treecb5693bc014cc8579dcf02a730fd4d2a5dfcf1a5 /legacy_data/src/data/local/workspace_config.rs
parentade2fcb9302a4ab759795820dbde3b2b269490ee (diff)
Remove examples and legacy code, update .gitignore
- Delete examples directory and its example action system - Rename actions/ to legacy_actions/ and data/ to legacy_data/ - Update Cargo.toml license file reference - Move setup scripts to scripts/dev/ directory - Add todo.txt patterns to .gitignore
Diffstat (limited to 'legacy_data/src/data/local/workspace_config.rs')
-rw-r--r--legacy_data/src/data/local/workspace_config.rs374
1 files changed, 374 insertions, 0 deletions
diff --git a/legacy_data/src/data/local/workspace_config.rs b/legacy_data/src/data/local/workspace_config.rs
new file mode 100644
index 0000000..f97d049
--- /dev/null
+++ b/legacy_data/src/data/local/workspace_config.rs
@@ -0,0 +1,374 @@
+use cfg_file::ConfigFile;
+use cfg_file::config::ConfigFile;
+use serde::{Deserialize, Serialize};
+use std::io::Error;
+use std::net::SocketAddr;
+use std::path::Path;
+use std::path::PathBuf;
+use string_proc::snake_case;
+
+use crate::constants::CLIENT_FILE_WORKSPACE;
+use crate::constants::CLIENT_FOLDER_WORKSPACE_ROOT_NAME;
+use crate::constants::CLIENT_PATH_LOCAL_DRAFT;
+use crate::constants::CLIENT_PATH_WORKSPACE_ROOT;
+use crate::constants::KEY_ACCOUNT;
+use crate::constants::KEY_SHEET_NAME;
+use crate::constants::PORT;
+use crate::data::local::latest_info::LatestInfo;
+use crate::data::member::MemberId;
+use crate::data::sheet::SheetName;
+use crate::data::vault::vault_config::VaultUuid;
+use crate::env::current_local_path;
+
+#[derive(Serialize, Deserialize, ConfigFile, Clone)]
+#[cfg_file(path = CLIENT_FILE_WORKSPACE)]
+pub struct LocalConfig {
+ /// The upstream address, representing the upstream address of the local workspace,
+ /// to facilitate timely retrieval of new updates from the upstream source.
+ #[serde(rename = "addr")]
+ upstream_addr: SocketAddr,
+
+ /// The member ID used by the current local workspace.
+ /// This ID will be used to verify access permissions when connecting to the upstream server.
+ #[serde(rename = "as")]
+ using_account: MemberId,
+
+ /// Whether the current member is interacting as a host.
+ /// In host mode, full Vault operation permissions are available except for adding new content.
+ #[serde(rename = "host")]
+ using_host_mode: bool,
+
+ /// Whether the local workspace is stained.
+ ///
+ /// If stained, it can only set an upstream server with the same identifier.
+ ///
+ /// If the value is None, it means not stained;
+ /// otherwise, it contains the stain identifier (i.e., the upstream vault's unique ID)
+ #[serde(rename = "up_uid")]
+ stained_uuid: Option<VaultUuid>,
+
+ /// The name of the sheet currently in use.
+ #[serde(rename = "use")]
+ sheet_in_use: Option<SheetName>,
+}
+
+impl Default for LocalConfig {
+ fn default() -> Self {
+ Self {
+ upstream_addr: SocketAddr::V4(std::net::SocketAddrV4::new(
+ std::net::Ipv4Addr::new(127, 0, 0, 1),
+ PORT,
+ )),
+ using_account: "unknown".to_string(),
+ using_host_mode: false,
+ stained_uuid: None,
+ sheet_in_use: None,
+ }
+ }
+}
+
+impl LocalConfig {
+ /// Set the vault address.
+ pub fn set_vault_addr(&mut self, addr: SocketAddr) {
+ self.upstream_addr = addr;
+ }
+
+ /// Get the vault address.
+ pub fn vault_addr(&self) -> SocketAddr {
+ self.upstream_addr
+ }
+
+ /// Set the currently used account
+ pub fn set_current_account(&mut self, account: MemberId) -> Result<(), std::io::Error> {
+ if self.sheet_in_use().is_some() {
+ return Err(Error::new(
+ std::io::ErrorKind::DirectoryNotEmpty,
+ "Please exit the current sheet before switching accounts",
+ ));
+ }
+ self.using_account = account;
+ Ok(())
+ }
+
+ /// Set the host mode
+ pub fn set_host_mode(&mut self, host_mode: bool) {
+ self.using_host_mode = host_mode;
+ }
+
+ /// Set the currently used sheet
+ pub async fn use_sheet(&mut self, sheet: SheetName) -> Result<(), std::io::Error> {
+ let sheet = snake_case!(sheet);
+
+ // Check if the sheet is already in use
+ if self.sheet_in_use().is_some() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::AlreadyExists,
+ "Sheet already in use",
+ ));
+ };
+
+ // Check if the local path exists
+ let local_path = self.get_local_path().await?;
+
+ // Get latest info
+ let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
+ &local_path,
+ &self.current_account(),
+ ))
+ .await
+ else {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ "No latest info found",
+ ));
+ };
+
+ // Check if the sheet exists
+ if !latest_info.visible_sheets.contains(&sheet) {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ "Sheet not found",
+ ));
+ }
+
+ // Check if there are any files or folders other than .jv
+ self.check_local_path_empty(&local_path).await?;
+
+ // Get the draft folder path
+ let draft_folder = self.draft_folder(&self.using_account, &sheet, &local_path);
+
+ if draft_folder.exists() {
+ // Exists
+ // Move the contents of the draft folder to the local path with rollback support
+ self.move_draft_to_local(&draft_folder, &local_path).await?;
+ }
+
+ self.sheet_in_use = Some(sheet);
+ LocalConfig::write(self).await?;
+
+ Ok(())
+ }
+
+ /// Exit the currently used sheet
+ pub async fn exit_sheet(&mut self) -> Result<(), std::io::Error> {
+ // Check if the sheet is already in use
+ if self.sheet_in_use().is_none() {
+ return Ok(());
+ }
+
+ // Check if the local path exists
+ let local_path = self.get_local_path().await?;
+
+ // Get the current sheet name
+ let sheet_name = self.sheet_in_use().as_ref().unwrap().clone();
+
+ // Get the draft folder path
+ let draft_folder = self.draft_folder(&self.using_account, &sheet_name, &local_path);
+
+ // Create the draft folder if it doesn't exist
+ if !draft_folder.exists() {
+ std::fs::create_dir_all(&draft_folder).map_err(std::io::Error::other)?;
+ }
+
+ // Move all files and folders (except .jv folder) to the draft folder with rollback support
+ self.move_local_to_draft(&local_path, &draft_folder).await?;
+
+ // Clear the sheet in use
+ self.sheet_in_use = None;
+ LocalConfig::write(self).await?;
+
+ Ok(())
+ }
+
+ /// Get local path or return error
+ async fn get_local_path(&self) -> Result<PathBuf, std::io::Error> {
+ current_local_path().ok_or_else(|| {
+ std::io::Error::new(std::io::ErrorKind::NotFound, "Fail to get local path")
+ })
+ }
+
+ /// Check if local path is empty (except for .jv folder)
+ async fn check_local_path_empty(&self, local_path: &Path) -> Result<(), std::io::Error> {
+ let jv_folder = local_path.join(CLIENT_PATH_WORKSPACE_ROOT);
+ let mut entries = std::fs::read_dir(local_path).map_err(std::io::Error::other)?;
+
+ if entries.any(|entry| {
+ if let Ok(entry) = entry {
+ let path = entry.path();
+ path != jv_folder
+ && path.file_name().and_then(|s| s.to_str())
+ != Some(CLIENT_FOLDER_WORKSPACE_ROOT_NAME)
+ } else {
+ false
+ }
+ }) {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::DirectoryNotEmpty,
+ "Local path is not empty!",
+ ));
+ }
+
+ Ok(())
+ }
+
+ /// Move contents from draft folder to local path with rollback support
+ async fn move_draft_to_local(
+ &self,
+ draft_folder: &Path,
+ local_path: &Path,
+ ) -> Result<(), std::io::Error> {
+ let draft_entries: Vec<_> = std::fs::read_dir(draft_folder)
+ .map_err(std::io::Error::other)?
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(std::io::Error::other)?;
+
+ let mut moved_items: Vec<MovedItem> = Vec::new();
+
+ for entry in &draft_entries {
+ let entry_path = entry.path();
+ let target_path = local_path.join(entry_path.file_name().unwrap());
+
+ // Move each file/directory from draft folder to local path
+ std::fs::rename(&entry_path, &target_path).map_err(|e| {
+ // Rollback all previously moved items
+ for moved_item in &moved_items {
+ let _ = std::fs::rename(&moved_item.target, &moved_item.source);
+ }
+ std::io::Error::other(e)
+ })?;
+
+ moved_items.push(MovedItem {
+ source: entry_path.clone(),
+ target: target_path.clone(),
+ });
+ }
+
+ // Remove the now-empty draft folder
+ std::fs::remove_dir(draft_folder).map_err(|e| {
+ // Rollback all moved items if folder removal fails
+ for moved_item in &moved_items {
+ let _ = std::fs::rename(&moved_item.target, &moved_item.source);
+ }
+ std::io::Error::other(e)
+ })?;
+
+ Ok(())
+ }
+
+ /// Move contents from local path to draft folder with rollback support (except .jv folder)
+ async fn move_local_to_draft(
+ &self,
+ local_path: &Path,
+ draft_folder: &Path,
+ ) -> Result<(), std::io::Error> {
+ let jv_folder = local_path.join(CLIENT_PATH_WORKSPACE_ROOT);
+ let entries: Vec<_> = std::fs::read_dir(local_path)
+ .map_err(std::io::Error::other)?
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(std::io::Error::other)?;
+
+ let mut moved_items: Vec<MovedItem> = Vec::new();
+
+ for entry in &entries {
+ let entry_path = entry.path();
+
+ // Skip the .jv folder
+ if entry_path == jv_folder
+ || entry_path.file_name().and_then(|s| s.to_str())
+ == Some(CLIENT_FOLDER_WORKSPACE_ROOT_NAME)
+ {
+ continue;
+ }
+
+ let target_path = draft_folder.join(entry_path.file_name().unwrap());
+
+ // Move each file/directory from local path to draft folder
+ std::fs::rename(&entry_path, &target_path).map_err(|e| {
+ // Rollback all previously moved items
+ for moved_item in &moved_items {
+ let _ = std::fs::rename(&moved_item.target, &moved_item.source);
+ }
+ std::io::Error::other(e)
+ })?;
+
+ moved_items.push(MovedItem {
+ source: entry_path.clone(),
+ target: target_path.clone(),
+ });
+ }
+
+ Ok(())
+ }
+
+ /// Get the currently used account
+ pub fn current_account(&self) -> MemberId {
+ self.using_account.clone()
+ }
+
+ /// Check if the current member is interacting as a host.
+ pub fn is_host_mode(&self) -> bool {
+ self.using_host_mode
+ }
+
+ /// Check if the local workspace is stained.
+ pub fn stained(&self) -> bool {
+ self.stained_uuid.is_some()
+ }
+
+ /// Get the UUID of the vault that the local workspace is stained with.
+ pub fn stained_uuid(&self) -> Option<VaultUuid> {
+ self.stained_uuid
+ }
+
+ /// Stain the local workspace with the given UUID.
+ pub fn stain(&mut self, uuid: VaultUuid) {
+ self.stained_uuid = Some(uuid);
+ }
+
+ /// Unstain the local workspace.
+ pub fn unstain(&mut self) {
+ self.stained_uuid = None;
+ }
+
+ /// Get the upstream address.
+ pub fn upstream_addr(&self) -> SocketAddr {
+ self.upstream_addr
+ }
+
+ /// Get the currently used sheet
+ pub fn sheet_in_use(&self) -> &Option<SheetName> {
+ &self.sheet_in_use
+ }
+
+ /// Get draft folder
+ pub fn draft_folder(
+ &self,
+ account: &MemberId,
+ sheet_name: &SheetName,
+ local_workspace_path: impl Into<PathBuf>,
+ ) -> PathBuf {
+ let account_str = snake_case!(account.as_str());
+ let sheet_name_str = snake_case!(sheet_name.as_str());
+ let draft_path = CLIENT_PATH_LOCAL_DRAFT
+ .replace(KEY_ACCOUNT, &account_str)
+ .replace(KEY_SHEET_NAME, &sheet_name_str);
+ local_workspace_path.into().join(draft_path)
+ }
+
+ /// Get current draft folder
+ pub fn current_draft_folder(&self) -> Option<PathBuf> {
+ let Some(sheet_name) = self.sheet_in_use() else {
+ return None;
+ };
+
+ let current_dir = current_local_path()?;
+
+ Some(self.draft_folder(&self.using_account, sheet_name, current_dir))
+ }
+}
+
+#[derive(Clone)]
+struct MovedItem {
+ source: PathBuf,
+ target: PathBuf,
+}