diff options
33 files changed, 1444 insertions, 118 deletions
@@ -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" @@ -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) { |
