summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock91
-rw-r--r--Cargo.toml4
-rw-r--r--rola-bucket/Cargo.toml1
-rw-r--r--rola-bucket/res/bucket.toml3
-rw-r--r--rola-bucket/src/bucket.rs6
-rw-r--r--rola-bucket/src/bucket/bind.rs210
-rw-r--r--rola-bucket/src/bucket/bind/test.rs231
-rw-r--r--rola-bucket/src/bucket/config.rs30
-rw-r--r--rola-bucket/src/bucket/idmap.rs0
-rw-r--r--rola-bucket/src/bucket/init.rs27
-rw-r--r--rola-bucket/src/bucket/space.rs4
-rw-r--r--rola-cli/Cargo.toml10
-rw-r--r--rola-cli/locales/errors/common.toml25
-rw-r--r--rola-cli/locales/errors/i18n_space_error.toml23
-rw-r--r--rola-cli/locales/i18n_bucket_manager.toml16
-rw-r--r--rola-cli/src/bin/rola.rs16
-rw-r--r--rola-cli/src/bucket_mgr.rs2
-rw-r--r--rola-cli/src/bucket_mgr/bind.rs262
-rw-r--r--rola-cli/src/bucket_mgr/creation.rs27
-rw-r--r--rola-cli/src/error.rs4
-rw-r--r--rola-cli/src/error/io.rs12
-rw-r--r--rola-cli/src/error/require_overwrite.rs19
-rw-r--r--rola-cli/src/error/space.rs64
-rw-r--r--rola-cli/src/lib.rs21
-rw-r--r--rola-cli/src/output/setup.rs16
-rw-r--r--rola-cli/src/res.rs2
-rw-r--r--rola-cli/src/res/bucket.rs25
-rw-r--r--rola-cli/src/res/overwrite.rs9
-rw-r--r--rola-utils/constants/src/bucket.rs13
-rw-r--r--rola-utils/space-system/Cargo.toml1
-rw-r--r--rola-utils/space-system/macros/src/space_root_test.rs8
-rw-r--r--rola-utils/space-system/src/space.rs369
-rw-r--r--rola-utils/space-system/src/space/error.rs11
33 files changed, 1444 insertions, 118 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4494a27..23750b8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -112,6 +112,46 @@ dependencies = [
]
[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -198,6 +238,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -238,6 +284,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
name = "jiff"
version = "0.2.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -308,26 +360,29 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "mingling"
version = "0.2.0"
-source = "git+https://github.com/mingling-rs/mingling.git?rev=002f3fd390f64b1d7632f8530a0db81d45edf6c2#002f3fd390f64b1d7632f8530a0db81d45edf6c2"
+source = "git+https://github.com/mingling-rs/mingling.git?rev=4be889ac2dc5263ce03bb014de24916bee2e9aa8#4be889ac2dc5263ce03bb014de24916bee2e9aa8"
dependencies = [
"mingling_core",
"mingling_macros",
+ "serde",
"size",
]
[[package]]
name = "mingling_core"
version = "0.2.0"
-source = "git+https://github.com/mingling-rs/mingling.git?rev=002f3fd390f64b1d7632f8530a0db81d45edf6c2#002f3fd390f64b1d7632f8530a0db81d45edf6c2"
+source = "git+https://github.com/mingling-rs/mingling.git?rev=4be889ac2dc5263ce03bb014de24916bee2e9aa8#4be889ac2dc5263ce03bb014de24916bee2e9aa8"
dependencies = [
"just_fmt",
"just_template",
+ "serde",
+ "serde_json",
]
[[package]]
name = "mingling_macros"
version = "0.2.0"
-source = "git+https://github.com/mingling-rs/mingling.git?rev=002f3fd390f64b1d7632f8530a0db81d45edf6c2#002f3fd390f64b1d7632f8530a0db81d45edf6c2"
+source = "git+https://github.com/mingling-rs/mingling.git?rev=4be889ac2dc5263ce03bb014de24916bee2e9aa8#4be889ac2dc5263ce03bb014de24916bee2e9aa8"
dependencies = [
"just_fmt",
"proc-macro2",
@@ -429,6 +484,7 @@ name = "rola-bucket"
version = "0.1.0"
dependencies = [
"log",
+ "serde",
"shared_constants",
"shared_functions",
"shared_macros",
@@ -442,11 +498,13 @@ name = "rola-cli"
version = "0.1.0"
dependencies = [
"chrono",
+ "clap",
"colored",
"env_logger",
"log",
"mingling",
"rorolala",
+ "serde",
"shakehand",
"shared_functions",
"shared_macros",
@@ -481,6 +539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
+ "serde_derive",
]
[[package]]
@@ -504,6 +563,19 @@ dependencies = [
]
[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -583,12 +655,19 @@ name = "space-system"
version = "0.1.0"
dependencies = [
"just_fmt",
+ "serde",
"space-macros",
"thiserror",
"tokio",
]
[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -814,3 +893,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
index baeab58..79be524 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,6 +40,10 @@ thiserror = "2.0.18"
just_fmt = "0.1.2"
log = "0.4.32"
+[workspace.dependencies.serde]
+version = "1.0.228"
+features = ["derive"]
+
[workspace.dependencies.tokio]
version = "1.52.3"
features = [
diff --git a/rola-bucket/Cargo.toml b/rola-bucket/Cargo.toml
index 61d7940..8899591 100644
--- a/rola-bucket/Cargo.toml
+++ b/rola-bucket/Cargo.toml
@@ -14,4 +14,5 @@ space-system.workspace = true
thiserror.workspace = true
tokio.workspace = true
+serde.workspace = true
log.workspace = true
diff --git a/rola-bucket/res/bucket.toml b/rola-bucket/res/bucket.toml
index 01abd07..adc7770 100644
--- a/rola-bucket/res/bucket.toml
+++ b/rola-bucket/res/bucket.toml
@@ -1 +1,4 @@
[bucket]
+# Bucket 类型
+ type = "client" # 本地存储,使用本地 ID
+# type = "bucket" # 中心存储,使用全局 ID
diff --git a/rola-bucket/src/bucket.rs b/rola-bucket/src/bucket.rs
index b70afd8..fe892f0 100644
--- a/rola-bucket/src/bucket.rs
+++ b/rola-bucket/src/bucket.rs
@@ -4,9 +4,11 @@ use crate::AsyncBucketTransferProtocol;
use crate::LocalFileSystemProtocol;
use space_system::SpaceRootTest;
-mod init;
-// pub use init::*;
+pub mod bind;
+pub mod config;
+pub mod init;
+mod idmap;
mod space;
/// Represents the state of a bucket in the transfer protocol.
diff --git a/rola-bucket/src/bucket/bind.rs b/rola-bucket/src/bucket/bind.rs
new file mode 100644
index 0000000..87f3382
--- /dev/null
+++ b/rola-bucket/src/bucket/bind.rs
@@ -0,0 +1,210 @@
+use serde::{Deserialize, Serialize};
+use shared_constants::bucket::PREFIX_BUCKET_BIND;
+use space_system::{Space, SpaceError};
+use std::borrow::Borrow;
+use std::cmp::Ordering;
+use std::fmt;
+use std::fs::ReadDir;
+use std::io::ErrorKind::NotFound;
+use std::ops::{Deref, DerefMut};
+
+use crate::{AsyncBucketTransferProtocol, Bucket};
+
+#[cfg(test)]
+mod test;
+
+/// Represents a binding between a bucket and a URL.
+///
+/// `BucketBind` is a newtype wrapper around a `String` that stores a URL
+/// associated with a bucket. It provides convenient access to the underlying
+/// URL string through `Deref`, `DerefMut`, `Borrow`, and `Display` trait
+/// implementations.
+#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone)]
+pub struct BucketBind {
+ /// The index of the bucket bind
+ index: u8,
+
+ /// The URL associated with the bucket bind.
+ url: String,
+}
+
+impl BucketBind {
+ /// Creates a new `BucketBind` with the given URL.
+ fn new(index: u8, url: impl Into<String>) -> Self {
+ Self {
+ index,
+ url: url.into(),
+ }
+ }
+
+ /// Returns the index of the bucket bind.
+ pub fn get_index(&self) -> u8 {
+ self.index
+ }
+
+ /// Returns a reference to the URL of the bucket bind.
+ pub fn get_url(&self) -> &str {
+ &self.url
+ }
+}
+
+/// Reads all bucket bind records from the space.
+///
+/// This function traverses the root directory of the specified space, filters files
+/// that start with a specific prefix (`PREFIX_BUCKET_BIND`), parses the index and URL
+/// from each binding record, and returns them as `BucketBind` objects.
+pub fn read_bucket_binds<Protocol: AsyncBucketTransferProtocol + Send + Sync>(
+ space: &Space<Bucket<Protocol>>,
+) -> Result<Vec<BucketBind>, SpaceError> {
+ // Fixed prefix for bucket bind filenames
+ const PREFIX: &str = PREFIX_BUCKET_BIND;
+
+ // Open a read stream for the space root directory
+ let reader: ReadDir = space.read_dir(".")?;
+ let mut binds = Vec::new();
+
+ // Loop through each entry in the directory
+ for entry in reader {
+ let entry = entry?;
+ let file_name = entry.file_name();
+ let name = file_name.to_string_lossy().to_string();
+
+ // Only process files starting with the bind prefix
+ if let Some(suffix) = name.strip_prefix(PREFIX) {
+ // Extract the part after the prefix as the index string
+ // Attempt to parse the suffix as a u8 index value
+ if let Ok(index) = suffix.parse::<u8>() {
+ // Read the file content as the URL
+ let content = space.read_to_string(&name)?;
+ let url = content.trim().to_string();
+
+ // Add the parsed binding record to the list
+ binds.push(BucketBind::new(index, url));
+ }
+ }
+ }
+
+ // Sort by index before returning
+ binds.sort();
+ Ok(binds)
+}
+
+/// Writes a bucket bind record to the space.
+///
+/// This function creates or updates a binding between a bucket and a URL
+/// at the specified index. It writes the URL content to a file named
+/// with the prefix `PREFIX_BUCKET_BIND` followed by the zero-padded index.
+pub fn write_bucket_bind<Protocol: AsyncBucketTransferProtocol + Send + Sync>(
+ space: &Space<Bucket<Protocol>>,
+ idx: u8,
+ url: &str,
+) -> Result<(), SpaceError> {
+ const PREFIX: &str = PREFIX_BUCKET_BIND;
+ let file_name = format!("{}{:03}", PREFIX, idx);
+ space.write(&file_name, url.trim())
+}
+
+/// Reads a single bucket bind record from the space by index.
+///
+/// This function looks for a file named with the prefix `PREFIX_BUCKET_BIND`
+/// followed by the zero-padded index, reads its content as a URL, and returns
+/// the corresponding `BucketBind`. Returns `None` if the file does not exist.
+pub fn read_bucket_bind<Protocol: AsyncBucketTransferProtocol + Send + Sync>(
+ space: &Space<Bucket<Protocol>>,
+ idx: u8,
+) -> Result<Option<BucketBind>, SpaceError> {
+ const PREFIX: &str = PREFIX_BUCKET_BIND;
+ let file_name = format!("{}{:03}", PREFIX, idx);
+
+ match space.read_to_string(&file_name) {
+ Ok(content) => {
+ let url = content.trim().to_string();
+ Ok(Some(BucketBind::new(idx, url)))
+ }
+ Err(SpaceError::Io(err)) => {
+ if err.kind() == NotFound {
+ Ok(None)
+ } else {
+ Err(SpaceError::Io(err))
+ }
+ }
+ Err(e) => Err(e),
+ }
+}
+
+/// Checks whether a bucket bind record exists at the given index.
+///
+/// Returns `true` if a file named with the prefix `PREFIX_BUCKET_BIND` followed
+/// by the zero-padded index exists in the space, `false` otherwise.
+pub fn check_bucket_bind_exists<Protocol: AsyncBucketTransferProtocol + Send + Sync>(
+ space: &Space<Bucket<Protocol>>,
+ idx: u8,
+) -> Result<bool, SpaceError> {
+ const PREFIX: &str = PREFIX_BUCKET_BIND;
+ let file_name = format!("{}{:03}", PREFIX, idx);
+
+ match space.read_to_string(&file_name) {
+ Ok(_) => Ok(true),
+ Err(SpaceError::Io(err)) => {
+ if err.kind() == NotFound {
+ Ok(false)
+ } else {
+ Err(SpaceError::Io(err))
+ }
+ }
+ Err(e) => Err(e),
+ }
+}
+
+/// Removes a bucket bind record from the space by index.
+///
+/// This function deletes the file named with the prefix `PREFIX_BUCKET_BIND`
+/// followed by the zero-padded index from the space. Returns `Ok(())` if the
+/// deletion succeeds, or an error if the operation fails (including if the
+/// file does not exist).
+pub fn remove_bucket_bind<Protocol: AsyncBucketTransferProtocol + Send + Sync>(
+ space: &Space<Bucket<Protocol>>,
+ idx: u8,
+) -> Result<(), SpaceError> {
+ const PREFIX: &str = PREFIX_BUCKET_BIND;
+ let file_name = format!("{}{:03}", PREFIX, idx);
+ space.remove_file(&file_name)
+}
+
+impl Ord for BucketBind {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.index.cmp(&other.index)
+ }
+}
+
+impl PartialOrd for BucketBind {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Deref for BucketBind {
+ type Target = String;
+
+ fn deref(&self) -> &Self::Target {
+ &self.url
+ }
+}
+
+impl DerefMut for BucketBind {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.url
+ }
+}
+
+impl Borrow<String> for BucketBind {
+ fn borrow(&self) -> &String {
+ &self.url
+ }
+}
+
+impl fmt::Display for BucketBind {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.url)
+ }
+}
diff --git a/rola-bucket/src/bucket/bind/test.rs b/rola-bucket/src/bucket/bind/test.rs
new file mode 100644
index 0000000..10126b1
--- /dev/null
+++ b/rola-bucket/src/bucket/bind/test.rs
@@ -0,0 +1,231 @@
+use std::{fs, path::Path};
+
+use shared_constants::bucket::FILE_BUCKET_BIND;
+use shared_functions::rola_test_sandbox;
+use space_system::Space;
+
+use crate::{
+ Bucket, NoProtocol,
+ bind::{
+ BucketBind, check_bucket_bind_exists, read_bucket_bind, read_bucket_binds,
+ remove_bucket_bind, write_bucket_bind,
+ },
+};
+
+fn init_bucket(path: &Path) -> Space<Bucket<NoProtocol>> {
+ let bucket = Bucket::<NoProtocol>::new_local();
+ let mut space = Space::new(bucket);
+ space.set_current_dir(&path).unwrap();
+ space.init(path).unwrap();
+ space
+}
+
+#[test]
+fn test_read_bucket_binds() {
+ let sandbox = rola_test_sandbox("bucket_bind_read");
+
+ let b = init_bucket(&sandbox.path);
+
+ let bind_1 = sandbox.join(FILE_BUCKET_BIND("1"));
+ let bind_2 = sandbox.join(FILE_BUCKET_BIND("2"));
+ let bind_3 = sandbox.join(FILE_BUCKET_BIND("3"));
+ let bind_fail = sandbox.join(FILE_BUCKET_BIND("@"));
+ let other = sandbox.join("ot");
+
+ fs::write(bind_1, "./bucket1").unwrap();
+ fs::write(bind_2, "\n./bucket2").unwrap();
+ fs::write(bind_3, "./bucket3\nbbb").unwrap();
+ fs::write(bind_fail, "omg").unwrap();
+ fs::write(other, "ok").unwrap();
+
+ let result = read_bucket_binds(&b).unwrap();
+ assert!(result.contains(&BucketBind::new(1, "./bucket1")));
+ assert!(result.contains(&BucketBind::new(2, "./bucket2")));
+ assert!(result.contains(&BucketBind::new(3, "./bucket3\nbbb")));
+
+ assert_eq!(result.len(), 3);
+}
+
+#[test]
+fn test_write_and_read_bucket_bind() {
+ let sandbox = rola_test_sandbox("bucket_bind_write_read");
+
+ let space = init_bucket(&sandbox.path);
+
+ // Write bucket bind records
+ write_bucket_bind(&space, 1, "./bucket1").unwrap();
+ write_bucket_bind(&space, 2, "./bucket2").unwrap();
+ write_bucket_bind(&space, 3, "./bucket3\nbbb").unwrap();
+
+ // Verify reads return the correct values
+ let bind1 = read_bucket_bind(&space, 1).unwrap().unwrap();
+ assert_eq!(bind1, BucketBind::new(1, "./bucket1"));
+
+ let bind2 = read_bucket_bind(&space, 2).unwrap().unwrap();
+ assert_eq!(bind2, BucketBind::new(2, "./bucket2"));
+
+ let bind3 = read_bucket_bind(&space, 3).unwrap().unwrap();
+ assert_eq!(bind3, BucketBind::new(3, "./bucket3\nbbb"));
+
+ // Read a non-existent bind should return None
+ let bind4 = read_bucket_bind(&space, 4).unwrap();
+ assert!(bind4.is_none());
+}
+
+#[test]
+fn test_write_bucket_bind_trims_whitespace() {
+ let sandbox = rola_test_sandbox("bucket_bind_trim");
+
+ let space = init_bucket(&sandbox.path);
+
+ // Write URL with surrounding whitespace
+ write_bucket_bind(&space, 1, " ./bucket1 ").unwrap();
+
+ // Verify it was trimmed on write
+ let bind = read_bucket_bind(&space, 1).unwrap().unwrap();
+ assert_eq!(bind, BucketBind::new(1, "./bucket1"));
+}
+
+#[test]
+fn test_write_bucket_bind_overwrites_existing() {
+ let sandbox = rola_test_sandbox("bucket_bind_overwrite");
+
+ let space = init_bucket(&sandbox.path);
+
+ write_bucket_bind(&space, 1, "./bucket_v1").unwrap();
+ write_bucket_bind(&space, 1, "./bucket_v2").unwrap();
+
+ let bind = read_bucket_bind(&space, 1).unwrap().unwrap();
+ assert_eq!(bind, BucketBind::new(1, "./bucket_v2"));
+}
+
+#[test]
+fn test_check_bucket_bind_exists() {
+ let sandbox = rola_test_sandbox("bucket_bind_check_exists");
+
+ let space = init_bucket(&sandbox.path);
+
+ // Initially, no bucket bind should exist
+ let exists = check_bucket_bind_exists(&space, 1).unwrap();
+ assert!(!exists);
+
+ // Write a bucket bind and verify it exists
+ write_bucket_bind(&space, 1, "./bucket1").unwrap();
+ let exists = check_bucket_bind_exists(&space, 1).unwrap();
+ assert!(exists);
+
+ // A different index should still not exist
+ let exists = check_bucket_bind_exists(&space, 2).unwrap();
+ assert!(!exists);
+
+ // Write another bind and check
+ write_bucket_bind(&space, 2, "./bucket2").unwrap();
+ let exists = check_bucket_bind_exists(&space, 2).unwrap();
+ assert!(exists);
+}
+
+#[test]
+fn test_check_bucket_bind_exists_after_delete() {
+ let sandbox = rola_test_sandbox("bucket_bind_check_exists_after_delete");
+
+ let space = init_bucket(&sandbox.path);
+
+ write_bucket_bind(&space, 1, "./bucket1").unwrap();
+ assert!(check_bucket_bind_exists(&space, 1).unwrap());
+
+ // Delete by removing the file directly
+ let bind_path = sandbox.path.join(format!(
+ "{}{:03}",
+ shared_constants::bucket::PREFIX_BUCKET_BIND,
+ 1
+ ));
+ std::fs::remove_file(bind_path).unwrap();
+
+ let exists = check_bucket_bind_exists(&space, 1).unwrap();
+ assert!(!exists);
+}
+
+#[test]
+fn test_remove_bucket_bind() {
+ let sandbox = rola_test_sandbox("bucket_bind_remove");
+
+ let space = init_bucket(&sandbox.path);
+
+ // Write a bucket bind
+ write_bucket_bind(&space, 1, "./bucket1").unwrap();
+ assert!(check_bucket_bind_exists(&space, 1).unwrap());
+
+ // Remove it
+ remove_bucket_bind(&space, 1).unwrap();
+ assert!(!check_bucket_bind_exists(&space, 1).unwrap());
+}
+
+#[test]
+fn test_remove_bucket_bind_nonexistent() {
+ let sandbox = rola_test_sandbox("bucket_bind_remove_nonexistent");
+
+ let space = init_bucket(&sandbox.path);
+
+ // Removing a non-existent bind should return an error
+ let result = remove_bucket_bind(&space, 99);
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_remove_bucket_bind_does_not_affect_others() {
+ let sandbox = rola_test_sandbox("bucket_bind_remove_others");
+
+ let space = init_bucket(&sandbox.path);
+
+ // Write multiple bucket binds
+ write_bucket_bind(&space, 1, "./bucket1").unwrap();
+ write_bucket_bind(&space, 2, "./bucket2").unwrap();
+ write_bucket_bind(&space, 3, "./bucket3").unwrap();
+
+ // Remove bind 2
+ remove_bucket_bind(&space, 2).unwrap();
+
+ // Bind 1 and 3 should still exist
+ assert!(check_bucket_bind_exists(&space, 1).unwrap());
+ assert!(!check_bucket_bind_exists(&space, 2).unwrap());
+ assert!(check_bucket_bind_exists(&space, 3).unwrap());
+
+ // Values should be preserved for remaining binds
+ assert_eq!(
+ read_bucket_bind(&space, 1).unwrap().unwrap(),
+ BucketBind::new(1, "./bucket1")
+ );
+ assert_eq!(
+ read_bucket_bind(&space, 3).unwrap().unwrap(),
+ BucketBind::new(3, "./bucket3")
+ );
+}
+
+#[test]
+fn test_remove_bucket_bind_then_read_returns_none() {
+ let sandbox = rola_test_sandbox("bucket_bind_remove_then_read");
+
+ let space = init_bucket(&sandbox.path);
+
+ write_bucket_bind(&space, 1, "./bucket1").unwrap();
+ remove_bucket_bind(&space, 1).unwrap();
+
+ let bind = read_bucket_bind(&space, 1).unwrap();
+ assert!(bind.is_none());
+}
+
+#[test]
+fn test_remove_bucket_bind_then_write_again() {
+ let sandbox = rola_test_sandbox("bucket_bind_remove_then_write");
+
+ let space = init_bucket(&sandbox.path);
+
+ write_bucket_bind(&space, 1, "./bucket_v1").unwrap();
+ remove_bucket_bind(&space, 1).unwrap();
+
+ // Write the same index again
+ write_bucket_bind(&space, 1, "./bucket_v2").unwrap();
+
+ let bind = read_bucket_bind(&space, 1).unwrap().unwrap();
+ assert_eq!(bind, BucketBind::new(1, "./bucket_v2"));
+}
diff --git a/rola-bucket/src/bucket/config.rs b/rola-bucket/src/bucket/config.rs
new file mode 100644
index 0000000..559db15
--- /dev/null
+++ b/rola-bucket/src/bucket/config.rs
@@ -0,0 +1,30 @@
+use serde::Deserialize;
+
+/// Configuration for a bucket.
+///
+/// This struct defines how a bucket should be configured, including its type.
+#[derive(Default, Deserialize)]
+pub struct BucketConfig {
+ /// The type of the bucket, e.g., client bucket or normal bucket.
+ ///
+ /// When deserializing from TOML, this is expected to be under the key `"type"`.
+ #[serde(rename = "type")]
+ pub bucket_type: BucketType,
+}
+
+/// Enum for bucket types, used to distinguish different types of buckets.
+///
+/// When deserializing, field names are mapped to string values in TOML via `serde(rename)`.
+#[derive(Default, Deserialize)]
+pub enum BucketType {
+ /// Client bucket
+ /// Uses local ID, mapped to remote ID via IDMAP
+ #[serde(rename = "client")]
+ ClientBucket,
+
+ /// Normal bucket
+ /// Uses global ID
+ #[default]
+ #[serde(rename = "bucket")]
+ Bucket,
+}
diff --git a/rola-bucket/src/bucket/idmap.rs b/rola-bucket/src/bucket/idmap.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/rola-bucket/src/bucket/idmap.rs
diff --git a/rola-bucket/src/bucket/init.rs b/rola-bucket/src/bucket/init.rs
index 30bf0f4..6834009 100644
--- a/rola-bucket/src/bucket/init.rs
+++ b/rola-bucket/src/bucket/init.rs
@@ -1,16 +1,18 @@
-use std::path::{Path, PathBuf};
+use std::{
+ fs,
+ path::{Path, PathBuf},
+};
use shared_constants::{
bucket::{
DIR_BUCKET_COMPRESSED_OBJ, DIR_BUCKET_DELTA, DIR_BUCKET_ID_REVS, DIR_BUCKET_ID_TAGS,
- DIR_BUCKET_OBJ,
+ DIR_BUCKET_IDMAP, DIR_BUCKET_OBJ,
},
common::FILE_BUCKET_ROOT_CONFIG,
};
use space_system::SpaceError;
-use tokio::fs;
-pub(crate) async fn init_bucket_at(path: PathBuf) -> Result<(), SpaceError> {
+pub(crate) fn init_bucket_at(path: PathBuf) -> Result<(), SpaceError> {
let bucket_config_file = path.join(FILE_BUCKET_ROOT_CONFIG);
// Check if directory is empty
@@ -19,32 +21,29 @@ pub(crate) async fn init_bucket_at(path: PathBuf) -> Result<(), SpaceError> {
return Err(SpaceError::RequireEmptyDirectory);
}
- write_config(&bucket_config_file).await?;
- create_dirs(&path).await?;
+ write_config(&bucket_config_file)?;
+ create_dirs(&path)?;
Ok(())
}
-async fn write_config(bucket_config_file: &Path) -> Result<(), SpaceError> {
- fs::write(bucket_config_file, include_str!("../../res/bucket.toml"))
- .await
- .map_err(SpaceError::Io)
+fn write_config(bucket_config_file: &Path) -> Result<(), SpaceError> {
+ fs::write(bucket_config_file, include_str!("../../res/bucket.toml")).map_err(SpaceError::Io)
}
-async fn create_dirs(bucket_dir: &Path) -> Result<(), SpaceError> {
+fn create_dirs(bucket_dir: &Path) -> Result<(), SpaceError> {
let dirs = [
DIR_BUCKET_OBJ,
DIR_BUCKET_COMPRESSED_OBJ,
DIR_BUCKET_DELTA,
DIR_BUCKET_ID_REVS,
DIR_BUCKET_ID_TAGS,
+ DIR_BUCKET_IDMAP,
];
for dir in dirs {
let full_path = bucket_dir.join(dir);
- fs::create_dir_all(&full_path)
- .await
- .map_err(SpaceError::Io)?;
+ fs::create_dir_all(&full_path).map_err(SpaceError::Io)?;
}
Ok(())
diff --git a/rola-bucket/src/bucket/space.rs b/rola-bucket/src/bucket/space.rs
index ed1311c..353075d 100644
--- a/rola-bucket/src/bucket/space.rs
+++ b/rola-bucket/src/bucket/space.rs
@@ -10,10 +10,10 @@ impl<Protocol: AsyncBucketTransferProtocol + Send + Sync> SpaceRoot for Bucket<P
SpaceRootFindPattern::IncludeFile(FILE_BUCKET_ROOT_CONFIG.into())
}
- async fn create_space(path: &std::path::Path) -> Result<(), space_system::SpaceError> {
+ fn create_space(path: &std::path::Path) -> Result<(), space_system::SpaceError> {
let path_str = path.display().to_string();
trace!("Creating bucket at: {}", &path_str);
- init_bucket_at(path.into()).await?;
+ init_bucket_at(path.into())?;
trace!("Bucket created at: {}", &path_str);
Ok(())
}
diff --git a/rola-cli/Cargo.toml b/rola-cli/Cargo.toml
index 0ed9a3b..c9cf562 100644
--- a/rola-cli/Cargo.toml
+++ b/rola-cli/Cargo.toml
@@ -13,6 +13,7 @@ shared_macros.workspace = true
space-system.workspace = true
+serde.workspace = true
tokio.workspace = true
colored = "3.1.1"
@@ -20,20 +21,23 @@ chrono = "0.4.45"
env_logger = "0.11.10"
log = "0.4.32"
shakehand = "0.1.3"
+clap = { version = "4.6.1", features = ["derive"] }
[dependencies.mingling]
git = "https://github.com/mingling-rs/mingling.git"
-rev = "002f3fd390f64b1d7632f8530a0db81d45edf6c2"
+rev = "4be889ac2dc5263ce03bb014de24916bee2e9aa8"
features = [
"parser",
"extra_macros",
"dispatch_tree",
- "comp"
+ "comp",
+ "clap",
+ "general_renderer"
]
[build-dependencies.mingling]
git = "https://github.com/mingling-rs/mingling.git"
-rev = "002f3fd390f64b1d7632f8530a0db81d45edf6c2"
+rev = "4be889ac2dc5263ce03bb014de24916bee2e9aa8"
features = [
"builds",
"comp"
diff --git a/rola-cli/locales/errors/common.toml b/rola-cli/locales/errors/common.toml
new file mode 100644
index 0000000..8c0acf8
--- /dev/null
+++ b/rola-cli/locales/errors/common.toml
@@ -0,0 +1,25 @@
+[en]
+command_not_found = """
+[[b_red]]**Error:**[[/]] Command not found: \"%{command}\"!
+
+[[b_yellow]]**Hint:**[[/]] You can use `rola -h` to query help.
+"""
+
+require_overwrite = """
+[[b_red]]**Operation blocked:**[[/]] This operation would overwrite an existing resource
+
+[[b_yellow]]**Hint:**[[/]] Explicitly specify [[b_red]]**--overwrite**[[/]]!
+"""
+
+[zh_CN]
+command_not_found = """
+[[b_red]]**错误:**[[/]]未找到命令 \"%{command}\"!
+
+[[b_yellow]]**提示:**[[/]]您可以使用 `rola -h` 来查询帮助。
+"""
+
+require_overwrite = """
+[[b_red]]**操作已阻止:**[[/]]该操作将会覆写已有资源
+
+[[b_yellow]]**提示:**[[/]]请显式指定 [[b_red]]**--overwrite**[[/]]!
+"""
diff --git a/rola-cli/locales/errors/i18n_space_error.toml b/rola-cli/locales/errors/i18n_space_error.toml
new file mode 100644
index 0000000..48c5aa1
--- /dev/null
+++ b/rola-cli/locales/errors/i18n_space_error.toml
@@ -0,0 +1,23 @@
+[en]
+space_not_found = "Space not found"
+path_format = "Path format error: {info}"
+require_empty_directory = "The specified directory is not empty"
+config_file_already_exist = "Configuration file already exists"
+io_error_name = "Space IO error"
+io_not_found = "{info}"
+io_permission_denied = "{info}"
+io_already_exists = "{info}"
+io_invalid_input = "{info}"
+io_other = "{info}"
+
+[zh_CN]
+space_not_found = "未找到空间"
+path_format = "路径格式错误: {info}"
+require_empty_directory = "指定目录不为空"
+config_file_already_exist = "配置文件已存在"
+io_error_name = "空间 IO 错误"
+io_not_found = "{info}"
+io_permission_denied = "{info}"
+io_already_exists = "{info}"
+io_invalid_input = "{info}"
+io_other = "{info}"
diff --git a/rola-cli/locales/i18n_bucket_manager.toml b/rola-cli/locales/i18n_bucket_manager.toml
index 09fae75..0e4f3c1 100644
--- a/rola-cli/locales/i18n_bucket_manager.toml
+++ b/rola-cli/locales/i18n_bucket_manager.toml
@@ -13,6 +13,14 @@ error_bucket_path_not_directory = "The path \"%{path}\" is not a directory!"
error_bucket_nested = "Cannot create a nested Bucket directory inside an existing Bucket space"
+error_bind_index_not_provided = """
+[[b_red]]**Error:**[[/]] Bucket bind index not provided
+"""
+
+error_bind_index_not_found = """
+[[b_red]]**Error:**[[/]] Bucket bind index not found: %{index}
+"""
+
[zh_CN]
created = "桶已被创建于 \"%{path}\""
@@ -27,3 +35,11 @@ error_bucket_path_not_provided = """
error_bucket_path_not_directory = "路径 \"%{path}\" 不是一个目录!"
error_bucket_nested = "无法在已有的桶内创建嵌套的桶目录"
+
+error_bind_index_not_provided = """
+[[b_red]]**错误:**[[/]]未提供桶绑定
+"""
+
+error_bind_index_not_found = """
+[[b_red]]**错误:**[[/]]未找到桶绑定:%{index}
+"""
diff --git a/rola-cli/src/bin/rola.rs b/rola-cli/src/bin/rola.rs
index a201953..3e97468 100644
--- a/rola-cli/src/bin/rola.rs
+++ b/rola-cli/src/bin/rola.rs
@@ -1,13 +1,14 @@
use std::{env::current_dir, process::exit};
use mingling::{
- Program,
+ LazyInit, Program,
macros::program_setup,
- setup::{ExitCodeSetup, HelpFlagSetup, QuietFlagSetup},
+ setup::{ExitCodeSetup, GeneralRendererSetup, HelpFlagSetup, QuietFlagSetup},
};
use rola_cli::{
- ThisProgram, locale, output::ColorOutputSetup, output::EnvLoggerSetup,
- res::current_dir::ResCurrentDir,
+ ThisProgram, locale,
+ output::{ColorOutputSetup, EnvLoggerSetup},
+ res::{bucket::ResBucketWithoutProtocol, current_dir::ResCurrentDir, overwrite::ResOverwrite},
};
fn main() {
@@ -31,7 +32,14 @@ fn main() {
cwd: current_dir().unwrap(),
});
+ let overwrite = program.pick_global_flag("--overwrite");
+ program.with_resource(ResOverwrite { overwrite });
+
+ // LazyResources
+ program.with_resource(ResBucketWithoutProtocol::lazy_default());
+
// Setup
+ program.with_setup(GeneralRendererSetup);
program.with_setup(HelpFlagSetup::new(["-h", "--help"]));
program.with_setup(StandardOutputSetup);
program.with_setup(ExitCodeSetup::default());
diff --git a/rola-cli/src/bucket_mgr.rs b/rola-cli/src/bucket_mgr.rs
index 9aa8847..82a76d4 100644
--- a/rola-cli/src/bucket_mgr.rs
+++ b/rola-cli/src/bucket_mgr.rs
@@ -1,2 +1,4 @@
mod creation;
pub use creation::*;
+mod bind;
+pub use bind::*;
diff --git a/rola-cli/src/bucket_mgr/bind.rs b/rola-cli/src/bucket_mgr/bind.rs
new file mode 100644
index 0000000..48186a2
--- /dev/null
+++ b/rola-cli/src/bucket_mgr/bind.rs
@@ -0,0 +1,262 @@
+use colored::Colorize;
+use mingling::{
+ Groupped, LazyRes,
+ macros::{chain, dispatcher_clap, pack, pack_err, r_println, renderer},
+ res::ResExitCode,
+};
+use rorolala::bucket::{self, bind::BucketBind};
+use serde::Serialize;
+use shared_functions::info;
+
+use crate::{
+ Next,
+ bucket_mgr::ResultEnumBucketOperation::Set,
+ error::{ErrorRequireOverwrite, ErrorSpace},
+ locale::I18nBucketManager,
+ output::display::markdown,
+ res::{bucket::ResBucketWithoutProtocol, overwrite::ResOverwrite},
+};
+
+pub const EC_BUCKET_BIND_INDEX_NOT_PROVIDED: i32 = 251;
+pub const EC_BUCKET_BIND_INDEX_NOT_FOUND: i32 = 252;
+
+#[derive(Default, clap::Parser, Serialize, Groupped)]
+#[dispatcher_clap("bucket.bind", CMDBucketBind)]
+pub struct EntryBucketBind {
+ index: Option<u8>,
+
+ #[arg(long, conflicts_with_all = ["set"])]
+ remove: bool,
+
+ #[arg(long, conflicts_with_all = ["remove"])]
+ set: Option<String>,
+}
+
+pack!(StateBucketBindListAll = ());
+pack!(StateBucketBindListSpecified = u8);
+pack!(StateBucketBindRemove = u8);
+pack!(StateBucketBindSet = (u8, String)); // idx, url
+
+#[derive(Default, Serialize, Groupped)]
+pub struct ResultAllBucketBind {
+ bind_list: Vec<BucketBind>,
+}
+
+#[derive(Default, Serialize, Groupped)]
+pub struct ResultOneBucketBind {
+ index: u8,
+ url: String,
+}
+
+#[derive(Default, Serialize, Groupped)]
+pub struct ResultBucketOperated {
+ pub operation: ResultEnumBucketOperation,
+ pub index: u8,
+ pub url: Option<String>,
+}
+
+#[derive(Default, Serialize)]
+pub enum ResultEnumBucketOperation {
+ #[serde(rename = "none")]
+ #[default]
+ None,
+ #[serde(rename = "set_bind")]
+ Set,
+ #[serde(rename = "remove_bind")]
+ Remove,
+}
+
+pack_err!(ErrorBucketBindIndexNotProvided);
+pack_err!(ErrorBucketBindIndexNotFound = u8);
+
+// Chains
+
+#[chain]
+pub fn handle_bucket_bind(args: EntryBucketBind) -> Next {
+ match args.index {
+ Some(op_idx) => {
+ if let Some(url) = args.set {
+ // bind idx --set url
+ return StateBucketBindSet::new((op_idx, url)).to_chain();
+ } else if args.remove {
+ // bind idx --remove
+ return StateBucketBindRemove::new(op_idx).to_chain();
+ } else {
+ // bind idx
+ return StateBucketBindListSpecified::new(op_idx).to_chain();
+ }
+ }
+ None => {
+ if !args.remove && args.set.is_none() {
+ // bind
+ return StateBucketBindListAll::new(()).to_chain();
+ } else {
+ // index not provided
+ // bind --set url
+ // or
+ // bind --remove
+ return ErrorBucketBindIndexNotProvided::default().to_render();
+ }
+ }
+ }
+}
+
+#[chain]
+pub fn handle_state_bucket_bind_set(
+ set: StateBucketBindSet,
+ bucket: &mut LazyRes<ResBucketWithoutProtocol>,
+ overwrite: &ResOverwrite,
+) -> Next {
+ let (index, url) = set.inner;
+ let bucket_space_ref = &bucket.get_ref().space;
+
+ let exist = match bucket::bind::check_bucket_bind_exists(bucket_space_ref, index) {
+ Ok(exists) => exists,
+ Err(e) => return ErrorSpace::from(e).to_chain(),
+ };
+
+ // When the bind already exists but overwrite is not specified
+ // Dispatch to ErrorRequireOverwrite
+ if exist && !overwrite.overwrite {
+ return ErrorRequireOverwrite::default().to_render();
+ }
+
+ if let Err(space_error) =
+ bucket::bind::write_bucket_bind(bucket_space_ref, index, url.clone().as_str())
+ {
+ ErrorSpace::from(space_error).to_chain();
+ }
+
+ ResultBucketOperated {
+ operation: Set,
+ index,
+ url: Some(url),
+ }
+ .to_render()
+}
+
+#[chain]
+pub fn handle_state_bucket_bind_remove(
+ remove: StateBucketBindRemove,
+ bucket: &mut LazyRes<ResBucketWithoutProtocol>,
+) -> Next {
+ let index = remove.inner;
+ let bucket_space_ref = &bucket.get_ref().space;
+
+ if let Err(space_error) = bucket::bind::remove_bucket_bind(bucket_space_ref, index) {
+ return ErrorSpace::from(space_error).to_chain();
+ }
+
+ ResultBucketOperated {
+ operation: ResultEnumBucketOperation::Remove,
+ index,
+ url: None,
+ }
+ .to_render()
+}
+
+#[chain]
+pub fn handle_state_bucket_bind_list_specified(
+ index: StateBucketBindListSpecified,
+ bucket: &mut LazyRes<ResBucketWithoutProtocol>,
+) -> Next {
+ let index = index.inner;
+ let bucket_space_ref = &bucket.get_ref().space;
+ let bind_info = match bucket::bind::read_bucket_bind(bucket_space_ref, index) {
+ Err(e) => return ErrorSpace::from(e).to_chain(),
+ Ok(None) => return ErrorBucketBindIndexNotFound::new(index).to_render(),
+ Ok(Some(r)) => r,
+ };
+
+ ResultOneBucketBind {
+ index: bind_info.get_index(),
+ url: bind_info.get_url().to_string(),
+ }
+ .to_render()
+}
+
+#[chain]
+pub fn handle_state_bucket_bind_list_all(
+ _p: StateBucketBindListAll,
+ bucket: &mut LazyRes<ResBucketWithoutProtocol>,
+) -> Next {
+ let bucket_space_ref = &bucket.get_ref().space;
+ info!("Reading all bucket binds from space");
+ let bind_list = match bucket::bind::read_bucket_binds(bucket_space_ref) {
+ Err(e) => return ErrorSpace::from(e).to_chain(),
+ Ok(r) => r,
+ };
+ info!("Read {} bucket binds", bind_list.len());
+ ResultAllBucketBind { bind_list }.to_render()
+}
+
+// Result Renderers
+
+#[renderer]
+pub fn render_result_one_bucket_bind(result: ResultOneBucketBind) {
+ r_println!("BIND_{} => \"{}\"", result.index, result.url.trim().bold());
+}
+
+#[renderer]
+pub fn render_result_all_bucket_bind(result: ResultAllBucketBind) {
+ let list: Vec<String> = result
+ .bind_list
+ .iter()
+ .map(|b| {
+ format!(
+ "BIND_{} => \"{}\"",
+ b.get_index(),
+ b.get_url().trim().bold()
+ )
+ })
+ .collect();
+ for item in list {
+ r_println!("{}", item);
+ }
+}
+
+#[renderer]
+pub fn render_result_bucket_operated(result: ResultBucketOperated) {
+ let index = result.index;
+ let url = result.url.unwrap_or("".to_string());
+ match result.operation {
+ ResultEnumBucketOperation::Set => {
+ r_println!(
+ "{} BIND_{} => \"{}\"",
+ "+++".bold().bright_green(),
+ index,
+ url.bold().trim()
+ )
+ }
+ ResultEnumBucketOperation::Remove => {
+ r_println!("{} BIND_{}", "---".bold().bright_red(), index)
+ }
+ _ => {}
+ }
+}
+
+// Error Renderers
+
+#[renderer]
+pub fn render_error_bucket_bind_index_not_provided(
+ _err: ErrorBucketBindIndexNotProvided,
+ ec: &mut ResExitCode,
+) {
+ r_println!(
+ "{}",
+ markdown(I18nBucketManager::error_bind_index_not_provided().trim())
+ );
+ ec.exit_code = EC_BUCKET_BIND_INDEX_NOT_PROVIDED;
+}
+
+#[renderer]
+pub fn render_error_bucket_bind_index_not_found(
+ err: ErrorBucketBindIndexNotFound,
+ ec: &mut ResExitCode,
+) {
+ r_println!(
+ "{}",
+ markdown(I18nBucketManager::error_bind_index_not_found(err.info.to_string()).trim())
+ );
+ ec.exit_code = EC_BUCKET_BIND_INDEX_NOT_FOUND;
+}
diff --git a/rola-cli/src/bucket_mgr/creation.rs b/rola-cli/src/bucket_mgr/creation.rs
index c363773..2394228 100644
--- a/rola-cli/src/bucket_mgr/creation.rs
+++ b/rola-cli/src/bucket_mgr/creation.rs
@@ -1,16 +1,16 @@
use std::{fs::create_dir_all, path::PathBuf};
use mingling::{
- macros::{chain, dispatcher, pack, r_println, renderer, route},
+ Groupped,
+ macros::{chain, dispatcher, pack, pack_err, r_println, renderer, route},
parser::AsPicker,
res::ResExitCode,
};
use rorolala::bucket::{Bucket, NoProtocol};
+use serde::Serialize;
use space_system::{Space, SpaceError, SpaceRoot, find_space_root_with};
-use crate::{
- Next, error::ErrorIo, locale::I18nBucketManager, res::current_dir::ResCurrentDir, tkr,
-};
+use crate::{Next, error::ErrorIo, locale::I18nBucketManager, res::current_dir::ResCurrentDir};
pub const EC_BUCKET_CREATE_DIR_NOT_EMPTY: i32 = 2400;
pub const EC_BUCKET_PATH_NOT_PROVIDED: i32 = 2401;
@@ -23,12 +23,15 @@ dispatcher!("bucket.create");
pack!(StateBucketCreationPrecheck = PathBuf);
pack!(StateBucketCreation = PathBuf);
-pack!(ResultBucketCreated = PathBuf);
+#[derive(Debug, Groupped, Serialize)]
+pub struct ResultBucketCreated {
+ bucket_path: PathBuf,
+}
-pack!(ErrorDirectoryNotEmpty = PathBuf);
-pack!(ErrorBucketPathNotProvided = ());
-pack!(ErrorBucketPathNotDirectory = PathBuf);
-pack!(ErrorBucketPathNested = PathBuf);
+pack_err!(ErrorDirectoryNotEmpty = PathBuf);
+pack_err!(ErrorBucketPathNotProvided = ());
+pack_err!(ErrorBucketPathNotDirectory = PathBuf);
+pack_err!(ErrorBucketPathNested = PathBuf);
#[chain]
pub fn handle_bucket_init(_args: EntryBucketInit, cwd: &mut ResCurrentDir) -> Next {
@@ -86,16 +89,16 @@ pub fn handle_state_bucket_creation(create: StateBucketCreation) -> Next {
// Initialize the Space and capture any SpaceError::Io
let path_to_init = path.clone();
- if let Err(SpaceError::Io(error)) = tkr! { bucket_space.init(path_to_init).await } {
+ if let Err(SpaceError::Io(error)) = bucket_space.init(path_to_init) {
return ErrorIo::from(error).to_render();
}
- ResultBucketCreated::new(path).to_render()
+ ResultBucketCreated { bucket_path: path }.to_render()
}
#[renderer]
pub fn render_result_bucket_created(result: ResultBucketCreated) {
- let path = result.inner.to_string_lossy();
+ let path = result.bucket_path.to_string_lossy();
r_println!("{}", I18nBucketManager::created(path));
}
diff --git a/rola-cli/src/error.rs b/rola-cli/src/error.rs
index 608d4e1..15bdc70 100644
--- a/rola-cli/src/error.rs
+++ b/rola-cli/src/error.rs
@@ -1,2 +1,6 @@
mod io;
pub use io::*;
+mod space;
+pub use space::*;
+mod require_overwrite;
+pub use require_overwrite::*;
diff --git a/rola-cli/src/error/io.rs b/rola-cli/src/error/io.rs
index d65b765..7f31824 100644
--- a/rola-cli/src/error/io.rs
+++ b/rola-cli/src/error/io.rs
@@ -3,6 +3,7 @@ use mingling::{
macros::{r_println, renderer},
res::ResExitCode,
};
+use serde::Serialize;
use crate::locale::errors::I18nIoError;
@@ -46,7 +47,7 @@ pub const EC_IOERR_UNEXPECTED_EOF: i32 = 2536;
pub const EC_IOERR_OUT_OF_MEMORY: i32 = 2537;
pub const EC_IOERR_OTHER: i32 = 2538;
-#[derive(Default, Groupped)]
+#[derive(Default, Serialize, Groupped)]
pub enum ErrorIo {
#[default]
/// DONT USE IT: This variant is only used to provide a Default derive for ErrorIo
@@ -54,7 +55,14 @@ pub enum ErrorIo {
/// In normal creation flow, you should directly use ErrorIo::from(/* std::io::Error */)
DontUse,
- Error(std::io::Error),
+ Error(#[serde(serialize_with = "serialize_io_error")] std::io::Error),
+}
+
+fn serialize_io_error<S: serde::Serializer>(
+ err: &std::io::Error,
+ serializer: S,
+) -> Result<S::Ok, S::Error> {
+ serializer.serialize_str(&format!("{:?}", err))
}
#[renderer]
diff --git a/rola-cli/src/error/require_overwrite.rs b/rola-cli/src/error/require_overwrite.rs
new file mode 100644
index 0000000..c84dcb1
--- /dev/null
+++ b/rola-cli/src/error/require_overwrite.rs
@@ -0,0 +1,19 @@
+use mingling::{
+ macros::{pack_err, r_println, renderer},
+ res::ResExitCode,
+};
+
+use crate::{locale, output::display::markdown};
+
+pub const EC_REQUIRE_OVERWRITE: i32 = 1001;
+
+pack_err!(ErrorRequireOverwrite);
+
+#[renderer]
+pub fn render_error_require_overwrite(_err: ErrorRequireOverwrite, ec: &mut ResExitCode) {
+ r_println!(
+ "{}",
+ markdown(locale::errors::Common::require_overwrite().trim())
+ );
+ ec.exit_code = EC_REQUIRE_OVERWRITE;
+}
diff --git a/rola-cli/src/error/space.rs b/rola-cli/src/error/space.rs
new file mode 100644
index 0000000..fb0a560
--- /dev/null
+++ b/rola-cli/src/error/space.rs
@@ -0,0 +1,64 @@
+use mingling::{
+ Groupped,
+ macros::{chain, r_println, renderer},
+ res::ResExitCode,
+};
+use serde::Serialize;
+use space_system::SpaceError;
+
+use crate::{Next, error::ErrorIo, locale::errors::I18nSpaceError};
+
+pub const EC_SPACE_NOT_FOUND: i32 = 2600;
+pub const EC_SPACE_PATH_FORMAT: i32 = 2601;
+pub const EC_SPACE_REQUIRE_EMPTY_DIR: i32 = 2602;
+pub const EC_SPACE_CONFIG_ALREADY_EXIST: i32 = 2603;
+
+#[derive(Serialize, Groupped)]
+pub struct ErrorSpace {
+ pub error: SpaceError,
+}
+
+#[chain]
+pub fn handle_error_space(err: ErrorSpace) -> Next {
+ match err.error {
+ // Forward to ErrorIo
+ SpaceError::Io(error) => ErrorIo::from(error).to_render(),
+
+ _ => err.to_render(),
+ }
+}
+
+#[renderer]
+pub fn render_error_space(err: ErrorSpace, ec: &mut ResExitCode) {
+ match &err.error {
+ SpaceError::SpaceNotFound => {
+ r_println!("{}", I18nSpaceError::space_not_found().trim());
+ ec.exit_code = EC_SPACE_NOT_FOUND;
+ }
+ SpaceError::PathFormatError(_) => {
+ r_println!("{}", I18nSpaceError::path_format().trim());
+ ec.exit_code = EC_SPACE_PATH_FORMAT;
+ }
+ SpaceError::RequireEmptyDirectory => {
+ r_println!("{}", I18nSpaceError::require_empty_directory().trim());
+ ec.exit_code = EC_SPACE_REQUIRE_EMPTY_DIR;
+ }
+ SpaceError::ConfigFileAlreadyExist => {
+ r_println!("{}", I18nSpaceError::config_file_already_exist().trim());
+ ec.exit_code = EC_SPACE_CONFIG_ALREADY_EXIST;
+ }
+ SpaceError::Io(_) => {
+ // Forwarded to ErrorIo via handle_error_space chain
+ }
+ SpaceError::Other(_) => {
+ r_println!("{}", I18nSpaceError::space_not_found().trim());
+ ec.exit_code = EC_SPACE_NOT_FOUND;
+ }
+ }
+}
+
+impl From<SpaceError> for ErrorSpace {
+ fn from(error: SpaceError) -> Self {
+ Self { error }
+ }
+}
diff --git a/rola-cli/src/lib.rs b/rola-cli/src/lib.rs
index b3587e2..b472169 100644
--- a/rola-cli/src/lib.rs
+++ b/rola-cli/src/lib.rs
@@ -1,6 +1,9 @@
use std::process::exit;
-use mingling::macros::{gen_program, help};
+use mingling::{
+ macros::{gen_program, help, r_println, renderer},
+ res::ResExitCode,
+};
pub mod output;
pub mod res;
@@ -14,8 +17,10 @@ use error::*;
use crate::output::display::markdown;
+pub const EC_COMMAND_NOT_FOUND: i32 = 1000;
+
#[help]
-fn handle_error_dispatch_not_found(_err: ErrorDispatcherNotFound) {
+fn help_global(_err: ErrorDispatcherNotFound) {
let help = locale::helps::Basic::help().trim();
// Print directly to stderr and exit with code 0
@@ -23,6 +28,18 @@ fn handle_error_dispatch_not_found(_err: ErrorDispatcherNotFound) {
exit(0)
}
+#[renderer]
+fn render_error_dispatch_not_found(err: ErrorDispatcherNotFound, ec: &mut ResExitCode) {
+ if !err.inner.is_empty() {
+ let cmd_str = err.inner.join(" ");
+ r_println!(
+ "{}",
+ markdown(locale::errors::Common::command_not_found(cmd_str).trim())
+ );
+ ec.exit_code = EC_COMMAND_NOT_FOUND;
+ }
+}
+
gen_program!();
pub mod locale {
diff --git a/rola-cli/src/output/setup.rs b/rola-cli/src/output/setup.rs
index 824348b..880b236 100644
--- a/rola-cli/src/output/setup.rs
+++ b/rola-cli/src/output/setup.rs
@@ -1,4 +1,4 @@
-use mingling::{Program, macros::program_setup};
+use mingling::{Program, hook::ProgramHook, macros::program_setup};
use shared_functions::info;
use crate::{
@@ -35,6 +35,20 @@ pub fn env_logger_setup(program: &mut Program<ThisProgram>) {
_ => log::Level::Info,
},
});
+
+ // Add Hook
+ program.with_hook(
+ ProgramHook::<ThisProgram>::empty()
+ .on_begin(|| info!("[INFO] Program is begin"))
+ .on_pre_dispatch(|args| info!("[INFO] Pre dispatch: {args:?}"))
+ .on_post_dispatch(|c: &_| info!("[INFO] Post dispatch: {c:?}"))
+ .on_pre_chain(|c: &_, _| {
+ info!("[INFO] Pre chain: {c}");
+ })
+ .on_post_chain(|any_output| info!("[INFO] Post chain: {}", any_output.member_id))
+ .on_pre_render(|c: &_, _| info!("[INFO] Pre render: {c}"))
+ .on_post_render(|_| info!("[INFO] Post render")),
+ );
}
info!("Verbose mode enabled!");
diff --git a/rola-cli/src/res.rs b/rola-cli/src/res.rs
index 2ff9b75..f85a889 100644
--- a/rola-cli/src/res.rs
+++ b/rola-cli/src/res.rs
@@ -1 +1,3 @@
+pub mod bucket;
pub mod current_dir;
+pub mod overwrite;
diff --git a/rola-cli/src/res/bucket.rs b/rola-cli/src/res/bucket.rs
new file mode 100644
index 0000000..16b8fc9
--- /dev/null
+++ b/rola-cli/src/res/bucket.rs
@@ -0,0 +1,25 @@
+use std::env::current_dir;
+
+use rorolala::bucket::{Bucket, NoProtocol};
+use space_system::Space;
+
+/// A resource holding a local filesystem bucket without a protocol.
+///
+/// This struct wraps a [`Space<Bucket<NoProtocol>>`] that provides access to a
+/// local filesystem bucket. It automatically initializes the bucket's current
+/// directory from the [`ResCurrentDir`] resource injected into [`ThisProgram`].
+#[derive(Clone)]
+pub struct ResBucketWithoutProtocol {
+ /// The space containing the protocol-less local bucket.
+ pub space: Space<Bucket<NoProtocol>>,
+}
+
+impl Default for ResBucketWithoutProtocol {
+ fn default() -> Self {
+ let current_dir = current_dir().unwrap();
+ let mut space = Space::new(Bucket::<NoProtocol>::new_local());
+ space.set_current_dir(current_dir).unwrap();
+
+ Self { space }
+ }
+}
diff --git a/rola-cli/src/res/overwrite.rs b/rola-cli/src/res/overwrite.rs
new file mode 100644
index 0000000..cef0932
--- /dev/null
+++ b/rola-cli/src/res/overwrite.rs
@@ -0,0 +1,9 @@
+/// A flag indicating whether to overwrite existing resources.
+///
+/// This struct encapsulates a boolean value that indicates whether to overwrite
+/// existing files or data during resource processing.
+#[derive(Debug, Default, Clone)]
+pub struct ResOverwrite {
+ /// Boolean flag, `true` means overwrite is allowed, `false` means it is not.
+ pub overwrite: bool,
+}
diff --git a/rola-utils/constants/src/bucket.rs b/rola-utils/constants/src/bucket.rs
index e2ebff2..b195b93 100644
--- a/rola-utils/constants/src/bucket.rs
+++ b/rola-utils/constants/src/bucket.rs
@@ -15,6 +15,9 @@ mod consts {
/// Tag storage directory, used to record tags for easy file location
pub const DIR_BUCKET_ID_TAGS: &str = "./tags/";
+ /// ID mapping table, used to map local IDs to remote IDs (client-only)
+ pub const DIR_BUCKET_IDMAP: &str = "./map/";
+
/// Full object file path template
pub const FILE_BUCKET_OBJ: &str = "./objects/{slice1}/{slice2}/{fullname}";
@@ -33,6 +36,16 @@ mod consts {
/// Tag file, internally points to a file_id
pub const FILE_BUCKET_ID_TAG: &str = "./tags/{tag_name}";
+
+ /// ID mapping chunk, used to map local IDs to remote IDs (client-only)
+ /// 26635 bytes per chunk
+ pub const FILE_BUCKET_IDMAP: &str = "./map/{chunk_num}";
+
+ /// Remote BUCKET binding (client-only)
+ pub const FILE_BUCKET_BIND: &str = "./BIND_{bind_id}";
+
+ /// Prefix of remote BUCKET binding file (client-only)
+ pub const PREFIX_BUCKET_BIND: &str = "BIND_";
}
pub use consts::*;
diff --git a/rola-utils/space-system/Cargo.toml b/rola-utils/space-system/Cargo.toml
index 5322cbf..1173ccb 100644
--- a/rola-utils/space-system/Cargo.toml
+++ b/rola-utils/space-system/Cargo.toml
@@ -10,3 +10,4 @@ space-macros.workspace = true
thiserror.workspace = true
just_fmt.workspace = true
tokio.workspace = true
+serde.workspace = true
diff --git a/rola-utils/space-system/macros/src/space_root_test.rs b/rola-utils/space-system/macros/src/space_root_test.rs
index 71c48c0..0c15e39 100644
--- a/rola-utils/space-system/macros/src/space_root_test.rs
+++ b/rola-utils/space-system/macros/src/space_root_test.rs
@@ -64,8 +64,8 @@ pub(crate) fn internal_space_root_test_derive(input: TokenStream) -> TokenStream
use space_system::{Space, SpaceRoot, SpaceRootFindPattern};
use std::env::set_current_dir;
- #[tokio::test]
- async fn test_create_space() {
+ #[test]
+ fn test_create_space() {
let sandbox = rola_test_sandbox(stringify!(#name));
set_current_dir(&*sandbox).unwrap();
@@ -84,7 +84,7 @@ pub(crate) fn internal_space_root_test_derive(input: TokenStream) -> TokenStream
println!("Checking if {} absolute directory does not exist before initialization", stringify!(#name));
assert!(!dir.exists());
- space.init_here().await.unwrap();
+ space.init_here().unwrap();
println!("Checking if {} absolute directory exists after initialization", stringify!(#name));
assert!(dir.exists());
@@ -98,7 +98,7 @@ pub(crate) fn internal_space_root_test_derive(input: TokenStream) -> TokenStream
println!("Checking if {} does not exist before initialization", stringify!(#name));
assert!(space.space_dir_current().is_err());
- space.init_here().await.unwrap();
+ space.init_here().unwrap();
println!("Checking if {} exists after initialization", stringify!(#name));
assert!(space.space_dir_current().is_ok());
diff --git a/rola-utils/space-system/src/space.rs b/rola-utils/space-system/src/space.rs
index 3fe3507..55c3add 100644
--- a/rola-utils/space-system/src/space.rs
+++ b/rola-utils/space-system/src/space.rs
@@ -10,7 +10,7 @@ use std::{
mod error;
pub use error::*;
-pub struct Space<T: SpaceRoot> {
+pub struct Space<T: SpaceRoot + Default> {
path_format_cfg: PathFormatConfig,
content: T,
@@ -20,7 +20,19 @@ pub struct Space<T: SpaceRoot> {
pub(crate) override_pattern: Option<SpaceRootFindPattern>,
}
-impl<T: SpaceRoot> Space<T> {
+impl<T: SpaceRoot + Default> Clone for Space<T> {
+ fn clone(&self) -> Self {
+ Self {
+ path_format_cfg: self.path_format_cfg,
+ content: T::default(),
+ space_dir: RwLock::new(None),
+ current_dir: self.current_dir.clone(),
+ override_pattern: None,
+ }
+ }
+}
+
+impl<T: SpaceRoot + Default> Space<T> {
/// Create a new `Space` instance with the given content.
pub fn new(content: T) -> Self {
Space {
@@ -35,11 +47,11 @@ impl<T: SpaceRoot> Space<T> {
}
}
- /// Initialize a space at the given path.
+ /// Initialize a space at the given path (sync version).
///
/// Checks if a space exists at the given path. If not, creates a new space
/// by calling `T::create_space()` at that path.
- pub async fn init(&self, path: impl AsRef<Path>) -> Result<(), SpaceError> {
+ pub fn init(&self, path: impl AsRef<Path>) -> Result<(), SpaceError> {
let path = path.as_ref();
let pattern = match &self.override_pattern {
Some(pattern) => pattern,
@@ -53,38 +65,90 @@ impl<T: SpaceRoot> Space<T> {
};
if find_space_root_with(&path, pattern).is_err() {
- T::create_space(&path).await?;
+ T::create_space(&path)?
}
Ok(())
}
- /// Create a new space at the given path with the specified name.
+ /// Initialize a space at the given path (async version).
+ ///
+ /// Checks if a space exists at the given path. If not, creates a new space
+ /// by calling `T::create_space()` at that path.
+ pub async fn init_async(&self, path: impl AsRef<Path>) -> Result<(), SpaceError> {
+ let path = path.as_ref();
+ let pattern = match &self.override_pattern {
+ Some(pattern) => pattern,
+ None => &T::get_pattern(),
+ };
+
+ // If using Absolute, directly read the internal path
+ let path = match &pattern {
+ SpaceRootFindPattern::AbsolutePath(path_buf) => path_buf.clone(),
+ _ => path.to_path_buf(),
+ };
+
+ if find_space_root_with(&path, pattern).is_err() {
+ T::create_space(&path)?;
+ }
+ Ok(())
+ }
+
+ /// Create a new space at the given path with the specified name (sync version).
+ ///
+ /// The full path is constructed as `path/name`. Checks if a space already
+ /// exists at that location. If not, creates a new space by calling
+ /// `T::create_space()` at that path.
+ pub fn create(&self, path: impl AsRef<Path>, name: &str) -> Result<(), SpaceError> {
+ let full_path = path.as_ref().join(name);
+ self.init(full_path)
+ }
+
+ /// Create a new space at the given path with the specified name (async version).
///
/// The full path is constructed as `path/name`. Checks if a space already
/// exists at that location. If not, creates a new space by calling
/// `T::create_space()` at that path.
- pub async fn create(&self, path: impl AsRef<Path>, name: &str) -> Result<(), SpaceError> {
+ pub async fn create_async(&self, path: impl AsRef<Path>, name: &str) -> Result<(), SpaceError> {
let full_path = path.as_ref().join(name);
- self.init(full_path).await
+ self.init_async(full_path).await
+ }
+
+ /// Initialize a space in the current directory (sync version).
+ ///
+ /// Checks if a space exists in the current directory. If not, creates a new space
+ /// by calling `T::create_space()` at the current directory.
+ pub fn init_here(&self) -> Result<(), SpaceError> {
+ let current_dir = self.current_dir()?;
+ self.init(current_dir)
}
- /// Initialize a space in the current directory.
+ /// Initialize a space in the current directory (async version).
///
/// Checks if a space exists in the current directory. If not, creates a new space
/// by calling `T::create_space()` at the current directory.
- pub async fn init_here(&self) -> Result<(), SpaceError> {
+ pub async fn init_here_async(&self) -> Result<(), SpaceError> {
let current_dir = self.current_dir()?;
- self.init(current_dir).await
+ self.init_async(current_dir).await
}
- /// Create a new space in the current directory with the specified name.
+ /// Create a new space in the current directory with the specified name (sync version).
///
/// The full path is constructed as `current_dir/name`. Checks if a space already
/// exists at that location. If not, creates a new space by calling
/// `T::create_space()` at that path.
- pub async fn create_here(&self, name: &str) -> Result<(), SpaceError> {
+ pub fn create_here(&self, name: &str) -> Result<(), SpaceError> {
let current_dir = self.current_dir()?;
- self.create(current_dir, name).await
+ self.create(current_dir, name)
+ }
+
+ /// Create a new space in the current directory with the specified name (async version).
+ ///
+ /// The full path is constructed as `current_dir/name`. Checks if a space already
+ /// exists at that location. If not, creates a new space by calling
+ /// `T::create_space()` at that path.
+ pub async fn create_here_async(&self, name: &str) -> Result<(), SpaceError> {
+ let current_dir = self.current_dir()?;
+ self.create_async(current_dir, name).await
}
/// Consume the `Space`, returning the inner content.
@@ -131,9 +195,9 @@ impl<T: SpaceRoot> Space<T> {
/// Set the current directory explicitly.
///
/// This clears any cached space directory.
- pub fn set_current_dir(&mut self, path: PathBuf) -> Result<(), SpaceError> {
+ pub fn set_current_dir(&mut self, path: impl AsRef<Path>) -> Result<(), SpaceError> {
self.update_space_dir(None);
- self.current_dir = Some(fmt_path(path)?);
+ self.current_dir = Some(fmt_path(path.as_ref())?);
Ok(())
}
@@ -177,7 +241,7 @@ impl<T: SpaceRoot> Space<T> {
}
}
-impl<T: SpaceRoot> Space<T> {
+impl<T: SpaceRoot + Default> Space<T> {
/// Convert a relative path to an absolute path within the space.
///
/// The path is formatted according to the space's path format configuration.
@@ -203,65 +267,122 @@ impl<T: SpaceRoot> Space<T> {
}
/// Canonicalize a relative path within the space.
- pub async fn canonicalize(
+ pub fn canonicalize(&self, relative_path: impl AsRef<Path>) -> Result<PathBuf, SpaceError> {
+ let path = self.local_path(relative_path)?;
+ Ok(std::fs::canonicalize(path)?)
+ }
+
+ /// Canonicalize a relative path within the space (async version).
+ pub async fn canonicalize_async(
&self,
relative_path: impl AsRef<Path>,
) -> Result<PathBuf, SpaceError> {
- let path = self.local_path(relative_path)?;
- Ok(tokio::fs::canonicalize(path).await?)
+ self.canonicalize(relative_path)
}
/// Copy a file from one relative path to another within the space.
- pub async fn copy(
+ pub fn copy(&self, from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<u64, SpaceError> {
+ let from_path = self.local_path(from)?;
+ let to_path = self.local_path(to)?;
+ Ok(std::fs::copy(from_path, to_path)?)
+ }
+
+ /// Copy a file from one relative path to another within the space (async version).
+ pub async fn copy_async(
&self,
from: impl AsRef<Path>,
to: impl AsRef<Path>,
) -> Result<u64, SpaceError> {
- let from_path = self.local_path(from)?;
- let to_path = self.local_path(to)?;
- Ok(tokio::fs::copy(from_path, to_path).await?)
+ self.copy(from, to)
}
/// Create a directory at the given relative path within the space.
- pub async fn create_dir(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
+ pub fn create_dir(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::create_dir(path).await?)
+ Ok(std::fs::create_dir(path)?)
+ }
+
+ /// Create a directory at the given relative path within the space (async version).
+ pub async fn create_dir_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<(), SpaceError> {
+ self.create_dir(relative_path)
}
/// Recursively create a directory and all its parents at the given relative path within the space.
- pub async fn create_dir_all(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
+ pub fn create_dir_all(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::create_dir_all(path).await?)
+ Ok(std::fs::create_dir_all(path)?)
+ }
+
+ /// Recursively create a directory and all its parents at the given relative path within the space (async version).
+ pub async fn create_dir_all_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<(), SpaceError> {
+ self.create_dir_all(relative_path)
}
/// Create a hard link from `src` to `dst` within the space.
- pub async fn hard_link(
+ pub fn hard_link(
&self,
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
) -> Result<(), SpaceError> {
let src_path = self.local_path(src)?;
let dst_path = self.local_path(dst)?;
- Ok(tokio::fs::hard_link(src_path, dst_path).await?)
+ Ok(std::fs::hard_link(src_path, dst_path)?)
+ }
+
+ /// Create a hard link from `src` to `dst` within the space (async version).
+ pub async fn hard_link_async(
+ &self,
+ src: impl AsRef<Path>,
+ dst: impl AsRef<Path>,
+ ) -> Result<(), SpaceError> {
+ self.hard_link(src, dst)
}
/// Get metadata for a file or directory at the given relative path within the space.
- pub async fn metadata(
+ pub fn metadata(
&self,
relative_path: impl AsRef<Path>,
) -> Result<std::fs::Metadata, SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::metadata(path).await?)
+ Ok(std::fs::metadata(path)?)
+ }
+
+ /// Get metadata for a file or directory at the given relative path within the space (async version).
+ pub async fn metadata_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<std::fs::Metadata, SpaceError> {
+ self.metadata(relative_path)
}
/// Read the entire contents of a file at the given relative path within the space.
- pub async fn read(&self, relative_path: impl AsRef<Path>) -> Result<Vec<u8>, SpaceError> {
+ pub fn read(&self, relative_path: impl AsRef<Path>) -> Result<Vec<u8>, SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::read(path).await?)
+ Ok(std::fs::read(path)?)
+ }
+
+ /// Read the entire contents of a file at the given relative path within the space (async version).
+ pub async fn read_async(&self, relative_path: impl AsRef<Path>) -> Result<Vec<u8>, SpaceError> {
+ self.read(relative_path)
}
/// Read the directory entries at the given relative path within the space.
- pub async fn read_dir(
+ pub fn read_dir(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<std::fs::ReadDir, SpaceError> {
+ let path = self.local_path(relative_path)?;
+ Ok(std::fs::read_dir(path)?)
+ }
+
+ /// Read the directory entries at the given relative path within the space (async version).
+ pub async fn read_dir_async(
&self,
relative_path: impl AsRef<Path>,
) -> Result<tokio::fs::ReadDir, SpaceError> {
@@ -270,112 +391,216 @@ impl<T: SpaceRoot> Space<T> {
}
/// Read the target of a symbolic link at the given relative path within the space.
- pub async fn read_link(&self, relative_path: impl AsRef<Path>) -> Result<PathBuf, SpaceError> {
+ pub fn read_link(&self, relative_path: impl AsRef<Path>) -> Result<PathBuf, SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::read_link(path).await?)
+ Ok(std::fs::read_link(path)?)
+ }
+
+ /// Read the target of a symbolic link at the given relative path within the space (async version).
+ pub async fn read_link_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<PathBuf, SpaceError> {
+ self.read_link(relative_path)
}
/// Read the entire contents of a file as a string at the given relative path within the space.
- pub async fn read_to_string(
+ pub fn read_to_string(&self, relative_path: impl AsRef<Path>) -> Result<String, SpaceError> {
+ let path = self.local_path(relative_path)?;
+ Ok(std::fs::read_to_string(path)?)
+ }
+
+ /// Read the entire contents of a file as a string at the given relative path within the space (async version).
+ pub async fn read_to_string_async(
&self,
relative_path: impl AsRef<Path>,
) -> Result<String, SpaceError> {
- let path = self.local_path(relative_path)?;
- Ok(tokio::fs::read_to_string(path).await?)
+ self.read_to_string(relative_path)
}
/// Remove an empty directory at the given relative path within the space.
- pub async fn remove_dir(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
+ pub fn remove_dir(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::remove_dir(path).await?)
+ Ok(std::fs::remove_dir(path)?)
+ }
+
+ /// Remove an empty directory at the given relative path within the space (async version).
+ pub async fn remove_dir_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<(), SpaceError> {
+ self.remove_dir(relative_path)
}
/// Remove a directory and all its contents at the given relative path within the space.
- pub async fn remove_dir_all(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
+ pub fn remove_dir_all(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::remove_dir_all(path).await?)
+ Ok(std::fs::remove_dir_all(path)?)
+ }
+
+ /// Remove a directory and all its contents at the given relative path within the space (async version).
+ pub async fn remove_dir_all_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<(), SpaceError> {
+ self.remove_dir_all(relative_path)
}
/// Remove a file at the given relative path within the space.
- pub async fn remove_file(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
+ pub fn remove_file(&self, relative_path: impl AsRef<Path>) -> Result<(), SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::remove_file(path).await?)
+ Ok(std::fs::remove_file(path)?)
+ }
+
+ /// Remove a file at the given relative path within the space (async version).
+ pub async fn remove_file_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<(), SpaceError> {
+ self.remove_file(relative_path)
}
/// Rename a file or directory from one relative path to another within the space.
- pub async fn rename(
+ pub fn rename(&self, from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<(), SpaceError> {
+ let from_path = self.local_path(from)?;
+ let to_path = self.local_path(to)?;
+ Ok(std::fs::rename(from_path, to_path)?)
+ }
+
+ /// Rename a file or directory from one relative path to another within the space (async version).
+ pub async fn rename_async(
&self,
from: impl AsRef<Path>,
to: impl AsRef<Path>,
) -> Result<(), SpaceError> {
- let from_path = self.local_path(from)?;
- let to_path = self.local_path(to)?;
- Ok(tokio::fs::rename(from_path, to_path).await?)
+ self.rename(from, to)
}
/// Set permissions for a file or directory at the given relative path within the space.
- pub async fn set_permissions(
+ pub fn set_permissions(
&self,
relative_path: impl AsRef<Path>,
perm: std::fs::Permissions,
) -> Result<(), SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::set_permissions(path, perm).await?)
+ Ok(std::fs::set_permissions(path, perm)?)
+ }
+
+ /// Set permissions for a file or directory at the given relative path within the space (async version).
+ pub async fn set_permissions_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ perm: std::fs::Permissions,
+ ) -> Result<(), SpaceError> {
+ self.set_permissions(relative_path, perm)
}
/// Create a symbolic link from `src` to `dst` within the space (Unix only).
#[cfg(unix)]
- pub async fn symlink(
+ pub fn symlink(&self, src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), SpaceError> {
+ let src_path = self.local_path(src)?;
+ let dst_path = self.local_path(dst)?;
+ Ok(std::os::unix::fs::symlink(src_path, dst_path)?)
+ }
+
+ /// Create a symbolic link from `src` to `dst` within the space (Unix only, async version).
+ #[cfg(unix)]
+ pub async fn symlink_async(
&self,
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
) -> Result<(), SpaceError> {
- let src_path = self.local_path(src)?;
- let dst_path = self.local_path(dst)?;
- Ok(tokio::fs::symlink(src_path, dst_path).await?)
+ self.symlink(src, dst)
}
/// Create a directory symbolic link from `src` to `dst` within the space (Windows only).
#[cfg(windows)]
- pub async fn symlink_dir(
+ pub fn symlink_dir(
&self,
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
) -> Result<(), SpaceError> {
let src_path = self.local_path(src)?;
let dst_path = self.local_path(dst)?;
- Ok(tokio::fs::symlink_dir(src_path, dst_path).await?)
+ Ok(std::os::windows::fs::symlink_dir(src_path, dst_path)?)
+ }
+
+ /// Create a directory symbolic link from `src` to `dst` within the space (Windows only, async version).
+ #[cfg(windows)]
+ pub async fn symlink_dir_async(
+ &self,
+ src: impl AsRef<Path>,
+ dst: impl AsRef<Path>,
+ ) -> Result<(), SpaceError> {
+ self.symlink_dir(src, dst)
}
/// Create a file symbolic link from `src` to `dst` within the space (Windows only).
#[cfg(windows)]
- pub async fn symlink_file(
+ pub fn symlink_file(
&self,
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
) -> Result<(), SpaceError> {
let src_path = self.local_path(src)?;
let dst_path = self.local_path(dst)?;
- Ok(tokio::fs::symlink_file(src_path, dst_path).await?)
+ Ok(std::os::windows::fs::symlink_file(src_path, dst_path)?)
+ }
+
+ /// Create a file symbolic link from `src` to `dst` within the space (Windows only, async version).
+ #[cfg(windows)]
+ pub async fn symlink_file_async(
+ &self,
+ src: impl AsRef<Path>,
+ dst: impl AsRef<Path>,
+ ) -> Result<(), SpaceError> {
+ self.symlink_file(src, dst)
}
/// Get metadata for a file or directory without following symbolic links.
- pub async fn symlink_metadata(
+ pub fn symlink_metadata(
&self,
relative_path: impl AsRef<Path>,
) -> Result<std::fs::Metadata, SpaceError> {
let path = self.local_path(relative_path)?;
- Ok(tokio::fs::symlink_metadata(path).await?)
+ Ok(std::fs::symlink_metadata(path)?)
+ }
+
+ /// Get metadata for a file or directory without following symbolic links (async version).
+ pub async fn symlink_metadata_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<std::fs::Metadata, SpaceError> {
+ self.symlink_metadata(relative_path)
}
/// Check if a file or directory exists at the given relative path within the space.
- pub async fn try_exists(&self, relative_path: impl AsRef<Path>) -> Result<bool, SpaceError> {
+ pub fn try_exists(&self, relative_path: impl AsRef<Path>) -> Result<bool, SpaceError> {
+ let path = self.local_path(relative_path)?;
+ Ok(path.try_exists()?)
+ }
+
+ /// Check if a file or directory exists at the given relative path within the space (async version).
+ pub async fn try_exists_async(
+ &self,
+ relative_path: impl AsRef<Path>,
+ ) -> Result<bool, SpaceError> {
let path = self.local_path(relative_path)?;
Ok(tokio::fs::try_exists(path).await?)
}
/// Write data to a file at the given relative path within the space.
- pub async fn write(
+ pub fn write(
+ &self,
+ relative_path: impl AsRef<Path>,
+ contents: impl AsRef<[u8]>,
+ ) -> Result<(), SpaceError> {
+ let path = self.local_path(relative_path)?;
+ Ok(std::fs::write(path, contents)?)
+ }
+
+ /// Write data to a file at the given relative path within the space (async version).
+ pub async fn write_async(
&self,
relative_path: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
@@ -385,25 +610,31 @@ impl<T: SpaceRoot> Space<T> {
}
/// Check if a file or directory exists at the given relative path within the space.
- pub async fn exists(&self, relative_path: impl AsRef<Path>) -> Result<bool, SpaceError> {
+ pub fn exists(&self, relative_path: impl AsRef<Path>) -> Result<bool, SpaceError> {
+ let path = self.local_path(relative_path)?;
+ Ok(path.try_exists()?)
+ }
+
+ /// Check if a file or directory exists at the given relative path within the space (async version).
+ pub async fn exists_async(&self, relative_path: impl AsRef<Path>) -> Result<bool, SpaceError> {
let path = self.local_path(relative_path)?;
Ok(tokio::fs::try_exists(path).await?)
}
}
-impl<T: SpaceRoot> From<T> for Space<T> {
+impl<T: SpaceRoot + Default> From<T> for Space<T> {
fn from(content: T) -> Self {
Space::<T>::new(content)
}
}
-impl<T: SpaceRoot> AsRef<T> for Space<T> {
+impl<T: SpaceRoot + Default> AsRef<T> for Space<T> {
fn as_ref(&self) -> &T {
&self.content
}
}
-impl<T: SpaceRoot> Deref for Space<T> {
+impl<T: SpaceRoot + Default> Deref for Space<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.as_ref()
@@ -415,7 +646,7 @@ pub trait SpaceRoot: Sized {
fn get_pattern() -> SpaceRootFindPattern;
/// Given a non-space directory, implement logic to make it a space-recognizable directory
- fn create_space(path: &Path) -> impl Future<Output = Result<(), SpaceError>> + Send;
+ fn create_space(path: &Path) -> Result<(), SpaceError>;
}
pub enum SpaceRootFindPattern {
diff --git a/rola-utils/space-system/src/space/error.rs b/rola-utils/space-system/src/space/error.rs
index 8e85010..f039698 100644
--- a/rola-utils/space-system/src/space/error.rs
+++ b/rola-utils/space-system/src/space/error.rs
@@ -1,3 +1,5 @@
+use serde::Serialize;
+
#[derive(thiserror::Error, Debug)]
pub enum SpaceError {
#[error("Space not found")]
@@ -19,6 +21,15 @@ pub enum SpaceError {
ConfigFileAlreadyExist,
}
+impl Serialize for SpaceError {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&self.to_string())
+ }
+}
+
impl PartialEq for SpaceError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {