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. --- 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 ++++++++ 9 files changed, 524 insertions(+), 1 deletion(-) 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 (limited to 'rola-cli/src') 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