diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-03-18 11:19:51 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-03-18 11:19:51 +0800 |
| commit | 2372495e1a0acb9ffead7651d8ed36a3bb98a15b (patch) | |
| tree | 5f330cdf1616b1e56a7b85b2b2530cdf1422ed54 | |
| parent | 6f8906f06f3efd009275dc23f861f5aaba76ce72 (diff) | |
Add new protocol crate with basic types and operations
| -rw-r--r-- | Cargo.lock | 18 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | protocol/Cargo.toml | 17 | ||||
| -rw-r--r-- | protocol/src/address.rs | 56 | ||||
| -rw-r--r-- | protocol/src/context.rs | 71 | ||||
| -rw-r--r-- | protocol/src/impls.rs | 1 | ||||
| -rw-r--r-- | protocol/src/lib.rs | 4 | ||||
| -rw-r--r-- | protocol/src/member.rs | 29 | ||||
| -rw-r--r-- | protocol/src/member/email.rs | 74 | ||||
| -rw-r--r-- | protocol/src/member/error.rs | 8 | ||||
| -rw-r--r-- | protocol/src/protocol.rs | 118 | ||||
| -rw-r--r-- | protocol/src/protocol/error.rs | 39 | ||||
| -rw-r--r-- | protocol/src/protocol/fetched_info.rs | 4 | ||||
| -rw-r--r-- | protocol/src/protocol/index_transfer.rs | 26 | ||||
| -rw-r--r-- | protocol/src/protocol/operations.rs | 42 | ||||
| -rw-r--r-- | protocol/src/server_space.rs | 1 | ||||
| -rw-r--r-- | protocol/src/user_space.rs | 1 | ||||
| -rw-r--r-- | systems/_constants/src/lib.rs | 33 | ||||
| -rw-r--r-- | systems/workspace/Cargo.toml | 1 | ||||
| -rw-r--r-- | systems/workspace/src/workspace/manager.rs | 1 | ||||
| -rw-r--r-- | systems/workspace/src/workspace/manager/sheet_state.rs | 73 |
21 files changed, 609 insertions, 10 deletions
@@ -991,6 +991,7 @@ dependencies = [ "framework", "hex_display", "jvlib", + "protocol", "serde", "sha1_hash", "sheet_system", @@ -1337,6 +1338,21 @@ dependencies = [ ] [[package]] +name = "protocol" +version = "0.1.0" +dependencies = [ + "constants", + "dirs", + "framework", + "serde", + "sheet_system", + "thiserror", + "tokio", + "vault_system", + "workspace_system", +] + +[[package]] name = "quote" version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2604,7 +2620,9 @@ dependencies = [ "config_system", "constants", "framework", + "just_fmt", "serde", + "sheet_system", "thiserror", "tokio", ] @@ -25,6 +25,7 @@ deprecated = [] members = [ "docs", "ffi", + "protocol", "systems/_asset", "systems/_asset/macros", "systems/_asset/test", @@ -138,6 +139,7 @@ asset_system = { path = "systems/_asset" } config_system = { path = "systems/_config" } constants = { path = "systems/_constants" } framework = { path = "systems/_framework" } +protocol = { path = "protocol" } sheet_system = { path = "systems/sheet" } vault_system = { path = "systems/vault" } workspace_system = { path = "systems/workspace" } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml new file mode 100644 index 0000000..bed182e --- /dev/null +++ b/protocol/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "protocol" +edition = "2024" +version.workspace = true + +[dependencies] +constants = { path = "../systems/_constants" } +framework = { path = "../systems/_framework" } +sheet_system = { path = "../systems/sheet" } +vault_system = { path = "../systems/vault" } +workspace_system = { path = "../systems/workspace" } + +serde.workspace = true +thiserror.workspace = true +tokio.workspace = true + +dirs = "6.0.0" diff --git a/protocol/src/address.rs b/protocol/src/address.rs new file mode 100644 index 0000000..ab49df4 --- /dev/null +++ b/protocol/src/address.rs @@ -0,0 +1,56 @@ +use crate::protocol::BasicProtocol; +use std::marker::PhantomData; + +/// Upstream, used by Workspace to describe the protocol of UpstreamVault +pub struct Upstream<Protocol> +where + Protocol: BasicProtocol, +{ + /// Protocol of the target upstream machine + _p: PhantomData<Protocol>, + + /// Address of the target upstream machine + target_address: String, +} + +impl<Protocol> Upstream<Protocol> +where + Protocol: BasicProtocol, +{ + pub fn new(addr: &str) -> Self { + Upstream { + _p: PhantomData, + target_address: addr.to_string(), + } + } +} + +/// Host, used by Vault to describe its own protocol +pub struct Host<Protocol> +where + Protocol: BasicProtocol, +{ + /// Protocol of the target upstream machine + _p: PhantomData<Protocol>, +} + +impl<Protocol> Upstream<Protocol> +where + Protocol: BasicProtocol, +{ + pub fn address(addr: &str) -> Self { + Upstream { + _p: PhantomData, + target_address: addr.to_string(), + } + } +} + +impl<Protocol> Host<Protocol> +where + Protocol: BasicProtocol, +{ + pub fn new() -> Self { + Host { _p: PhantomData } + } +} diff --git a/protocol/src/context.rs b/protocol/src/context.rs new file mode 100644 index 0000000..4fdddb9 --- /dev/null +++ b/protocol/src/context.rs @@ -0,0 +1,71 @@ +use framework::space::{Space, SpaceRoot}; + +pub struct ProtocolContext<User, Server> +where + User: SpaceRoot, + Server: SpaceRoot, +{ + /// Current context's Vault + pub remote: Option<Space<Server>>, + + /// Current context's Workspace + pub user: Option<Space<User>>, +} + +impl<User, Server> ProtocolContext<User, Server> +where + User: SpaceRoot, + Server: SpaceRoot, +{ + /// Create a new local context with a user + pub fn new_local(user: Space<User>) -> Self { + Self { + remote: None, + user: Some(user), + } + } + + /// Create a new remote context with a server + pub fn new_remote(server: Space<Server>) -> Self { + Self { + remote: Some(server), + user: None, + } + } +} + +impl<User, Server> ProtocolContext<User, Server> +where + User: SpaceRoot, + Server: SpaceRoot, +{ + /// Check if the context is remote (has a server) + pub fn is_remote(&self) -> bool { + self.remote.is_some() + } + + /// Check if the context is local (has a user) + pub fn is_local(&self) -> bool { + self.user.is_some() + } + + /// Get the remote vault if it exists + pub fn remote(&self) -> Option<&Space<Server>> { + self.remote.as_ref() + } + + /// Get the local workspace if it exists + pub fn local(&self) -> Option<&Space<User>> { + self.user.as_ref() + } + + /// Unwrap the local workspace, panics if not local + pub fn unwrap_local(&self) -> &Space<User> { + self.user.as_ref().expect("Not a local context") + } + + /// Unwrap the remote vault, panics if not remote + pub fn unwrap_remote(&self) -> &Space<Server> { + self.remote.as_ref().expect("Not a remote context") + } +} diff --git a/protocol/src/impls.rs b/protocol/src/impls.rs new file mode 100644 index 0000000..453d7dd --- /dev/null +++ b/protocol/src/impls.rs @@ -0,0 +1 @@ +pub mod jvcs; diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs new file mode 100644 index 0000000..6b54baa --- /dev/null +++ b/protocol/src/lib.rs @@ -0,0 +1,4 @@ +pub mod address; +pub mod context; +pub mod member; +pub mod protocol; diff --git a/protocol/src/member.rs b/protocol/src/member.rs new file mode 100644 index 0000000..a892118 --- /dev/null +++ b/protocol/src/member.rs @@ -0,0 +1,29 @@ +use crate::member::email::EMailAddress; +use serde::{Deserialize, Serialize}; + +pub mod email; +pub mod error; + +#[derive(Debug, Clone, Eq, Serialize, Deserialize)] +pub struct Member { + name: String, + mail: EMailAddress, +} + +impl PartialEq for Member { + fn eq(&self, other: &Self) -> bool { + self.mail == other.mail + } +} + +impl std::hash::Hash for Member { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.mail.hash(state); + } +} + +impl std::fmt::Display for Member { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} diff --git a/protocol/src/member/email.rs b/protocol/src/member/email.rs new file mode 100644 index 0000000..a3b829f --- /dev/null +++ b/protocol/src/member/email.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::member::error::EMailAddressParseError; +use std::fmt::{Display, Formatter}; + +#[derive(Clone, Debug, Eq)] +pub struct EMailAddress { + account: String, + domain: String, +} + +impl std::hash::Hash for EMailAddress { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.account.hash(state); + self.domain.hash(state); + } +} + +impl PartialEq for EMailAddress { + fn eq(&self, other: &Self) -> bool { + self.account == other.account && self.domain == other.domain + } +} + +impl Display for EMailAddress { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.account, self.domain) + } +} + +impl TryFrom<&str> for EMailAddress { + type Error = EMailAddressParseError; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + let trimmed_value = value.trim(); + let parts: Vec<&str> = trimmed_value.split('@').collect(); + if parts.len() != 2 { + return Err(EMailAddressParseError::InvalidFormat); + } + let account = parts[0].trim().to_string(); + let domain = parts[1].trim().to_string(); + if account.is_empty() || domain.is_empty() { + return Err(EMailAddressParseError::EmptyPart); + } + Ok(Self { account, domain }) + } +} + +impl TryFrom<String> for EMailAddress { + type Error = EMailAddressParseError; + + fn try_from(value: String) -> Result<Self, Self::Error> { + Self::try_from(value.as_str()) + } +} + +impl Serialize for EMailAddress { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for EMailAddress { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + EMailAddress::try_from(s).map_err(serde::de::Error::custom) + } +} diff --git a/protocol/src/member/error.rs b/protocol/src/member/error.rs new file mode 100644 index 0000000..143329c --- /dev/null +++ b/protocol/src/member/error.rs @@ -0,0 +1,8 @@ +#[derive(Debug, thiserror::Error)] +pub enum EMailAddressParseError { + #[error("Invalid email format")] + InvalidFormat, + + #[error("Account or domain cannot be empty")] + EmptyPart, +} diff --git a/protocol/src/protocol.rs b/protocol/src/protocol.rs new file mode 100644 index 0000000..a097989 --- /dev/null +++ b/protocol/src/protocol.rs @@ -0,0 +1,118 @@ +use std::path::PathBuf; + +use crate::{ + context::ProtocolContext, + member::Member, + protocol::{ + error::{FetchLatestInfoFailed, ProtocolAuthorizeFailed, VaultOperationFailed}, + fetched_info::FetchedInfo, + index_transfer::IndexesTransfer, + operations::{VaultHostOperations, VaultOperations}, + }, +}; +use framework::space::{Space, SpaceRoot}; +use vault_system::vault::Vault; +use workspace_system::workspace::Workspace; + +pub mod error; +pub mod fetched_info; +pub mod index_transfer; +pub mod operations; + +pub trait BasicProtocol { + type User: SpaceRoot; + type Server: SpaceRoot; + + /// Protocol name + /// For example: return "jvcs" to accept protocols starting with "jvcs://" + fn protocol_name() -> &'static str; + + /// Authentication + /// + /// authorizing is executed on the client side, and for some protocols also on the server side, + /// ultimately outputting the authentication result + /// - Ok(Member): indicates a successfully authenticated member + /// - Err(ProtocolAuthorizeFailed): indicates the reason for failure + fn authorizing( + &self, + ctx: &ProtocolContext<Self::User, Self::Server>, + ) -> impl Future<Output = Result<Member, ProtocolAuthorizeFailed>> + Send; + + /// Host authentication + /// + /// Authenticate as a host (administrator) on the server side. + /// Returns the authenticated host member if successful. + fn authorizing_host( + &self, + ctx: &ProtocolContext<Self::User, Self::Server>, + ) -> impl Future<Output = Result<Member, ProtocolAuthorizeFailed>> + Send; + + /// Build user space + /// + /// Build and return the user space based on the given current directory path. + fn build_user_space(&self, current_dir: PathBuf) -> Space<Self::User>; + + /// Build server space + /// + /// Build and return the server space based on the given current directory path. + fn build_server_space(&self, current_dir: PathBuf) -> Space<Self::Server>; + + /// Fetch the latest information from upstream + /// + /// - On the local side, workspace will be Some + /// - On the remote side, vault will be Some + /// + /// # Result + /// - The remote side returns Ok(Some(FetchedInfo)) to send data to the local side + /// - The local side returns Ok(Some(FetchedInfo)) to get the latest information + fn fetch_latest_info( + &self, + workspace: Option<Space<Workspace>>, + vault: Option<Space<Vault>>, + ) -> impl Future<Output = Result<Option<FetchedInfo>, FetchLatestInfoFailed>> + Send; + + /// Transfer indexes + /// + /// - `index_transfer` and `storage` represent the index file and + /// the corresponding block storage path, respectively. + /// - If `vault` is Some, send block information from the Vault. + /// - If `workspace` is Some, send block information from the Workspace. + /// + /// Each block transfer is atomic, but the overall transfer is not atomic. + /// Blocks that have been successfully transferred are permanently retained on the upstream side. + /// + /// After the block information is correctly stored on the other side, + /// transfer the `index_file` and register it to the corresponding IndexSource. + /// + /// If the IndexSource to be registered is a `local_id`, the server will issue a `remote_id` + /// and write it into the local ID mapping. + fn transfer_indexes( + &self, + index_transfer: IndexesTransfer, + storage: PathBuf, + workspace: Option<Space<Workspace>>, + vault: Option<Space<Vault>>, + ) -> impl Future<Output = Result<(), FetchLatestInfoFailed>> + Send; + + /// Handle operations + /// + /// Requests are sent from the client to the server, + /// and the following requests are uniformly processed on the server side. + fn handle_operation( + &self, + operation: VaultOperations, + authorized_member: Member, + vault: Space<Vault>, + ) -> impl Future<Output = Result<(), VaultOperationFailed>> + Send; + + /// Handle host operations + /// + /// Requests are sent from the host (administrator) to the server, + /// and the following requests are uniformly processed on the server side. + fn handle_host_operation( + &self, + operation: VaultHostOperations, + authorized_host: Member, + vault: Space<Vault>, + ) -> impl Future<Output = Result<(), VaultOperationFailed>> + Send; +} diff --git a/protocol/src/protocol/error.rs b/protocol/src/protocol/error.rs new file mode 100644 index 0000000..ed39aaa --- /dev/null +++ b/protocol/src/protocol/error.rs @@ -0,0 +1,39 @@ +use crate::member::Member; + +#[derive(Debug, thiserror::Error)] +pub enum ProtocolAuthorizeFailed { + #[error("Member not found")] + MemberNotFound, + + #[error("No permission")] + NoPermission, +} + +#[derive(Debug, thiserror::Error)] +pub enum FetchLatestInfoFailed { + #[error("Connection failed")] + Connection, + + #[error("Permission denied")] + PermissionDenied, +} + +#[derive(Debug, thiserror::Error)] +pub enum VaultOperationFailed { + /// Index is already held + /// Cannot advance version, claim, or relinquish ownership + #[error("Index already held by `{0}`")] + IndexAlreadyHeldBy(Member), + + /// Sheet depends on local namespace + /// Cannot backup or write Ref + #[error("Sheet depends on local namespace")] + SheetDependsOnLocalNamespace, + + /// Operation not supported by the current protocol + #[error("Operation not supported")] + OperationNotSupported, + + #[error("IO error: `{0}`")] + IOError(#[from] std::io::Error), +} diff --git a/protocol/src/protocol/fetched_info.rs b/protocol/src/protocol/fetched_info.rs new file mode 100644 index 0000000..1ad64de --- /dev/null +++ b/protocol/src/protocol/fetched_info.rs @@ -0,0 +1,4 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FetchedInfo {} diff --git a/protocol/src/protocol/index_transfer.rs b/protocol/src/protocol/index_transfer.rs new file mode 100644 index 0000000..4170289 --- /dev/null +++ b/protocol/src/protocol/index_transfer.rs @@ -0,0 +1,26 @@ +use std::{collections::HashMap, path::PathBuf}; + +use sheet_system::index_source::IndexSource; + +#[derive(Default)] +pub struct IndexesTransfer { + inner: HashMap<PathBuf, IndexSource>, +} + +impl IndexesTransfer { + /// Insert an index file path and its source into the collection + pub fn insert(&mut self, path: PathBuf, source: IndexSource) -> Option<IndexSource> { + self.inner.insert(path, source) + } + + /// Returns an iterator over the index file paths and their sources + pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &IndexSource)> { + self.inner.iter() + } + + /// Insert an index file path and its source into the collection and return self for chaining + pub fn with(mut self, path: PathBuf, source: IndexSource) -> Self { + self.inner.insert(path, source); + self + } +} diff --git a/protocol/src/protocol/operations.rs b/protocol/src/protocol/operations.rs new file mode 100644 index 0000000..5d49b29 --- /dev/null +++ b/protocol/src/protocol/operations.rs @@ -0,0 +1,42 @@ +use sheet_system::sheet::Sheet; + +use crate::member::Member; + +pub type SheetName = String; + +pub enum VaultOperations { + /// Claim ownership of an index + HoldIndex(u32), + + /// Release ownership of an index + ThrowIndex(u32), + + /// Backup a Sheet to personal space + BackupSheet(Sheet), + + /// Download a Sheet from personal space + DownloadSheet(SheetName), + + /// Download a RefSheet from public space + DownloadRefSheet(SheetName), +} + +pub enum VaultHostOperations { + /// Forcefully grant ownership of an index to a member + HoldIndexForce(Member, u32), + + /// Forcefully discard ownership of some indices + ThrowIndexForce(Vec<u32>), + + /// Write a RefSheet to upstream + WriteRefSheet(Sheet), + + /// Erase a Ref + EraseRefSheet(SheetName), + + /// Erase some indices + DangerousEraseIndex(Vec<u32>), + + /// Erase some versions of an index + DangerousEraseVersion(u32, Vec<u16>), +} diff --git a/protocol/src/server_space.rs b/protocol/src/server_space.rs new file mode 100644 index 0000000..453d7dd --- /dev/null +++ b/protocol/src/server_space.rs @@ -0,0 +1 @@ +pub mod jvcs; diff --git a/protocol/src/user_space.rs b/protocol/src/user_space.rs new file mode 100644 index 0000000..453d7dd --- /dev/null +++ b/protocol/src/user_space.rs @@ -0,0 +1 @@ +pub mod jvcs; diff --git a/systems/_constants/src/lib.rs b/systems/_constants/src/lib.rs index b9d313c..e576be3 100644 --- a/systems/_constants/src/lib.rs +++ b/systems/_constants/src/lib.rs @@ -14,7 +14,7 @@ pub mod server { /// File constants #[constants_macros::constants("server_file")] pub mod files { - c! { CONFIG = "config.toml" } + c! { CONFIG = "server.toml" } // Storage location for keys and passwords c! { KEY = "auth/key/{member_name}.pem" } @@ -114,10 +114,13 @@ pub mod workspace { // ### Sheets ### // Records the latest state of local physical files, used to calculate deviations - c! { LOCAL_STATUS = ".jv/sheets/{account}/{sheet}.local" } + c! { LOCAL_STATUS = ".jv/sheets/{sheet}.local" } // Personal sheet, represents the desired file structure, held only by the member, can be backed up to the vault - c! { SHEET = ".jv/sheets/{account}/{sheet}.sheet" } + c! { SHEET = ".jv/sheets/{sheet}.sheet" } + + // Current sheet name + c! { CURRENT_SHEET = ".jv/sheets/CURRENT" } // Draft file, when switching to another sheet, fully records modified but untracked files c! { DRAFTED_FILE = ".jv/drafts/{account}_{sheet}/{mapping}" } @@ -131,7 +134,7 @@ pub mod workspace { pub mod dirs { c! { WORKSPACE = ".jv" } c! { VAULT_MIRROR = ".jv/UPSTREAM/" } - c! { LOCAL_SHEETS = ".jv/sheets/{account}/" } + c! { LOCAL_SHEETS = ".jv/sheets/" } c! { DRAFT_AREA = ".jv/drafts/{account}_{sheet}/" } c! { ID_MAPPING = ".jv/idmp/" } c! { WORKING_AREA = "" } @@ -141,26 +144,36 @@ pub mod workspace { /// File and directory path constants for the user root #[allow(unused)] pub mod user { + /// Others + #[constants_macros::constants("user_value")] + pub mod values { + c! { USER_CONFIG_NAME = "usr.toml" } + } + /// File path constants #[constants_macros::constants("user_file")] pub mod files { // Upstream public key, stored after initial login, used to verify the trustworthiness of that upstream - c! { UPSTREAM_PUB = "upstreams/{upstream_addr}.pem" } + c! { JVCS_UPSTREAM_PUB = ".jvcs/upstreams/{jvcs_upstream_addr}.pem" } // Account private key, stored only locally, used for login authentication - c! { PRIVATE_KEY = "private/{account}.pem" } + c! { PRIVATE_KEY = ".jvcs/private/{account}.pem" } // Account public key, automatically generated from the private key and stored, // will be placed into the server's "join request list" upon initial login (if server.toml permits this action) // The server administrator can optionally approve this request - c! { PUBLIC_KEY = "public/{account}.pem" } + c! { PUBLIC_KEY = ".jvcs/public/{account}.pem" } + + // Account configuration file + c! { USER_CONFIG = ".jvcs/usr.toml" } } /// Directory path constants #[constants_macros::constants("user_dir")] pub mod dirs { - c! { UPSTREAM_PUBS = "upstreams/" } - c! { PRIVATE_KEYS = "private/" } - c! { PUBLIC_KEYS = "public/" } + c! { ROOT = ".jvcs/" } + c! { UPSTREAM_PUBS = ".jvcs/upstreams/" } + c! { PRIVATE_KEYS = ".jvcs/private/" } + c! { PUBLIC_KEYS = ".jvcs/public/" } } } diff --git a/systems/workspace/Cargo.toml b/systems/workspace/Cargo.toml index c601370..90690f8 100644 --- a/systems/workspace/Cargo.toml +++ b/systems/workspace/Cargo.toml @@ -10,6 +10,7 @@ constants = { path = "../_constants" } framework = { path = "../_framework" } sheet_system = { path = "../sheet" } +just_fmt.workspace = true serde.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/systems/workspace/src/workspace/manager.rs b/systems/workspace/src/workspace/manager.rs index adee9b7..58eb409 100644 --- a/systems/workspace/src/workspace/manager.rs +++ b/systems/workspace/src/workspace/manager.rs @@ -4,6 +4,7 @@ use constants::workspace::files::workspace_file_config; use framework::space::Space; pub mod id_aliases; +pub mod sheet_state; pub struct WorkspaceManager { space: Space<Workspace>, diff --git a/systems/workspace/src/workspace/manager/sheet_state.rs b/systems/workspace/src/workspace/manager/sheet_state.rs new file mode 100644 index 0000000..5603adf --- /dev/null +++ b/systems/workspace/src/workspace/manager/sheet_state.rs @@ -0,0 +1,73 @@ +use std::path::PathBuf; + +use crate::workspace::manager::WorkspaceManager; +use asset_system::asset::ReadOnlyAsset; +use constants::workspace::files::{workspace_file_current_sheet, workspace_file_sheet}; +use framework::space::error::SpaceError; +use just_fmt::snake_case; +use sheet_system::sheet::{Sheet, SheetData}; + +impl WorkspaceManager { + /// Read the name of the currently active Sheet + pub async fn using_sheet_name(&self) -> Result<Option<String>, SpaceError> { + match self + .space + .read_to_string(workspace_file_current_sheet()) + .await + { + Ok(s) => Ok(Some(s.trim().to_string())), + Err(SpaceError::Io(io_error)) => match io_error.kind() { + std::io::ErrorKind::NotFound => Ok(None), + _ => Err(SpaceError::Io(io_error)), + }, + Err(e) => Err(e), + } + } + + /// Set the name of the currently active Sheet + pub async fn edit_using_sheet_name(&self, name: &str) -> Result<(), SpaceError> { + self.space + .write(workspace_file_current_sheet(), name.as_bytes()) + .await + } + + /// Read a sheet from the workspace space by name + /// + /// Simple read of Sheet data, no disk write operations involved + pub async fn read_sheet(&self, sheet_name: &str) -> Option<Sheet> { + let sheet_name = snake_case!(sheet_name); + let sheet_path = self.get_sheet_path(&sheet_name); + + let mut sheet_data = SheetData::empty(); + if sheet_path.exists() { + // If reading fails, treat it as if the sheet does not exist and return `None` + sheet_data.full_read(sheet_path).await.ok()?; + return Some(sheet_data.pack(sheet_name)); + } else { + None + } + } + + /// Get a resource pointing to local Sheet data by name + /// + /// Can be used to load content, edit, and transactionally write + pub fn get_sheet_data_asset(&self, sheet_name: &str) -> Option<ReadOnlyAsset<SheetData>> { + let sheet_name = snake_case!(sheet_name); + let sheet_path = self.get_sheet_path(&sheet_name); + if sheet_path.exists() { + return Some(sheet_path.into()); + } + None + } + + /// Get the local filesystem path for a sheet by name + pub fn get_sheet_path(&self, sheet_name: impl AsRef<str>) -> PathBuf { + let sheet_name = sheet_name.as_ref(); + self.space + .local_path(workspace_file_sheet(&sheet_name)) + // The `local_path` only produces path formatting errors. + // If the path cannot be guaranteed to be correct, + // execution should not continue, so we unwrap() + .unwrap() + } +} |
