summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-18 04:40:25 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-18 04:40:25 +0800
commit7879ac01b24eb9723ec0a814adaee1fc9c52610a (patch)
treed1c9a07e3ef8819869494c45e96bcd3e98856bdb
parent0b8e6e7d18abb94bd99553dc1d2b0ba5d4f265ea (diff)
feat(rola-cli): implement bucket creation and CLI entry point
Add bucket creation logic with pre-checks, localized error handling, and a basic CLI entry point using the mingling framework. Introduce a placeholder protocol for bucket transfer testing.
-rw-r--r--Cargo.lock169
-rw-r--r--rola-bucket/src/protocol.rs3
-rw-r--r--rola-bucket/src/protocol/placeholder.rs45
-rw-r--r--rola-cli/Cargo.toml17
-rw-r--r--rola-cli/locales/errors/i18n_io_error.toml85
-rw-r--r--rola-cli/locales/helps/basic.toml9
-rw-r--r--rola-cli/locales/i18n_bucket_manager.toml25
-rw-r--r--rola-cli/src/bin/rola.rs64
-rw-r--r--rola-cli/src/bucket_mgr.rs2
-rw-r--r--rola-cli/src/bucket_mgr/creation.rs119
-rw-r--r--rola-cli/src/error.rs2
-rw-r--r--rola-cli/src/error/io.rs236
-rw-r--r--rola-cli/src/lib.rs42
-rw-r--r--rola-cli/src/res.rs1
-rw-r--r--rola-cli/src/res/current_dir.rs11
-rw-r--r--rola-cli/src/tokio_wrapper.rs48
16 files changed, 877 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6347833..c3f4755 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,12 +3,69 @@
version = 4
[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
name = "just_fmt"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "mingling"
+version = "0.2.0"
+source = "git+https://github.com/mingling-rs/mingling.git?rev=002f3fd390f64b1d7632f8530a0db81d45edf6c2#002f3fd390f64b1d7632f8530a0db81d45edf6c2"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+ "size",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.2.0"
+source = "git+https://github.com/mingling-rs/mingling.git?rev=002f3fd390f64b1d7632f8530a0db81d45edf6c2#002f3fd390f64b1d7632f8530a0db81d45edf6c2"
+dependencies = [
+ "just_fmt",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.2.0"
+source = "git+https://github.com/mingling-rs/mingling.git?rev=002f3fd390f64b1d7632f8530a0db81d45edf6c2#002f3fd390f64b1d7632f8530a0db81d45edf6c2"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -48,8 +105,13 @@ dependencies = [
name = "rola-cli"
version = "0.1.0"
dependencies = [
+ "mingling",
+ "rorolala",
+ "shakehand",
"shared_functions",
"shared_macros",
+ "space-system",
+ "tokio",
]
[[package]]
@@ -67,6 +129,57 @@ dependencies = [
]
[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "shakehand"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d600165497e6e8907a7fc9cc7a82dbd7f28d41f6147540cd75b75903a67f019e"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "toml",
+]
+
+[[package]]
name = "shared_constants"
version = "0.1.0"
dependencies = [
@@ -91,6 +204,12 @@ dependencies = [
]
[[package]]
+name = "size"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
+
+[[package]]
name = "space-macros"
version = "0.1.0"
dependencies = [
@@ -163,7 +282,57 @@ dependencies = [
]
[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
diff --git a/rola-bucket/src/protocol.rs b/rola-bucket/src/protocol.rs
index 5523176..ba50a42 100644
--- a/rola-bucket/src/protocol.rs
+++ b/rola-bucket/src/protocol.rs
@@ -8,6 +8,9 @@ pub use error::*;
mod local_fs;
pub use local_fs::*;
+mod placeholder;
+pub use placeholder::*;
+
/// Request used in [BucketTransferProtocol] or [AsyncBucketTransferProtocol]
pub struct UploadToBucketRequest<'a, ExtraInfo>
where
diff --git a/rola-bucket/src/protocol/placeholder.rs b/rola-bucket/src/protocol/placeholder.rs
new file mode 100644
index 0000000..6fd5a10
--- /dev/null
+++ b/rola-bucket/src/protocol/placeholder.rs
@@ -0,0 +1,45 @@
+use crate::BucketTransferProtocol;
+
+/// A placeholder implementation of the bucket transfer protocol.
+///
+/// This struct serves as a temporary or stub implementation of
+/// [`BucketTransferProtocol`], intended for use in contexts where
+/// actual data transfer is not required (e.g., testing, scaffolding,
+/// or as a default before a real implementation is provided).
+///
+/// Calling any of the transfer methods on this implementation will
+/// result in a panic with `unreachable!()`, signaling that the
+/// placeholder should be replaced before use.
+pub struct NoProtocol;
+
+impl BucketTransferProtocol for NoProtocol {
+ type ExtraInfo = ();
+
+ fn upload_to_bucket(
+ &self,
+ _req: &super::UploadToBucketRequest<Self::ExtraInfo>,
+ ) -> Result<(), super::BucketTransferProtocolError> {
+ unreachable!()
+ }
+
+ fn download_from_bucket(
+ &self,
+ _req: &super::DownloadFromBucketRequest<Self::ExtraInfo>,
+ ) -> Result<(), super::BucketTransferProtocolError> {
+ unreachable!()
+ }
+
+ fn transfer_to_client(
+ &self,
+ _req: &super::TransferToClientRequest<Self::ExtraInfo>,
+ ) -> Result<(), super::BucketTransferProtocolError> {
+ unreachable!()
+ }
+
+ fn receive_from_client(
+ &self,
+ _req: &super::ReceiveFromClientRequest<Self::ExtraInfo>,
+ ) -> Result<(), super::BucketTransferProtocolError> {
+ unreachable!()
+ }
+}
diff --git a/rola-cli/Cargo.toml b/rola-cli/Cargo.toml
index 9ee6816..a04a403 100644
--- a/rola-cli/Cargo.toml
+++ b/rola-cli/Cargo.toml
@@ -6,5 +6,22 @@ authors.workspace = true
license.workspace = true
[dependencies]
+rorolala.workspace = true
+
shared_functions.workspace = true
shared_macros.workspace = true
+
+space-system.workspace = true
+
+tokio.workspace = true
+
+shakehand = "0.1.3"
+
+[dependencies.mingling]
+git = "https://github.com/mingling-rs/mingling.git"
+rev = "002f3fd390f64b1d7632f8530a0db81d45edf6c2"
+features = [
+ "parser",
+ "extra_macros",
+ "dispatch_tree"
+]
diff --git a/rola-cli/locales/errors/i18n_io_error.toml b/rola-cli/locales/errors/i18n_io_error.toml
new file mode 100644
index 0000000..9e92f7e
--- /dev/null
+++ b/rola-cli/locales/errors/i18n_io_error.toml
@@ -0,0 +1,85 @@
+[en]
+io_error_name = "IO Error: "
+
+not_found = "not found: %{info}"
+permission_denied = "permission denied: %{info}"
+connection_refused = "connection refused: %{info}"
+connection_reset = "connection reset: %{info}"
+host_unreachable = "host unreachable: %{info}"
+network_unreachable = "network unreachable: %{info}"
+connection_aborted = "connection aborted: %{info}"
+not_connected = "not connected: %{info}"
+addr_in_use = "address in use: %{info}"
+addr_not_available = "address not available: %{info}"
+network_down = "network down: %{info}"
+broken_pipe = "broken pipe: %{info}"
+already_exists = "already exists: %{info}"
+would_block = "would block: %{info}"
+not_a_directory = "not a directory: %{info}"
+is_a_directory = "is a directory: %{info}"
+directory_not_empty = "directory not empty: %{info}"
+read_only_filesystem = "read-only filesystem: %{info}"
+stale_network_file_handle = "stale network file handle: %{info}"
+invalid_input = "invalid input: %{info}"
+invalid_data = "invalid data: %{info}"
+timed_out = "timed out: %{info}"
+write_zero = "write zero: %{info}"
+storage_full = "storage full: %{info}"
+not_seekable = "not seekable: %{info}"
+quota_exceeded = "quota exceeded: %{info}"
+file_too_large = "file too large: %{info}"
+resource_busy = "resource busy: %{info}"
+executable_file_busy = "executable file busy: %{info}"
+deadlock = "deadlock: %{info}"
+crosses_devices = "crosses devices: %{info}"
+too_many_links = "too many links: %{info}"
+invalid_filename = "invalid filename: %{info}"
+argument_list_too_long = "argument list too long: %{info}"
+interrupted = "interrupted: %{info}"
+unsupported = "unsupported: %{info}"
+unexpected_eof = "unexpected end of file: %{info}"
+out_of_memory = "out of memory: %{info}"
+other = "other error: %{info}"
+
+[zh_CN]
+io_error_name = "IO 错误: "
+
+not_found = "未找到: %{info}"
+permission_denied = "权限被拒绝: %{info}"
+connection_refused = "连接被拒绝: %{info}"
+connection_reset = "连接已重置: %{info}"
+host_unreachable = "主机不可达: %{info}"
+network_unreachable = "网络不可达: %{info}"
+connection_aborted = "连接已中止: %{info}"
+not_connected = "未连接: %{info}"
+addr_in_use = "地址已在使用中: %{info}"
+addr_not_available = "地址不可用: %{info}"
+network_down = "网络已断开: %{info}"
+broken_pipe = "管道破裂: %{info}"
+already_exists = "对象已存在: %{info}"
+would_block = "操作会阻塞,无法立即完成: %{info}"
+not_a_directory = "路径不是一个目录: %{info}"
+is_a_directory = "路径是一个目录: %{info}"
+directory_not_empty = "目录非空,无法删除: %{info}"
+read_only_filesystem = "文件系统为只读: %{info}"
+stale_network_file_handle = "网络文件句柄已过期: %{info}"
+invalid_input = "输入无效: %{info}"
+invalid_data = "数据无效: %{info}"
+timed_out = "操作超时: %{info}"
+write_zero = "写入了零字节: %{info}"
+storage_full = "存储空间已满: %{info}"
+not_seekable = "不可定位: %{info}"
+quota_exceeded = "超出配额限制: %{info}"
+file_too_large = "文件过大,无法处理: %{info}"
+resource_busy = "资源正忙,请稍后重试: %{info}"
+executable_file_busy = "可执行文件忙,无法访问: %{info}"
+deadlock = "发生死锁: %{info}"
+crosses_devices = "跨设备操作: %{info}"
+too_many_links = "链接数量过多: %{info}"
+invalid_filename = "文件名无效: %{info}"
+argument_list_too_long = "参数列表过长: %{info}"
+interrupted = "操作被中断: %{info}"
+unsupported = "不支持此操作: %{info}"
+unexpected_eof = "意外地遇到了文件结尾: %{info}"
+out_of_memory = "内存不足: %{info}"
+other = "发生了其他错误: %{info}"
diff --git a/rola-cli/locales/helps/basic.toml b/rola-cli/locales/helps/basic.toml
new file mode 100644
index 0000000..656cdc4
--- /dev/null
+++ b/rola-cli/locales/helps/basic.toml
@@ -0,0 +1,9 @@
+[en]
+help = """
+NO YET
+"""
+
+[zh_CN]
+help = """
+暂无
+"""
diff --git a/rola-cli/locales/i18n_bucket_manager.toml b/rola-cli/locales/i18n_bucket_manager.toml
new file mode 100644
index 0000000..d7f0ffe
--- /dev/null
+++ b/rola-cli/locales/i18n_bucket_manager.toml
@@ -0,0 +1,25 @@
+[en]
+created = "Bucket was created at \"%{path}\""
+
+error_directory_not_empty = "The specified directory is not empty, cannot create bucket here"
+
+error_bucket_path_not_provided = """
+Please provide a bucket path!
+
+Example: `rola bucket create <name>`
+"""
+
+error_bucket_path_not_directory = "The path \"%{path}\" is not a directory!"
+
+[zh_CN]
+created = "桶已被创建于 \"%{path}\""
+
+error_directory_not_empty = "您指定的目录不为空,无法在此处创建桶"
+
+error_bucket_path_not_provided = """
+请提供桶路径!
+
+例如:`rola bucket create <名称>`
+"""
+
+error_bucket_path_not_directory = "路径 \"%{path}\" 不是一个目录!"
diff --git a/rola-cli/src/bin/rola.rs b/rola-cli/src/bin/rola.rs
index f328e4d..1ebc3ae 100644
--- a/rola-cli/src/bin/rola.rs
+++ b/rola-cli/src/bin/rola.rs
@@ -1 +1,63 @@
-fn main() {}
+use std::{env::current_dir, process::exit};
+
+use mingling::{
+ Program,
+ macros::program_setup,
+ setup::{BasicProgramSetup, ExitCodeSetup, QuietFlagSetup},
+};
+use rola_cli::{ThisProgram, locale, res::current_dir::ResCurrentDir};
+
+fn main() {
+ let mut program = ThisProgram::new();
+
+ program.global_flag(["-v", "--version"], |_| {
+ let help = locale::helps::Basic::help().trim();
+ eprintln!("{}", help);
+ exit(0)
+ });
+
+ // Language
+ locale::set_lang(
+ program
+ .pick_global_argument(["-L", "--lang"])
+ .unwrap_or(locale::current_locales()),
+ );
+
+ // Resources
+ program.with_resource(ResCurrentDir {
+ cwd: current_dir().unwrap(),
+ });
+
+ // Setup
+ program.with_setup(StandardOutputSetup);
+ program.with_setup(BasicProgramSetup);
+ program.with_setup(ExitCodeSetup::default());
+
+ // Execute
+ let quiet = program.stdout_setting.quiet;
+ let error_output = program.stdout_setting.error_output && !quiet;
+ let render_output = program.stdout_setting.render_output && !quiet;
+ let result = program.exec_without_render().unwrap();
+ if !result.is_empty() {
+ if result.exit_code == 0 && render_output {
+ println!("{}", result.trim());
+ } else if error_output {
+ eprintln!("{}", result.trim());
+ }
+ }
+ exit(result.exit_code);
+}
+
+#[program_setup]
+fn standard_output_setup(program: &mut Program<ThisProgram>) {
+ program.with_setup(QuietFlagSetup::new("--silence"));
+ program.global_flag(["--no-error"], |program| {
+ program.stdout_setting.error_output = false;
+ });
+ program.global_flag(["--no-result"], |program| {
+ program.stdout_setting.render_output = false;
+ });
+ program.global_flag(["--silence", "--quiet"], |program| {
+ program.stdout_setting.quiet = true;
+ });
+}
diff --git a/rola-cli/src/bucket_mgr.rs b/rola-cli/src/bucket_mgr.rs
new file mode 100644
index 0000000..9aa8847
--- /dev/null
+++ b/rola-cli/src/bucket_mgr.rs
@@ -0,0 +1,2 @@
+mod creation;
+pub use creation::*;
diff --git a/rola-cli/src/bucket_mgr/creation.rs b/rola-cli/src/bucket_mgr/creation.rs
new file mode 100644
index 0000000..831a3e7
--- /dev/null
+++ b/rola-cli/src/bucket_mgr/creation.rs
@@ -0,0 +1,119 @@
+use std::{fs::create_dir_all, path::PathBuf};
+
+use mingling::{
+ macros::{chain, dispatcher, pack, r_println, renderer, route},
+ parser::AsPicker,
+ res::ResExitCode,
+};
+use rorolala::bucket::{Bucket, NoProtocol};
+use space_system::{Space, SpaceError};
+
+use crate::{
+ Next, error::ErrorIo, locale::I18nBucketManager, res::current_dir::ResCurrentDir, tkr,
+};
+
+pub const EC_BUCKET_CREATE_DIR_NOT_EMPTY: i32 = 2400;
+pub const EC_BUCKET_PATH_NOT_PROVIDED: i32 = 2401;
+pub const EC_BUCKET_PATH_NOT_DIRECTORY: i32 = 2402;
+
+dispatcher!("bucket.init");
+dispatcher!("bucket.create");
+
+pack!(StateBucketCreationPrecheck = PathBuf);
+pack!(StateBucketCreation = PathBuf);
+
+pack!(ResultBucketCreated = PathBuf);
+
+pack!(ErrorDirectoryNotEmpty = PathBuf);
+pack!(ErrorBucketPathNotProvided = ());
+pack!(ErrorBucketPathNotDirectory = PathBuf);
+
+#[chain]
+pub fn handle_bucket_init(_args: EntryBucketInit, cwd: &mut ResCurrentDir) -> Next {
+ // NOTE: It's a dirty operation :D
+ // Directly extract the value of the cwd resource for use, reducing Clone
+ // because it's guaranteed that `ResCurrentDir` won't be used after `handle_bucket_init`
+ let cwd = std::mem::take(&mut cwd.cwd);
+ StateBucketCreationPrecheck::new(cwd)
+}
+
+#[chain]
+pub fn handle_bucket_create(args: EntryBucketCreate) -> Next {
+ let join = route! {
+ args.pick_or_route::<PathBuf, _>((),
+ ErrorBucketPathNotProvided::new(()).to_render()
+ ).unpack()
+ };
+ StateBucketCreationPrecheck::new(join).to_chain()
+}
+
+#[chain]
+pub fn handle_state_bucket_creation_precheck(create: StateBucketCreationPrecheck) -> Next {
+ let path = create.inner;
+ if path.exists() {
+ if !path.is_dir() {
+ return ErrorBucketPathNotDirectory::new(path).to_render();
+ }
+ if path
+ .read_dir()
+ .map(|mut it| it.next().is_some())
+ .unwrap_or(false)
+ {
+ return ErrorDirectoryNotEmpty::new(path).to_render();
+ }
+ }
+ StateBucketCreation::new(path).to_chain()
+}
+
+#[chain]
+pub fn handle_state_bucket_creation(create: StateBucketCreation) -> Next {
+ let path = create.inner;
+
+ route! {
+ create_dir_all(&path).map_err(|e| ErrorIo::from(e).to_render())
+ };
+
+ // Use a protocol-less Bucket as a temporary Space for initialization
+ let bucket_space = Space::<Bucket<NoProtocol>>::new(Bucket::<NoProtocol>::new_local());
+
+ // 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 } {
+ return ErrorIo::from(error).to_render();
+ }
+
+ ResultBucketCreated::new(path).to_render()
+}
+
+#[renderer]
+pub fn render_result_bucket_created(result: ResultBucketCreated) {
+ let path = result.inner.to_string_lossy();
+ r_println!("{}", I18nBucketManager::created(path));
+}
+
+#[renderer]
+pub fn render_error_directory_not_empty(_err: ErrorDirectoryNotEmpty, ec: &mut ResExitCode) {
+ r_println!("{}", I18nBucketManager::error_directory_not_empty().trim());
+ ec.exit_code = EC_BUCKET_CREATE_DIR_NOT_EMPTY;
+}
+
+#[renderer]
+pub fn render_error_bucket_path_not_provided(
+ _err: ErrorBucketPathNotProvided,
+ ec: &mut ResExitCode,
+) {
+ r_println!(
+ "{}",
+ I18nBucketManager::error_bucket_path_not_provided().trim()
+ );
+ ec.exit_code = EC_BUCKET_PATH_NOT_PROVIDED;
+}
+
+#[renderer]
+pub fn render_error_bucket_path_not_directory(
+ _err: ErrorBucketPathNotDirectory,
+ ec: &mut ResExitCode,
+) {
+ r_println!("{}", I18nBucketManager::error_directory_not_empty().trim());
+ ec.exit_code = EC_BUCKET_PATH_NOT_DIRECTORY;
+}
diff --git a/rola-cli/src/error.rs b/rola-cli/src/error.rs
new file mode 100644
index 0000000..608d4e1
--- /dev/null
+++ b/rola-cli/src/error.rs
@@ -0,0 +1,2 @@
+mod io;
+pub use io::*;
diff --git a/rola-cli/src/error/io.rs b/rola-cli/src/error/io.rs
new file mode 100644
index 0000000..7e4de56
--- /dev/null
+++ b/rola-cli/src/error/io.rs
@@ -0,0 +1,236 @@
+use mingling::{
+ Groupped,
+ macros::{r_println, renderer},
+ res::ResExitCode,
+};
+
+use crate::locale::errors::I18nIoError;
+
+pub const EC_IOERR_NOT_FOUND: i32 = 2500;
+pub const EC_IOERR_PERMISSION_DENIED: i32 = 2501;
+pub const EC_IOERR_CONNECTION_REFUSED: i32 = 2502;
+pub const EC_IOERR_CONNECTION_RESET: i32 = 2503;
+pub const EC_IOERR_HOST_UNREACHABLE: i32 = 2504;
+pub const EC_IOERR_NETWORK_UNREACHABLE: i32 = 2505;
+pub const EC_IOERR_CONNECTION_ABORTED: i32 = 2506;
+pub const EC_IOERR_NOT_CONNECTED: i32 = 2507;
+pub const EC_IOERR_ADDR_IN_USE: i32 = 2508;
+pub const EC_IOERR_ADDR_NOT_AVAILABLE: i32 = 2509;
+pub const EC_IOERR_NETWORK_DOWN: i32 = 2510;
+pub const EC_IOERR_BROKEN_PIPE: i32 = 2511;
+pub const EC_IOERR_ALREADY_EXISTS: i32 = 2512;
+pub const EC_IOERR_WOULD_BLOCK: i32 = 2513;
+pub const EC_IOERR_NOT_A_DIRECTORY: i32 = 2514;
+pub const EC_IOERR_IS_A_DIRECTORY: i32 = 2515;
+pub const EC_IOERR_DIRECTORY_NOT_EMPTY: i32 = 2516;
+pub const EC_IOERR_READ_ONLY_FILESYSTEM: i32 = 2517;
+pub const EC_IOERR_STALE_NETWORK_FILE_HANDLE: i32 = 2518;
+pub const EC_IOERR_INVALID_INPUT: i32 = 2519;
+pub const EC_IOERR_INVALID_DATA: i32 = 2520;
+pub const EC_IOERR_TIMED_OUT: i32 = 2521;
+pub const EC_IOERR_WRITE_ZERO: i32 = 2522;
+pub const EC_IOERR_STORAGE_FULL: i32 = 2523;
+pub const EC_IOERR_NOT_SEEKABLE: i32 = 2524;
+pub const EC_IOERR_QUOTA_EXCEEDED: i32 = 2525;
+pub const EC_IOERR_FILE_TOO_LARGE: i32 = 2526;
+pub const EC_IOERR_RESOURCE_BUSY: i32 = 2527;
+pub const EC_IOERR_EXECUTABLE_FILE_BUSY: i32 = 2528;
+pub const EC_IOERR_DEADLOCK: i32 = 2529;
+pub const EC_IOERR_CROSSES_DEVICES: i32 = 2530;
+pub const EC_IOERR_TOO_MANY_LINKS: i32 = 2531;
+pub const EC_IOERR_INVALID_FILENAME: i32 = 2532;
+pub const EC_IOERR_ARGUMENT_LIST_TOO_LONG: i32 = 2533;
+pub const EC_IOERR_INTERRUPTED: i32 = 2534;
+pub const EC_IOERR_UNSUPPORTED: i32 = 2535;
+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)]
+pub enum ErrorIo {
+ #[default]
+ /// DONT USE IT: This variant is only used to provide a Default derive for ErrorIo
+ ///
+ /// In normal creation flow, you should directly use ErrorIo::from(/* std::io::Error */)
+ DontUse,
+
+ Error(std::io::Error),
+}
+
+#[renderer]
+pub fn render_error_io(err: ErrorIo, ec: &mut ResExitCode) {
+ let err: std::io::Error = err.into();
+ let content = format!("{:?}", err);
+ let (error_info, exit_code) = match err.kind() {
+ std::io::ErrorKind::NotFound => (I18nIoError::not_found(content), EC_IOERR_NOT_FOUND),
+ std::io::ErrorKind::PermissionDenied => (
+ I18nIoError::permission_denied(content),
+ EC_IOERR_PERMISSION_DENIED,
+ ),
+ std::io::ErrorKind::ConnectionRefused => (
+ I18nIoError::connection_refused(content),
+ EC_IOERR_CONNECTION_REFUSED,
+ ),
+ std::io::ErrorKind::ConnectionReset => (
+ I18nIoError::connection_reset(content),
+ EC_IOERR_CONNECTION_RESET,
+ ),
+ std::io::ErrorKind::HostUnreachable => (
+ I18nIoError::host_unreachable(content),
+ EC_IOERR_HOST_UNREACHABLE,
+ ),
+ std::io::ErrorKind::NetworkUnreachable => (
+ I18nIoError::network_unreachable(content),
+ EC_IOERR_NETWORK_UNREACHABLE,
+ ),
+ std::io::ErrorKind::ConnectionAborted => (
+ I18nIoError::connection_aborted(content),
+ EC_IOERR_CONNECTION_ABORTED,
+ ),
+ std::io::ErrorKind::NotConnected => {
+ (I18nIoError::not_connected(content), EC_IOERR_NOT_CONNECTED)
+ }
+ std::io::ErrorKind::AddrInUse => (I18nIoError::addr_in_use(content), EC_IOERR_ADDR_IN_USE),
+ std::io::ErrorKind::AddrNotAvailable => (
+ I18nIoError::addr_not_available(content),
+ EC_IOERR_ADDR_NOT_AVAILABLE,
+ ),
+ std::io::ErrorKind::NetworkDown => {
+ (I18nIoError::network_down(content), EC_IOERR_NETWORK_DOWN)
+ }
+ std::io::ErrorKind::BrokenPipe => (I18nIoError::broken_pipe(content), EC_IOERR_BROKEN_PIPE),
+ std::io::ErrorKind::AlreadyExists => (
+ I18nIoError::already_exists(content),
+ EC_IOERR_ALREADY_EXISTS,
+ ),
+ std::io::ErrorKind::WouldBlock => (I18nIoError::would_block(content), EC_IOERR_WOULD_BLOCK),
+ std::io::ErrorKind::NotADirectory => (
+ I18nIoError::not_a_directory(content),
+ EC_IOERR_NOT_A_DIRECTORY,
+ ),
+ std::io::ErrorKind::IsADirectory => (
+ I18nIoError::is_a_directory(content),
+ EC_IOERR_IS_A_DIRECTORY,
+ ),
+ std::io::ErrorKind::DirectoryNotEmpty => (
+ I18nIoError::directory_not_empty(content),
+ EC_IOERR_DIRECTORY_NOT_EMPTY,
+ ),
+ std::io::ErrorKind::ReadOnlyFilesystem => (
+ I18nIoError::read_only_filesystem(content),
+ EC_IOERR_READ_ONLY_FILESYSTEM,
+ ),
+ std::io::ErrorKind::StaleNetworkFileHandle => (
+ I18nIoError::stale_network_file_handle(content),
+ EC_IOERR_STALE_NETWORK_FILE_HANDLE,
+ ),
+ std::io::ErrorKind::InvalidInput => {
+ (I18nIoError::invalid_input(content), EC_IOERR_INVALID_INPUT)
+ }
+ std::io::ErrorKind::InvalidData => {
+ (I18nIoError::invalid_data(content), EC_IOERR_INVALID_DATA)
+ }
+ std::io::ErrorKind::TimedOut => (I18nIoError::timed_out(content), EC_IOERR_TIMED_OUT),
+ std::io::ErrorKind::WriteZero => (I18nIoError::write_zero(content), EC_IOERR_WRITE_ZERO),
+ std::io::ErrorKind::StorageFull => {
+ (I18nIoError::storage_full(content), EC_IOERR_STORAGE_FULL)
+ }
+ std::io::ErrorKind::NotSeekable => {
+ (I18nIoError::not_seekable(content), EC_IOERR_NOT_SEEKABLE)
+ }
+ std::io::ErrorKind::QuotaExceeded => (
+ I18nIoError::quota_exceeded(content),
+ EC_IOERR_QUOTA_EXCEEDED,
+ ),
+ std::io::ErrorKind::FileTooLarge => (
+ I18nIoError::file_too_large(content),
+ EC_IOERR_FILE_TOO_LARGE,
+ ),
+ std::io::ErrorKind::ResourceBusy => {
+ (I18nIoError::resource_busy(content), EC_IOERR_RESOURCE_BUSY)
+ }
+ std::io::ErrorKind::ExecutableFileBusy => (
+ I18nIoError::executable_file_busy(content),
+ EC_IOERR_EXECUTABLE_FILE_BUSY,
+ ),
+ std::io::ErrorKind::Deadlock => (I18nIoError::deadlock(content), EC_IOERR_DEADLOCK),
+ std::io::ErrorKind::CrossesDevices => (
+ I18nIoError::crosses_devices(content),
+ EC_IOERR_CROSSES_DEVICES,
+ ),
+ std::io::ErrorKind::TooManyLinks => (
+ I18nIoError::too_many_links(content),
+ EC_IOERR_TOO_MANY_LINKS,
+ ),
+ std::io::ErrorKind::InvalidFilename => (
+ I18nIoError::invalid_filename(content),
+ EC_IOERR_INVALID_FILENAME,
+ ),
+ std::io::ErrorKind::ArgumentListTooLong => (
+ I18nIoError::argument_list_too_long(content),
+ EC_IOERR_ARGUMENT_LIST_TOO_LONG,
+ ),
+ std::io::ErrorKind::Interrupted => {
+ (I18nIoError::interrupted(content), EC_IOERR_INTERRUPTED)
+ }
+ std::io::ErrorKind::Unsupported => {
+ (I18nIoError::unsupported(content), EC_IOERR_UNSUPPORTED)
+ }
+ std::io::ErrorKind::UnexpectedEof => (
+ I18nIoError::unexpected_eof(content),
+ EC_IOERR_UNEXPECTED_EOF,
+ ),
+ std::io::ErrorKind::OutOfMemory => {
+ (I18nIoError::out_of_memory(content), EC_IOERR_OUT_OF_MEMORY)
+ }
+ std::io::ErrorKind::Other => (I18nIoError::other(content), EC_IOERR_OTHER),
+ _ => (I18nIoError::other(content), EC_IOERR_OTHER),
+ };
+
+ r_println!("{}: {}", I18nIoError::io_error_name(), error_info);
+ ec.exit_code = exit_code;
+}
+
+impl From<std::io::Error> for ErrorIo {
+ fn from(err: std::io::Error) -> Self {
+ ErrorIo::Error(err)
+ }
+}
+
+impl From<ErrorIo> for std::io::Error {
+ fn from(err: ErrorIo) -> Self {
+ match err {
+ ErrorIo::Error(err) => err,
+ ErrorIo::DontUse => std::io::Error::other("Unknown error"),
+ }
+ }
+}
+
+impl std::ops::Deref for ErrorIo {
+ type Target = std::io::Error;
+
+ fn deref(&self) -> &Self::Target {
+ match self {
+ ErrorIo::Error(err) => err,
+ ErrorIo::DontUse => panic!("Cannot deref ErrorIo::Unknown"),
+ }
+ }
+}
+
+impl std::ops::DerefMut for ErrorIo {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ match self {
+ ErrorIo::Error(err) => err,
+ ErrorIo::DontUse => panic!("Cannot deref_mut ErrorIo::Unknown"),
+ }
+ }
+}
+
+impl std::borrow::Borrow<std::io::Error> for ErrorIo {
+ fn borrow(&self) -> &std::io::Error {
+ match self {
+ ErrorIo::Error(err) => err,
+ ErrorIo::DontUse => panic!("Cannot borrow ErrorIo::Unknown"),
+ }
+ }
+}
diff --git a/rola-cli/src/lib.rs b/rola-cli/src/lib.rs
index 8b13789..f81c34e 100644
--- a/rola-cli/src/lib.rs
+++ b/rola-cli/src/lib.rs
@@ -1 +1,43 @@
+use mingling::macros::gen_program;
+pub mod res;
+pub mod tokio_wrapper;
+
+mod bucket_mgr;
+use bucket_mgr::*;
+
+mod error;
+use error::*;
+
+gen_program!();
+
+pub mod locale {
+ shakehand::locale!("locales");
+
+ /// Determines the current locale string used by the application.
+ ///
+ /// The locale is resolved by checking environment variables in the following
+ /// priority order:
+ /// 1. `JV_LANG` — application-specific variable.
+ /// 2. `APP_LANG` — general application language override.
+ /// 3. `LANG` — system locale (e.g. `en_US.UTF-8`). If this variable contains a
+ /// character encoding suffix (e.g. `.UTF-8`), only the base part is kept,
+ /// and underscores (`_`) are replaced with hyphens (`-`) to form a
+ /// language tag (e.g. `en-US`).
+ /// 4. Falls back to `"en"` if none of the above are set.
+ pub fn current_locales() -> String {
+ if let Ok(lang) = std::env::var("JV_LANG") {
+ return lang;
+ }
+ if let Ok(lang) = std::env::var("APP_LANG") {
+ return lang;
+ }
+ if let Ok(lang) = std::env::var("LANG") {
+ if let Some(base_lang) = lang.split('.').next() {
+ return base_lang.replace('_', "-");
+ }
+ return lang;
+ }
+ "en".to_string()
+ }
+}
diff --git a/rola-cli/src/res.rs b/rola-cli/src/res.rs
new file mode 100644
index 0000000..2ff9b75
--- /dev/null
+++ b/rola-cli/src/res.rs
@@ -0,0 +1 @@
+pub mod current_dir;
diff --git a/rola-cli/src/res/current_dir.rs b/rola-cli/src/res/current_dir.rs
new file mode 100644
index 0000000..b774705
--- /dev/null
+++ b/rola-cli/src/res/current_dir.rs
@@ -0,0 +1,11 @@
+use std::path::PathBuf;
+
+/// Represents the current working directory.
+///
+/// This struct encapsulates the path of the current working directory,
+/// providing a clear abstraction for directory context throughout the application.
+#[derive(Debug, Default, Clone)]
+pub struct ResCurrentDir {
+ /// The current working directory path.
+ pub cwd: PathBuf,
+}
diff --git a/rola-cli/src/tokio_wrapper.rs b/rola-cli/src/tokio_wrapper.rs
new file mode 100644
index 0000000..8293355
--- /dev/null
+++ b/rola-cli/src/tokio_wrapper.rs
@@ -0,0 +1,48 @@
+use std::future::Future;
+
+/// Runs an async function to completion by creating a Tokio runtime
+/// and blocking on the future within it.
+///
+/// # Example
+///
+/// ```
+/// use tokio_wrapper::tokio_run;
+///
+/// let result = tokio_run(async {
+/// // your async code here
+/// 42
+/// });
+/// println!("Result: {}", result);
+/// ```
+pub fn tokio_run<F, T>(future: F) -> T
+where
+ F: Future<Output = T>,
+{
+ // Create a new Tokio runtime and block on the future
+ let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
+ rt.block_on(future)
+}
+
+/// A macro that wraps an async expression with `tokio_run`.
+///
+/// Equivalent to:
+/// ```ignore
+/// tokio_run(async move { ... })
+/// ```
+///
+/// # Example
+///
+/// ```
+/// use tokio_wrapper::tkr;
+///
+/// // Instead of:
+/// // tokio_run(async move { some_async_fn().await })
+/// // Use:
+/// let result = tkr! { some_async_fn().await };
+/// ```
+#[macro_export]
+macro_rules! tkr {
+ ($($expr:tt)*) => {
+ $crate::tokio_wrapper::tokio_run(async move { $($expr)* })
+ };
+}