summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock18
-rw-r--r--Cargo.toml2
-rw-r--r--protocol/Cargo.toml17
-rw-r--r--protocol/src/address.rs56
-rw-r--r--protocol/src/context.rs71
-rw-r--r--protocol/src/impls.rs1
-rw-r--r--protocol/src/lib.rs4
-rw-r--r--protocol/src/member.rs29
-rw-r--r--protocol/src/member/email.rs74
-rw-r--r--protocol/src/member/error.rs8
-rw-r--r--protocol/src/protocol.rs118
-rw-r--r--protocol/src/protocol/error.rs39
-rw-r--r--protocol/src/protocol/fetched_info.rs4
-rw-r--r--protocol/src/protocol/index_transfer.rs26
-rw-r--r--protocol/src/protocol/operations.rs42
-rw-r--r--protocol/src/server_space.rs1
-rw-r--r--protocol/src/user_space.rs1
-rw-r--r--systems/_constants/src/lib.rs33
-rw-r--r--systems/workspace/Cargo.toml1
-rw-r--r--systems/workspace/src/workspace/manager.rs1
-rw-r--r--systems/workspace/src/workspace/manager/sheet_state.rs73
21 files changed, 609 insertions, 10 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a231b6c..b59b0da 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
]
diff --git a/Cargo.toml b/Cargo.toml
index 78216b0..31b13ff 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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()
+ }
+}