From 7879ac01b24eb9723ec0a814adaee1fc9c52610a Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 18 Jun 2026 04:40:25 +0800 Subject: 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. --- Cargo.lock | 169 +++++++++++++++++++++ rola-bucket/src/protocol.rs | 3 + rola-bucket/src/protocol/placeholder.rs | 45 ++++++ rola-cli/Cargo.toml | 17 +++ rola-cli/locales/errors/i18n_io_error.toml | 85 +++++++++++ rola-cli/locales/helps/basic.toml | 9 ++ rola-cli/locales/i18n_bucket_manager.toml | 25 +++ rola-cli/src/bin/rola.rs | 64 +++++++- rola-cli/src/bucket_mgr.rs | 2 + rola-cli/src/bucket_mgr/creation.rs | 119 +++++++++++++++ rola-cli/src/error.rs | 2 + rola-cli/src/error/io.rs | 236 +++++++++++++++++++++++++++++ rola-cli/src/lib.rs | 42 +++++ rola-cli/src/res.rs | 1 + rola-cli/src/res/current_dir.rs | 11 ++ rola-cli/src/tokio_wrapper.rs | 48 ++++++ 16 files changed, 877 insertions(+), 1 deletion(-) create mode 100644 rola-bucket/src/protocol/placeholder.rs create mode 100644 rola-cli/locales/errors/i18n_io_error.toml create mode 100644 rola-cli/locales/helps/basic.toml create mode 100644 rola-cli/locales/i18n_bucket_manager.toml create mode 100644 rola-cli/src/bucket_mgr.rs create mode 100644 rola-cli/src/bucket_mgr/creation.rs create mode 100644 rola-cli/src/error.rs create mode 100644 rola-cli/src/error/io.rs create mode 100644 rola-cli/src/res.rs create mode 100644 rola-cli/src/res/current_dir.rs create mode 100644 rola-cli/src/tokio_wrapper.rs diff --git a/Cargo.lock b/Cargo.lock index 6347833..c3f4755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,69 @@ # It is not intended for manual editing. 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" @@ -48,8 +105,13 @@ dependencies = [ name = "rola-cli" version = "0.1.0" dependencies = [ + "mingling", + "rorolala", + "shakehand", "shared_functions", "shared_macros", + "space-system", + "tokio", ] [[package]] @@ -66,6 +128,57 @@ dependencies = [ "shared_macros", ] +[[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" @@ -90,6 +203,12 @@ dependencies = [ "syn", ] +[[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" @@ -162,8 +281,58 @@ dependencies = [ "syn", ] +[[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, + ) -> Result<(), super::BucketTransferProtocolError> { + unreachable!() + } + + fn download_from_bucket( + &self, + _req: &super::DownloadFromBucketRequest, + ) -> Result<(), super::BucketTransferProtocolError> { + unreachable!() + } + + fn transfer_to_client( + &self, + _req: &super::TransferToClientRequest, + ) -> Result<(), super::BucketTransferProtocolError> { + unreachable!() + } + + fn receive_from_client( + &self, + _req: &super::ReceiveFromClientRequest, + ) -> 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 ` +""" + +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) { + 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::((), + 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::>::new(Bucket::::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 for ErrorIo { + fn from(err: std::io::Error) -> Self { + ErrorIo::Error(err) + } +} + +impl From 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 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(future: F) -> T +where + F: Future, +{ + // 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)* }) + }; +} -- cgit