diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-06-18 20:56:05 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-18 20:56:05 +0800 |
| commit | 68daa10abfe3015beca966825d32cf67c9f5d5d7 (patch) | |
| tree | c28f0470240e7cdc3748cee57ef74353514c47b7 | |
| parent | 669898193bebeadc975881bee496fe0239df76a0 (diff) | |
feat(bucket): implement bucket initialization and logging infrastructure
Add bucket init logic with directory structure creation and log macros
for tracing
25 files changed, 1298 insertions, 23 deletions
@@ -3,3 +3,5 @@ rola-desktop/obj rola-desktop/bin rola-devtools/scripts/last_check + +drafts.txt @@ -3,6 +3,15 @@ version = 4 [[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -12,6 +21,56 @@ dependencies = [ ] [[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -53,12 +112,50 @@ dependencies = [ ] [[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys", +] + +[[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -135,6 +232,36 @@ dependencies = [ ] [[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jiff" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "js-sys" version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -224,12 +351,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -248,9 +396,39 @@ dependencies = [ ] [[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] name = "rola-bucket" version = "0.1.0" dependencies = [ + "log", "shared_constants", "shared_functions", "shared_macros", @@ -264,6 +442,9 @@ name = "rola-cli" version = "0.1.0" dependencies = [ "chrono", + "colored", + "env_logger", + "log", "mingling", "rorolala", "shakehand", @@ -356,6 +537,7 @@ dependencies = [ name = "shared_functions" version = "0.1.0" dependencies = [ + "log", "tokio", ] @@ -506,6 +688,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] name = "wasm-bindgen" version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -610,6 +798,15 @@ dependencies = [ ] [[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -38,6 +38,7 @@ rola-draft = { path = "rola-draft" } thiserror = "2.0.18" just_fmt = "0.1.2" +log = "0.4.32" [workspace.dependencies.tokio] version = "1.52.3" diff --git a/rola-bucket/Cargo.toml b/rola-bucket/Cargo.toml index 07c555e..61d7940 100644 --- a/rola-bucket/Cargo.toml +++ b/rola-bucket/Cargo.toml @@ -14,3 +14,4 @@ space-system.workspace = true thiserror.workspace = true tokio.workspace = true +log.workspace = true diff --git a/rola-bucket/res/bucket.toml b/rola-bucket/res/bucket.toml new file mode 100644 index 0000000..01abd07 --- /dev/null +++ b/rola-bucket/res/bucket.toml @@ -0,0 +1 @@ +[bucket] diff --git a/rola-bucket/src/bucket/init.rs b/rola-bucket/src/bucket/init.rs index 8b13789..2733a37 100644 --- a/rola-bucket/src/bucket/init.rs +++ b/rola-bucket/src/bucket/init.rs @@ -1 +1,51 @@ +use std::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, + }, + common::FILE_BUCKET_ROOT_CONFIG, +}; +use space_system::SpaceError; +use tokio::fs; + +pub(crate) async fn init_bucket_at(path: PathBuf) -> Result<(), SpaceError> { + let bucket_config_file = path.join(FILE_BUCKET_ROOT_CONFIG); + + // Check if directory is empty + let mut dir_entries = std::fs::read_dir(&path).map_err(SpaceError::Io)?; + if dir_entries.next().is_some() { + return Err(SpaceError::RequireEmptyDirectory); + } + + write_config(&bucket_config_file).await?; + create_dirs(&bucket_config_file).await?; + + 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) +} + +async fn create_dirs(bucket_config_file: &Path) -> Result<(), SpaceError> { + let dirs = [ + DIR_BUCKET_OBJ, + DIR_BUCKET_COMPRESSED_OBJ, + DIR_BUCKET_DELTA, + DIR_BUCKET_ID_REVS, + DIR_BUCKET_ID_TAGS, + ]; + + for dir in dirs { + let full_path = bucket_config_file.join(dir); + fs::create_dir_all(&full_path) + .await + .map_err(SpaceError::Io)?; + } + + Ok(()) +} diff --git a/rola-bucket/src/bucket/space.rs b/rola-bucket/src/bucket/space.rs index 9559b1d..ed1311c 100644 --- a/rola-bucket/src/bucket/space.rs +++ b/rola-bucket/src/bucket/space.rs @@ -1,22 +1,20 @@ -use shared_constants::common::DRAFT_META_DIR; -use space_system::{SpaceError, SpaceRoot, SpaceRootFindPattern}; -use tokio::fs::create_dir_all; +// use log::trace; +use shared_constants::common::FILE_BUCKET_ROOT_CONFIG; +use shared_functions::trace; +use space_system::{SpaceRoot, SpaceRootFindPattern}; -use crate::{AsyncBucketTransferProtocol, Bucket}; +use crate::{AsyncBucketTransferProtocol, Bucket, bucket::init::init_bucket_at}; impl<Protocol: AsyncBucketTransferProtocol + Send + Sync> SpaceRoot for Bucket<Protocol> { fn get_pattern() -> SpaceRootFindPattern { - SpaceRootFindPattern::IncludeDotDir(DRAFT_META_DIR.into()) + SpaceRootFindPattern::IncludeFile(FILE_BUCKET_ROOT_CONFIG.into()) } async fn create_space(path: &std::path::Path) -> Result<(), space_system::SpaceError> { - let draft_meta_dir = path.join(DRAFT_META_DIR); - - // Create workspace directory - create_dir_all(&draft_meta_dir) - .await - .map_err(SpaceError::from)?; - + let path_str = path.display().to_string(); + trace!("Creating bucket at: {}", &path_str); + init_bucket_at(path.into()).await?; + trace!("Bucket created at: {}", &path_str); Ok(()) } } diff --git a/rola-cli/Cargo.toml b/rola-cli/Cargo.toml index 6b45c5a..0ed9a3b 100644 --- a/rola-cli/Cargo.toml +++ b/rola-cli/Cargo.toml @@ -15,6 +15,10 @@ space-system.workspace = true tokio.workspace = true +colored = "3.1.1" +chrono = "0.4.45" +env_logger = "0.11.10" +log = "0.4.32" shakehand = "0.1.3" [dependencies.mingling] diff --git a/rola-cli/locales/helps/basic.toml b/rola-cli/locales/helps/basic.toml index 656cdc4..ff0dfd1 100644 --- a/rola-cli/locales/helps/basic.toml +++ b/rola-cli/locales/helps/basic.toml @@ -1,9 +1,182 @@ [en] help = """ -NO YET +[[b_blue]]**Usage**[[/]]: **rola** [-v | --version] [-h | --help] [-L | --lang *<LANG>*] +__ **OUTPUT CONTROL:** +__ [--silence | --quiet] [--no-error] [--no-result] +__ [--no-color] [--json | --json-pretty] +__ **LOGS:** +__ [-V | --verbose] [--log-time] +__ [--log-level _disable_ | [[gray]]_trace_[[/]] | [[b_cyan]]_debug_[[/]] | [[b_yellow]]_warn_[[/]] | [[b_red]]_error_[[/]]] +__ **CONTEXT:** +__ [--bucket-dir *<DIR>*] [--draft-dir *<DIR>*] [--dir *<CWD>*] +__ **BEHAVIOUR CONTROL:** +__ [[b_yellow]]**[-y | --yes]**[[/]] [[b_red]]**[-O | --overwrite]**[[/]] [--no-pager] """ [zh_CN] help = """ -暂无 +[[b_blue]]**用法**[[/]]:**rola** [-v | --version] [-h | --help] [-L | --lang *<LANG>*] +__ **输出控制:** +__ [--silence | --quiet] [--no-error] [--no-result] +__ [--no-color] [--json | --json-pretty] +__ **日志:** +__ [-V | --verbose] [--log-time] +__ [--log-level _disable_ | [[gray]]_trace_[[/]] | [[b_cyan]]_debug_[[/]] | [[b_yellow]]_warn_[[/]] | [[b_red]]_error_[[/]]] +__ **上下文:** +__ [--bucket-dir *<DIR>*] [--draft-dir *<DIR>*] [--dir *<CWD>*] +__ **行为控制:** +__ [[b_yellow]]**[-y | --yes]**[[/]] [[b_red]]**[-O | --overwrite]**[[/]] [--no-pager] """ + + +# 全局: +# --draft-dir 草稿路径 +# --bucket-dir 桶路径 +# --dir 路径 +# -V --version +# -v --verbose +# -h --help +# -L --lang --language +# --json --json-pretty +# --no-color --no-pager +# --no-error --only-error +# --quiet / --silence +# -y / --yes +# --overwrite + +# # 创建草稿,临时视图 +# rola create --draft +# rola init --draft + +# # 创建桶,存储库,远程真相 +# rola create --bucket +# rola init --bucket + +# # 仅 draft 中命令 +# # 桶 +# rola bucket origin --bind-to-url url +# rola bucket origin --delete / -D +# rola bucket origin --rename-to other +# rola bucket # 列出 +# rola bucket origin # 显示元数据 + +# # 检查连接 +# rola test-connection --url url +# rola test-connection --bucket origin + +# # 信息同步 +# rola update +# rola update --all + +# # 视图 +# rola view # 列出所有视图 +# rola view THIS # 当前视图 +# rola view origin/main # 主视图 +# rola view --new other # 新建视图 +# rola view --clone other # 克隆视图 +# rola view --checkout-clone other # 新建视图并直接切换 +# rola view --checkout-new other # 新建视图并直接切换 +# rola view --checkout other # 切换 +# rola view origin --rename-to other # 命名 +# rola view --forget origin # 忘记视图 +# rola view origin/main --track-to main # 跟踪 +# rola view main --track-to origin/main # 跟踪 +# rola view main --break-track # 断开跟踪 +# rola view --show-track # 展示跟踪状态 +# rola view main --show-track # 展示跟踪状态 +# rola view main --up-track # 同步视图 上传 (只提醒使用--overwrite-up-track) +# rola view main --overwrite-up-track # 同步视图 上传 +# rola view main --down-track # 同步视图 下载 + +# # 状态 +# rola status --view main # 检查视图状态 (等效 rola view main) +# rola status # 检查草稿状态(如果在draft) +# rola status # 检查桶状态(如果在bucket) +# rola status --file ./file # 检查文件状态 +# rola status --file . # 检查多个文件状态 +# rola status --bucket origin # 检查远程桶状态 (等效 rola bucket origin) +# rola status --bucket-url url # 检查url状态 (等效 rola test-connection --url url) +# rola status --align task # 检查对齐状态 (等效 rola align task) +# rola status --align-task # 检查所有对齐状态 (等效 rola align) + +# # 实际版控 +# rola set-forward ./my.file --follow latest # 将文件总是追踪最新版本 +# rola set-forward ./my.file --follow ver:16 # 锁定到16版本 +# rola set-forward ./my.file --follow origin/main # 跟随某个view的版本 + +# rola track . # 跟踪文件 +# 为每个文件判断: +# 在 forward 为 latest 时 +# 1. 本地版本同步,且已修改:尝试上传(可能冲突) +# 2. 本地版本不同步,且未修改:下载 +# 3. 本地版本不同步,且已修改:阻止!(使用 --overwrite 覆盖) +# 4. 在 3 的情况下,使用 --overwrite 覆盖本地(或显式使用 --down --overwrite) +# 5. 在 3 的情况下,使用 --up --overwrite 覆盖远程(强制提交) +# 在 forward 为 ver 时 +# 1. 本地版本同步 ver,且已修改:阻止!(使用 --overwrite 覆盖) +# 2. 本地版本不同步 ver,且未修改:下载 +# 3. 本地版本同步 ver,未修改,什么都不做 +# 在 forward 为 view 时 +# 1. 如果view中没有该文件,阻止!(使用 --allow-non-forward) +# 2. 如果view中没有该文件,且使用 --allow-non-forward:不做任何事 +# 3. 如果view存在该文件,将view中该文件的版本视作ver,同(在 forward 为 ver 时)的行为 + +# 显式模式: +# --up --down 显式指定上传或下载,若状态不一,必须使用 --overwrite + +# # 对齐 +# rola align # 查看所有本地 view 和实际结构不一致的情况(移动、新增、丢失) +# rola align 移动项 --connect 丢失项 +# rola align 丢失项 --connect 移动项 # 匹配为移动 +# rola align 移动项 --confirm # 确认移动 +# rola align 新增项 --confirm # 确认新增 +# rola align 丢失项 --confirm # 确认丢失 (为删除) +# rola align --confirm-all # 全部确认(谨慎!) +# rola align 移动项 --break # 断开为丢失和新增 +# rola align 项 # 查看项 + +# # view 之间操作 (--overwrite 覆写) +# rola send 文件 --to-view view # 将该文件发送到view(同当前view的位置) +# rola send 文件 --to-view view --path # 将文件发送到view(指定位置) +# rola send view:文件 --to-view ... # 使用指定view的文件 +# rola send view:文件 --to-view THIS # 从其他view拉取文件到此处 +# rola send bucket/view:文件 --to-view THIS # 从远程view拉取文件到此处 + +# # 直接 view-tree 操作(危险的修复行为) +# rola op-view-tree rm path # 删除某条路径记录 +# rola op-view-tree add path --id 文件id # 新增某条路径记录 +# rola op-view-tree set path --forward xx --id xx # 修改某条路径记录 +# rola op-view-tree mv path ... path2 # 移动某条路径记录 +# rola op-view-tree cp path ... path2 # 移动某条路径记录 +# rola stack view1 view2 view3 ... --clone-into view4 (将view1 view2 view3的视图堆叠成一个视图,放置到view4) +# rola stack ... --into view4 (同上,但是会删除旧视图) +# rola op-idmap ls # 列出所有 id map +# rola op-idmap ls id # 列出关于该 id 的映射 +# rola op-idmap ls --remote id # 反向查找:通过远程 id 查找本地 id +# rola op-idmap write 12 --to-remote 25 # 将 12 号 id 映射到 远程 id 25 +# rola op-idmap clean 12 # 将 12 号 id 映射移除 +# rola op-idmap clean --remote 12 # 将映射到远程12号id的本地id移除 + +# # 查询操作 +# rola query bind-buckets +# rola query views +# rola query idmaps +# rola query objects + +# # 视图查看 +# rola ls # 等效别名 (rls):展示当前目录的元数据(富有rola元数据的ls命令) +# rola tree # 等效别名 (rtree): 展示当前目录的树 + +# # GUI +# rola desktop --install # 安装 RorolalaVCS - Dashboard +# rola desktop # 启动 RorolalaVCS - Dashboard + +# # 更新 +# rola source show # 展示远程更新源 +# rola source update-info # 更新远程信息 +# rola source update # 更新rola +# rola source change 源 # 切换更新源 +# rola source show-info # 查询更新信息 + +# # 交互式 +# rola op-view-tree --shell-mode # 进入 REPL 直接使用 cd ls rm add set mv 操作 tree diff --git a/rola-cli/src/bin/rola.rs b/rola-cli/src/bin/rola.rs index 0cfe675..a201953 100644 --- a/rola-cli/src/bin/rola.rs +++ b/rola-cli/src/bin/rola.rs @@ -5,7 +5,10 @@ use mingling::{ macros::program_setup, setup::{ExitCodeSetup, HelpFlagSetup, QuietFlagSetup}, }; -use rola_cli::{ThisProgram, locale, res::current_dir::ResCurrentDir}; +use rola_cli::{ + ThisProgram, locale, output::ColorOutputSetup, output::EnvLoggerSetup, + res::current_dir::ResCurrentDir, +}; fn main() { let mut program = ThisProgram::new(); @@ -32,11 +35,18 @@ fn main() { program.with_setup(HelpFlagSetup::new(["-h", "--help"])); program.with_setup(StandardOutputSetup); program.with_setup(ExitCodeSetup::default()); + program.with_setup(ColorOutputSetup); - // Execute + // stdout/stderr control let quiet = program.stdout_setting.quiet; let error_output = program.stdout_setting.error_output && !quiet; let render_output = program.stdout_setting.render_output && !quiet; + + if error_output { + program.with_setup(EnvLoggerSetup); + } + + // Execute let result = program.exec_without_render().unwrap(); if !result.is_empty() { if result.exit_code == 0 && render_output { diff --git a/rola-cli/src/error/io.rs b/rola-cli/src/error/io.rs index 7e4de56..d65b765 100644 --- a/rola-cli/src/error/io.rs +++ b/rola-cli/src/error/io.rs @@ -61,7 +61,14 @@ pub enum ErrorIo { 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() { + let (error_info, exit_code) = io_error_info(err, content); + + r_println!("{}{}", I18nIoError::io_error_name(), error_info); + ec.exit_code = exit_code; +} + +fn io_error_info(err: std::io::Error, content: String) -> (String, i32) { + match err.kind() { std::io::ErrorKind::NotFound => (I18nIoError::not_found(content), EC_IOERR_NOT_FOUND), std::io::ErrorKind::PermissionDenied => ( I18nIoError::permission_denied(content), @@ -185,10 +192,7 @@ pub fn render_error_io(err: ErrorIo, ec: &mut ResExitCode) { } 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 { diff --git a/rola-cli/src/lib.rs b/rola-cli/src/lib.rs index 54ff09a..b3587e2 100644 --- a/rola-cli/src/lib.rs +++ b/rola-cli/src/lib.rs @@ -2,6 +2,7 @@ use std::process::exit; use mingling::macros::{gen_program, help}; +pub mod output; pub mod res; pub mod tokio_wrapper; @@ -11,12 +12,14 @@ use bucket_mgr::*; mod error; use error::*; +use crate::output::display::markdown; + #[help] fn handle_error_dispatch_not_found(_err: ErrorDispatcherNotFound) { let help = locale::helps::Basic::help().trim(); // Print directly to stderr and exit with code 0 - eprintln!("{}", help); + eprintln!("{}", markdown(help)); exit(0) } diff --git a/rola-cli/src/output.rs b/rola-cli/src/output.rs new file mode 100644 index 0000000..65cafed --- /dev/null +++ b/rola-cli/src/output.rs @@ -0,0 +1,6 @@ +pub mod ansi_control; +pub mod display; +pub mod env_logger; + +mod setup; +pub use setup::*; diff --git a/rola-cli/src/output/ansi_control.rs b/rola-cli/src/output/ansi_control.rs new file mode 100644 index 0000000..5db8c2a --- /dev/null +++ b/rola-cli/src/output/ansi_control.rs @@ -0,0 +1,32 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +mod colorize_wrapper; +pub use colorize_wrapper::*; + +/// Global flag controlling whether ANSI color output is enabled. +static ANSI_ENABLED: AtomicBool = AtomicBool::new(true); + +/// Enable ANSI color codes in output. +pub fn enable_ansi() { + ANSI_ENABLED.store(true, Ordering::Relaxed); +} + +/// Disable ANSI color codes in output. +pub fn disable_ansi() { + ANSI_ENABLED.store(false, Ordering::Relaxed); +} + +/// Set ANSI color output to the specified state (`true` to enable, `false` to disable). +pub fn set_ansi(enabled: bool) { + ANSI_ENABLED.store(enabled, Ordering::Relaxed); +} + +/// Check whether ANSI color output is currently enabled. +pub fn is_ansi_enabled() -> bool { + ANSI_ENABLED.load(Ordering::Relaxed) +} + +/// Returns `true` if ANSI is enabled, `false` otherwise. +pub fn is_enabled() -> bool { + is_ansi_enabled() +} diff --git a/rola-cli/src/output/ansi_control/colorize_wrapper.rs b/rola-cli/src/output/ansi_control/colorize_wrapper.rs new file mode 100644 index 0000000..4c92f66 --- /dev/null +++ b/rola-cli/src/output/ansi_control/colorize_wrapper.rs @@ -0,0 +1,209 @@ +use colored::ColoredString; +use colored::Colorize as _ColoredColorize; + +use super::is_ansi_enabled; + +macro_rules! if_ansi { + ($self:expr, $method:ident $(, $arg:expr)*) => {{ + if is_ansi_enabled() { + _ColoredColorize::$method($self $(, $arg)*) + } else { + _ColoredColorize::clear($self) + } + }}; +} + +/// A drop-in wrapper for [`colored::Colorize`] that respects the global ANSI +/// output flag controlled by [`enable_ansi()`](super::enable_ansi) / +/// [`disable_ansi()`](super::disable_ansi). +/// +/// When ANSI is **disabled**, all colorization methods return a plain +/// [`ColoredString`] with no foreground/background color or style — +/// effectively a no-op. +/// +/// Import this trait instead of `colored::Colorize` to automatically honour +/// the `--no-color` / `--quiet` flags set by the CLI. +/// +/// # Example +/// +/// ```ignore +/// use rola_cli::res::ansi_control::Colorize; +/// +/// println!("{}", "error".red().bold()); +/// // ^ ANSI on → red bold text +/// // ^ ANSI off → plain "error" +/// ``` +pub trait Colorize: _ColoredColorize { + fn black(self) -> ColoredString { + if_ansi!(self, black) + } + fn red(self) -> ColoredString { + if_ansi!(self, red) + } + fn green(self) -> ColoredString { + if_ansi!(self, green) + } + fn yellow(self) -> ColoredString { + if_ansi!(self, yellow) + } + fn blue(self) -> ColoredString { + if_ansi!(self, blue) + } + fn magenta(self) -> ColoredString { + if_ansi!(self, magenta) + } + fn purple(self) -> ColoredString { + if_ansi!(self, purple) + } + fn cyan(self) -> ColoredString { + if_ansi!(self, cyan) + } + fn white(self) -> ColoredString { + if_ansi!(self, white) + } + + fn bright_black(self) -> ColoredString { + if_ansi!(self, bright_black) + } + fn bright_red(self) -> ColoredString { + if_ansi!(self, bright_red) + } + fn bright_green(self) -> ColoredString { + if_ansi!(self, bright_green) + } + fn bright_yellow(self) -> ColoredString { + if_ansi!(self, bright_yellow) + } + fn bright_blue(self) -> ColoredString { + if_ansi!(self, bright_blue) + } + fn bright_magenta(self) -> ColoredString { + if_ansi!(self, bright_magenta) + } + fn bright_purple(self) -> ColoredString { + if_ansi!(self, bright_purple) + } + fn bright_cyan(self) -> ColoredString { + if_ansi!(self, bright_cyan) + } + fn bright_white(self) -> ColoredString { + if_ansi!(self, bright_white) + } + + fn truecolor(self, r: u8, g: u8, b: u8) -> ColoredString { + if_ansi!(self, truecolor, r, g, b) + } + + fn color<C: Into<colored::Color>>(self, color: C) -> ColoredString { + if is_ansi_enabled() { + _ColoredColorize::color(self, color) + } else { + _ColoredColorize::clear(self) + } + } + + fn on_black(self) -> ColoredString { + if_ansi!(self, on_black) + } + fn on_red(self) -> ColoredString { + if_ansi!(self, on_red) + } + fn on_green(self) -> ColoredString { + if_ansi!(self, on_green) + } + fn on_yellow(self) -> ColoredString { + if_ansi!(self, on_yellow) + } + fn on_blue(self) -> ColoredString { + if_ansi!(self, on_blue) + } + fn on_magenta(self) -> ColoredString { + if_ansi!(self, on_magenta) + } + fn on_purple(self) -> ColoredString { + if_ansi!(self, on_purple) + } + fn on_cyan(self) -> ColoredString { + if_ansi!(self, on_cyan) + } + fn on_white(self) -> ColoredString { + if_ansi!(self, on_white) + } + + fn on_bright_black(self) -> ColoredString { + if_ansi!(self, on_bright_black) + } + fn on_bright_red(self) -> ColoredString { + if_ansi!(self, on_bright_red) + } + fn on_bright_green(self) -> ColoredString { + if_ansi!(self, on_bright_green) + } + fn on_bright_yellow(self) -> ColoredString { + if_ansi!(self, on_bright_yellow) + } + fn on_bright_blue(self) -> ColoredString { + if_ansi!(self, on_bright_blue) + } + fn on_bright_magenta(self) -> ColoredString { + if_ansi!(self, on_bright_magenta) + } + fn on_bright_purple(self) -> ColoredString { + if_ansi!(self, on_bright_purple) + } + fn on_bright_cyan(self) -> ColoredString { + if_ansi!(self, on_bright_cyan) + } + fn on_bright_white(self) -> ColoredString { + if_ansi!(self, on_bright_white) + } + + fn on_truecolor(self, r: u8, g: u8, b: u8) -> ColoredString { + if_ansi!(self, on_truecolor, r, g, b) + } + + fn on_color<C: Into<colored::Color>>(self, color: C) -> ColoredString { + if is_ansi_enabled() { + _ColoredColorize::on_color(self, color) + } else { + _ColoredColorize::clear(self) + } + } + + fn clear(self) -> ColoredString { + if_ansi!(self, clear) + } + fn normal(self) -> ColoredString { + if_ansi!(self, normal) + } + fn bold(self) -> ColoredString { + if_ansi!(self, bold) + } + fn dimmed(self) -> ColoredString { + if_ansi!(self, dimmed) + } + fn italic(self) -> ColoredString { + if_ansi!(self, italic) + } + fn underline(self) -> ColoredString { + if_ansi!(self, underline) + } + fn blink(self) -> ColoredString { + if_ansi!(self, blink) + } + #[allow(deprecated)] + fn reverse(self) -> ColoredString { + if_ansi!(self, reverse) + } + fn reversed(self) -> ColoredString { + if_ansi!(self, reversed) + } + fn hidden(self) -> ColoredString { + if_ansi!(self, hidden) + } + fn strikethrough(self) -> ColoredString { + if_ansi!(self, strikethrough) + } +} + +impl<T: _ColoredColorize> Colorize for T {} diff --git a/rola-cli/src/output/display.rs b/rola-cli/src/output/display.rs new file mode 100644 index 0000000..0845456 --- /dev/null +++ b/rola-cli/src/output/display.rs @@ -0,0 +1,384 @@ +use crate::output::ansi_control::Colorize; +use std::collections::VecDeque; + +/// Trait for adding markdown formatting to strings +pub trait Markdown { + fn markdown(&self) -> String; +} + +impl Markdown for &str { + fn markdown(&self) -> String { + markdown(self) + } +} + +impl Markdown for String { + fn markdown(&self) -> String { + markdown(self) + } +} + +/// Converts a string to colored/formatted text with ANSI escape codes. +/// +/// Supported syntax: +/// - Bold: `**text**` +/// - Italic: `*text*` +/// - Underline: `_text_` +/// - Angle-bracketed content: `<text>` (displayed as cyan) +/// - Inline code: `` `text` `` (displayed as green) +/// - Color tags: `[[color_name]]` and `[[/]]` to close color +/// - Escape characters: `\*`, `\<`, `\>`, `` \` ``, `\_` for literal characters +/// - Headings: `# Heading 1`, `## Heading 2`, up to `###### Heading 6` +/// - Blockquote: `> text` (displays a gray background marker at the beginning of the line) +/// +/// Color tags support the following color names: +/// Color tags support the following color names: +/// +/// | Type | Color Names | +/// |-------------------------|-----------------------------------------------------------------------------| +/// | Standard colors | `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` | +/// | Bright colors | `bright_black` | +/// | | `bright_red` | +/// | | `bright_green` | +/// | | `bright_yellow` | +/// | | `bright_blue` | +/// | | `bright_magenta` | +/// | | `bright_cyan` | +/// | | `bright_white` | +/// | Bright color shorthands | `b_black` | +/// | | `b_red` | +/// | | `b_green` | +/// | | `b_yellow` | +/// | | `b_blue` | +/// | | `b_magenta` | +/// | | `b_cyan` | +/// | | `b_white` | +/// | Gray colors | `gray`/`grey` | +/// | | `bright_gray`/`bright_grey` | +/// | | `b_gray`/`b_grey` | +/// +/// Color tags can be nested, `[[/]]` will close the most recently opened color tag. +/// +/// # Arguments +/// * `text` - The text to format, can be any type that implements `AsRef<str>` +/// +/// # Returns +/// Returns a `String` containing ANSI escape codes that can display colored/formatted text in ANSI-supported terminals. +/// +/// # Examples +/// ``` +/// # use mingling_cli::display::markdown; +/// let formatted = markdown("Hello **world**!"); +/// println!("{}", formatted); +/// +/// let colored = markdown("[[red]]Red text[[/]] and normal text"); +/// println!("{}", colored); +/// +/// let nested = markdown("[[blue]]Blue [[green]]Green[[/]] Blue[[/]] normal"); +/// println!("{}", nested); +/// ``` +pub fn markdown(text: impl AsRef<str>) -> String { + let text = text.as_ref(); + let lines: Vec<&str> = text.lines().collect(); + let mut result = String::new(); + let mut content_indent = 0; + + for line in lines { + // Don't trim the line initially, we need to check if it starts with # + let trimmed_line = line.trim(); + let mut line_result = String::new(); + + // Check if line starts with # for heading + // Check if the original line (not trimmed) starts with # + if line.trim_start().starts_with('#') { + let chars: Vec<char> = line.trim_start().chars().collect(); + let mut level = 0; + + // Count # characters at the beginning + while level < chars.len() && level < 7 && chars[level] == '#' { + level += 1; + } + + // Cap level at 6 + let effective_level = if level > 6 { 6 } else { level }; + + // Skip # characters and any whitespace after them + let mut content_start = level; + while content_start < chars.len() && chars[content_start].is_whitespace() { + content_start += 1; + } + + // Extract heading content + let heading_content: String = if content_start < chars.len() { + chars[content_start..].iter().collect() + } else { + String::new() + }; + + // Process the heading content with formatting + let processed_content = process_line(&heading_content); + + // Format heading as white background, black text, bold + // ANSI codes: \x1b[1m for bold, \x1b[47m for white background, \x1b[30m for black text + let formatted_heading = format!("\x1b[1m\x1b[47m\x1b[30m {processed_content} \x1b[0m"); + + // Add indentation to the heading line itself + // Heading indentation = level - 1 + let heading_indent = if effective_level > 0 { + effective_level - 1 + } else { + 0 + }; + let indent = " ".repeat(heading_indent); + line_result.push_str(&indent); + line_result.push_str(&formatted_heading); + + // Update content indent level for subsequent content + // Content after heading should be indented by effective_level + content_indent = effective_level; + } else if !trimmed_line.is_empty() { + // Process regular line with existing formatting + let processed_line = process_line_with_quote(trimmed_line); + + // Add indentation based on content_indent + let indent = " ".repeat(content_indent); + line_result.push_str(&indent); + line_result.push_str(&processed_line); + } else { + line_result.push(' '); + } + + if !line_result.is_empty() { + result.push_str(&line_result); + result.push('\n'); + } + } + + // Remove trailing newline + if result.ends_with('\n') { + result.pop(); + } + + result +} + +// Helper function to process a single line with existing formatting and handle > quotes +fn process_line_with_quote(line: &str) -> String { + let chars: Vec<char> = line.chars().collect(); + + // Check if line starts with '>' and not escaped + if !chars.is_empty() && chars[0] == '>' { + // Check if it's escaped + if chars.len() > 1 && chars[1] == '\\' { + // It's \>, so treat as normal text starting from position 0 + return process_line(line); + } + + // It's a regular > at the beginning, replace with gray background gray text space + let gray_bg_space = "\x1b[48;5;242m\x1b[38;5;242m \x1b[0m"; + let rest_of_line = if chars.len() > 1 { + chars[1..].iter().collect::<String>() + } else { + String::new() + }; + + // Process the rest of the line normally + let processed_rest = process_line(&rest_of_line); + + // Combine the gray background space with the processed rest + format!("{gray_bg_space}{processed_rest}") + } else { + // No > at the beginning, process normally + process_line(line) + } +} + +// Helper function to process a single line with existing formatting +fn process_line(line: &str) -> String { + let mut result = String::new(); + let mut color_stack: VecDeque<String> = VecDeque::new(); + + let chars: Vec<char> = line.chars().collect(); + let mut i = 0; + + while i < chars.len() { + // Check for escape character \ + if chars[i] == '\\' && i + 1 < chars.len() { + let escaped_char = chars[i + 1]; + // Only escape specific characters + if matches!(escaped_char, '*' | '<' | '>' | '`' | '_') { + let mut escaped_text = escaped_char.to_string(); + apply_color_stack(&mut escaped_text, &color_stack); + result.push_str(&escaped_text); + i += 2; + continue; + } + } + + // Check for color tag start [[color]] + if i + 1 < chars.len() + && chars[i] == '[' + && chars[i + 1] == '[' + && let Some(end) = find_tag_end(&chars, i) + { + let tag_content: String = chars[i + 2..end].iter().collect(); + + // Check if it's a closing tag [[/]] + if tag_content == "/" { + color_stack.pop_back(); + } else { + // It's a color tag + color_stack.push_back(tag_content.clone()); + } + i = end + 2; + continue; + } + + // Check for bold **text** + if i + 1 < chars.len() + && chars[i] == '*' + && chars[i + 1] == '*' + && let Some(end) = find_matching(&chars, i + 2, "**") + { + let bold_text: String = chars[i + 2..end].iter().collect(); + let mut formatted_text = bold_text.bold().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 2; + continue; + } + + // Check for italic *text* + if chars[i] == '*' + && let Some(end) = find_matching(&chars, i + 1, "*") + { + let italic_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = italic_text.italic().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Check for underline _text_ + if chars[i] == '_' + && let Some(end) = find_matching(&chars, i + 1, "_") + { + let underline_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = format!("\x1b[4m{underline_text}\x1b[0m"); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Check for angle-bracketed content <text> + if chars[i] == '<' + && let Some(end) = find_matching(&chars, i + 1, ">") + { + // Include the angle brackets in the output + let angle_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = angle_text.cyan().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Check for inline code `text` + if chars[i] == '`' + && let Some(end) = find_matching(&chars, i + 1, "`") + { + // Include the backticks in the output + let code_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = code_text.green().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Regular character + let mut current_char = chars[i].to_string(); + apply_color_stack(&mut current_char, &color_stack); + result.push_str(¤t_char); + i += 1; + } + + result +} + +// Helper function to find matching delimiter +fn find_matching(chars: &[char], start: usize, delimiter: &str) -> Option<usize> { + let delim_chars: Vec<char> = delimiter.chars().collect(); + let delim_len = delim_chars.len(); + + let mut j = start; + while j < chars.len() { + if delim_len == 1 { + if chars[j] == delim_chars[0] { + return Some(j); + } + } else if j + 1 < chars.len() + && chars[j] == delim_chars[0] + && chars[j + 1] == delim_chars[1] + { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to find color tag end +fn find_tag_end(chars: &[char], start: usize) -> Option<usize> { + let mut j = start + 2; + while j + 1 < chars.len() { + if chars[j] == ']' && chars[j + 1] == ']' { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to apply color stack to text +fn apply_color_stack(text: &mut String, color_stack: &VecDeque<String>) { + let mut result = text.clone(); + for color in color_stack.iter().rev() { + result = apply_color(&result, color); + } + *text = result; +} + +// Helper function to apply color to text +fn apply_color(text: impl AsRef<str>, color_name: impl AsRef<str>) -> String { + let text = text.as_ref(); + let color_name = color_name.as_ref(); + match color_name { + // Normal colors + "black" => text.black().to_string(), + "red" => text.red().to_string(), + "green" => text.green().to_string(), + "yellow" => text.yellow().to_string(), + "blue" => text.blue().to_string(), + "magenta" => text.magenta().to_string(), + "cyan" => text.cyan().to_string(), + "white" | "b_white" | "bright_gray" | "bright_grey" | "b_gray" | "b_grey" => { + text.white().to_string() + } + + // Bright colors and their b_ short aliases + "bright_black" | "b_black" | "gray" | "grey" => text.bright_black().to_string(), + "bright_red" | "b_red" => text.bright_red().to_string(), + "bright_green" | "b_green" => text.bright_green().to_string(), + "bright_yellow" | "b_yellow" => text.bright_yellow().to_string(), + "bright_blue" | "b_blue" => text.bright_blue().to_string(), + "bright_magenta" | "b_magenta" => text.bright_magenta().to_string(), + "bright_cyan" | "b_cyan" => text.bright_cyan().to_string(), + "bright_white" => text.bright_white().to_string(), + + // Default to white if color not recognized + _ => text.to_string(), + } +} diff --git a/rola-cli/src/output/env_logger.rs b/rola-cli/src/output/env_logger.rs new file mode 100644 index 0000000..289de77 --- /dev/null +++ b/rola-cli/src/output/env_logger.rs @@ -0,0 +1,54 @@ +use chrono::Local; +use std::io::Write; + +use crate::output::ansi_control::Colorize; + +/// Simple env logger that prints formatted messages. +/// Usage: `env_logger::init_with(EnvLogger { show_time: true, show_level: true });` +pub struct EnvLogger { + pub show_time: bool, + pub show_level: bool, + pub level: log::Level, +} + +impl log::Log for EnvLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::Level::Info + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let mut buf = Vec::new(); + + if self.show_time { + let now = Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + write!(buf, "[{}] ", now).ok(); + } + + if self.show_level { + let level_str = match record.level() { + log::Level::Trace => "TRACE".bright_black(), + log::Level::Debug => "DEBUG".cyan(), + log::Level::Info => "INFO".green(), + log::Level::Warn => "WARN".yellow(), + log::Level::Error => "ERROR".red(), + }; + write!(buf, "{}: ", level_str.bold()).ok(); + } + + write!(buf, "{}", record.args()).ok(); + eprintln!("{}", String::from_utf8_lossy(&buf)); + } + + fn flush(&self) {} +} + +/// Initialize the env logger with the given configuration. +pub fn init_envlogger(config: EnvLogger) { + log::set_boxed_logger(Box::new(config)) + .map(|()| log::set_max_level(log::LevelFilter::Info)) + .ok(); +} diff --git a/rola-cli/src/output/setup.rs b/rola-cli/src/output/setup.rs new file mode 100644 index 0000000..824348b --- /dev/null +++ b/rola-cli/src/output/setup.rs @@ -0,0 +1,41 @@ +use mingling::{Program, macros::program_setup}; +use shared_functions::info; + +use crate::{ + ThisProgram, + output::{ + ansi_control::disable_ansi, + env_logger::{EnvLogger, init_envlogger}, + }, +}; + +#[program_setup] +pub fn color_output_setup(program: &mut Program<ThisProgram>) { + program.global_flag("--no-color", |_| { + disable_ansi(); + }); +} + +#[program_setup] +pub fn env_logger_setup(program: &mut Program<ThisProgram>) { + program.stdout_setting.verbose = program.pick_global_flag(["-V", "--verbose"]); + let log_show_time = program.pick_global_flag("--log-time"); + let log_level = program + .pick_global_argument("--log-level") + .unwrap_or("info".to_string()); + if program.stdout_setting.verbose { + init_envlogger(EnvLogger { + show_time: log_show_time, + show_level: log_level.as_str() != "disable", + level: match log_level.as_str() { + "trace" => log::Level::Trace, + "debug" => log::Level::Debug, + "warn" => log::Level::Warn, + "error" => log::Level::Error, + _ => log::Level::Info, + }, + }); + } + + info!("Verbose mode enabled!"); +} diff --git a/rola-utils/constants/src/bucket.rs b/rola-utils/constants/src/bucket.rs new file mode 100644 index 0000000..e2ebff2 --- /dev/null +++ b/rola-utils/constants/src/bucket.rs @@ -0,0 +1,38 @@ +#[shared_macros::constants] +mod consts { + /// Full object storage pool directory + pub const DIR_BUCKET_OBJ: &str = "./objects/"; + + /// Full compressed object storage pool directory + pub const DIR_BUCKET_COMPRESSED_OBJ: &str = "./compressed-objects/"; + + /// Incremental object fragment storage pool directory + pub const DIR_BUCKET_DELTA: &str = "./delta-objects/"; + + /// Version information storage directory + pub const DIR_BUCKET_ID_REVS: &str = "./revs/"; + + /// Tag storage directory, used to record tags for easy file location + pub const DIR_BUCKET_ID_TAGS: &str = "./tags/"; + + /// Full object file path template + pub const FILE_BUCKET_OBJ: &str = "./objects/{slice1}/{slice2}/{fullname}"; + + /// Full compressed object file path template + pub const FILE_BUCKET_COMPRESSED_OBJ: &str = + "./compressed-objects/{slice1}/{slice2}/{fullname}"; + + /// Incremental object file path template (records change info between the previous delta and the current delta) + pub const FILE_BUCKET_DELTA: &str = "./delta-objects/{slice1}/{slice2}/{fullname}"; + + /// Version information file, records the storage mode, pointed object, and its log offset for all versions of a given file ID (append-only) + pub const FILE_BUCKET_ID_REV: &str = "./revs/{file_id}.v"; + + /// Version log file, records all operation logs for a given file ID (append-only) + pub const FILE_BUCKET_ID_REV_LOG: &str = "./revs/{file_id}.log"; + + /// Tag file, internally points to a file_id + pub const FILE_BUCKET_ID_TAG: &str = "./tags/{tag_name}"; +} + +pub use consts::*; diff --git a/rola-utils/constants/src/common.rs b/rola-utils/constants/src/common.rs index 6ce6bd8..8b333e7 100644 --- a/rola-utils/constants/src/common.rs +++ b/rola-utils/constants/src/common.rs @@ -1,7 +1,10 @@ #[shared_macros::constants] mod consts { /// Directory name for Rorolala metadata storage in Workdraft - pub const DRAFT_META_DIR: &str = ".rola"; + pub const DIR_DRAFT_META: &str = ".rola"; + + /// Configuration file indicating the root directory of a bucket + pub const FILE_BUCKET_ROOT_CONFIG: &str = "bucket.toml"; } pub use consts::*; diff --git a/rola-utils/constants/src/lib.rs b/rola-utils/constants/src/lib.rs index 566440d..ab8bd4a 100644 --- a/rola-utils/constants/src/lib.rs +++ b/rola-utils/constants/src/lib.rs @@ -3,3 +3,5 @@ //! This module records all constant information for Rorolala pub mod common; + +pub mod bucket; diff --git a/rola-utils/functions/Cargo.toml b/rola-utils/functions/Cargo.toml index 564dc54..6ce5adf 100644 --- a/rola-utils/functions/Cargo.toml +++ b/rola-utils/functions/Cargo.toml @@ -7,3 +7,4 @@ license.workspace = true [dependencies] tokio.workspace = true +log.workspace = true diff --git a/rola-utils/functions/src/lib.rs b/rola-utils/functions/src/lib.rs index 72d5b9c..92b4f03 100644 --- a/rola-utils/functions/src/lib.rs +++ b/rola-utils/functions/src/lib.rs @@ -6,3 +6,5 @@ pub use levenshtein_distance::*; mod test_sandbox; pub use test_sandbox::*; + +mod log_macros; diff --git a/rola-utils/functions/src/log_macros.rs b/rola-utils/functions/src/log_macros.rs new file mode 100644 index 0000000..4ee4e68 --- /dev/null +++ b/rola-utils/functions/src/log_macros.rs @@ -0,0 +1,53 @@ +//! Custom logging macros, with the same names as `log` crate's `trace!`, `debug!`, `info!`, `warn!`, `error!`, +//! but different behavior: automatically prepend `file!()` information to the message. +//! +//! # Usage +//! ```ignore +//! trace!("value = {}", x); +//! debug!("something happened"); +//! info!("user {} logged in", username); +//! warn!("disk space low: {} GB", free); +//! error!("failed to open file: {}", path); +//! ``` +//! +//! Expands to: +//! ```ignore +//! ::log::trace!("[{}]: {}", file!(), format!("value = {}", x)); +//! ::log::debug!("[{}]: {}", file!(), format!("something happened")); +//! ... +//! ``` + +#[macro_export] +macro_rules! trace { + ($($arg:tt)*) => { + ::log::trace!("[{}]: {}", file!(), format!($($arg)*)); + }; +} + +#[macro_export] +macro_rules! debug { + ($($arg:tt)*) => { + ::log::debug!("[{}]: {}", file!(), format!($($arg)*)); + }; +} + +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => { + ::log::info!("[{}]: {}", file!(), format!($($arg)*)); + }; +} + +#[macro_export] +macro_rules! warn { + ($($arg:tt)*) => { + ::log::warn!("[{}]: {}", file!(), format!($($arg)*)); + }; +} + +#[macro_export] +macro_rules! error { + ($($arg:tt)*) => { + ::log::error!("[{}]: {}", file!(), format!($($arg)*)); + }; +} diff --git a/rola-utils/space-system/src/space/error.rs b/rola-utils/space-system/src/space/error.rs index 33ee6e4..8e85010 100644 --- a/rola-utils/space-system/src/space/error.rs +++ b/rola-utils/space-system/src/space/error.rs @@ -11,6 +11,12 @@ pub enum SpaceError { #[error("Other: {0}")] Other(String), + + #[error("Require empty directory")] + RequireEmptyDirectory, + + #[error("Config file already exist")] + ConfigFileAlreadyExist, } impl PartialEq for SpaceError { |
