summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2025-10-30 09:48:59 +0800
committerGitHub <noreply@github.com>2025-10-30 09:48:59 +0800
commit699adaa270dd698dd4e94eb83de2768058d838d0 (patch)
treec2269fa15f7374435a18db6acf0780b871603538 /src
parentf8e0fd6f4f38917a60207142761b21ac6a949cf3 (diff)
parent3a3f40b2abbaa47063cdc3aeb0149e3d02276c1e (diff)
Merge branch 'main' into docs
Diffstat (limited to 'src')
-rw-r--r--src/bin/jv.rs524
-rw-r--r--src/bin/jvv.rs119
-rw-r--r--src/utils.rs2
-rw-r--r--src/utils/input.rs105
-rw-r--r--src/utils/socket_addr_helper.rs184
5 files changed, 899 insertions, 35 deletions
diff --git a/src/bin/jv.rs b/src/bin/jv.rs
index 9530203..95176b5 100644
--- a/src/bin/jv.rs
+++ b/src/bin/jv.rs
@@ -1,6 +1,30 @@
+use std::{env::current_dir, net::SocketAddr, path::PathBuf, process::exit};
+
+use just_enough_vcs::{
+ system::action_system::action::ActionContext,
+ utils::{cfg_file::config::ConfigFile, tcp_connection::instance::ConnectionInstance},
+ vcs::{
+ actions::local_actions::SetUpstreamVaultActionResult,
+ constants::PORT,
+ current::current_local_path,
+ data::{
+ local::{LocalWorkspace, config::LocalConfig},
+ member::Member,
+ user::UserDirectory,
+ },
+ },
+};
+
use clap::{Parser, Subcommand, arg, command};
-use just_enough_vcs_cli::utils::{lang_selector::current_locales, md_colored::md};
+use just_enough_vcs::{
+ utils::tcp_connection::error::TcpTargetError,
+ vcs::{actions::local_actions::proc_set_upstream_vault_action, registry::client_registry},
+};
+use just_enough_vcs_cli::utils::{
+ input::confirm_hint_or, lang_selector::current_locales, md_colored::md, socket_addr_helper,
+};
use rust_i18n::{set_locale, t};
+use tokio::{fs, net::TcpSocket};
// Import i18n files
rust_i18n::i18n!("locales", fallback = "en");
@@ -20,9 +44,12 @@ struct JustEnoughVcsWorkspace {
#[derive(Subcommand, Debug)]
enum JustEnoughVcsWorkspaceCommand {
+ #[command(alias = "--help", alias = "-h")]
+ Help,
+
// Member management
/// Manage your local accounts
- #[command(subcommand)]
+ #[command(subcommand, alias = "acc")]
Account(AccountManage),
/// Create an empty workspace
@@ -84,6 +111,25 @@ enum AccountManage {
/// Show help information
#[command(alias = "--help", alias = "-h")]
Help,
+
+ /// Register a member to this computer
+ #[command(alias = "+")]
+ Add(AccountAddArgs),
+
+ /// Remove a account from this computer
+ #[command(alias = "rm", alias = "-")]
+ Remove(AccountRemoveArgs),
+
+ /// List all accounts in this computer
+ #[command(alias = "ls")]
+ List(AccountListArgs),
+
+ /// Set current local workspace account
+ As(SetLocalWorkspaceAccountArgs),
+
+ /// Move private key file to account
+ #[command(alias = "mvkey", alias = "mvk")]
+ MoveKey(MoveKeyToAccountArgs),
}
#[derive(Subcommand, Debug)]
@@ -98,6 +144,13 @@ struct CreateWorkspaceArgs {
/// Show help information
#[arg(short, long)]
help: bool,
+
+ /// Workspace directory path
+ path: PathBuf,
+
+ /// Force create, ignore files in the directory
+ #[arg(short, long)]
+ force: bool,
}
#[derive(Parser, Debug)]
@@ -105,6 +158,10 @@ struct InitWorkspaceArgs {
/// Show help information
#[arg(short, long)]
help: bool,
+
+ /// Force create, ignore files in the directory
+ #[arg(short, long)]
+ force: bool,
}
#[derive(Parser, Debug)]
@@ -115,6 +172,56 @@ struct HereArgs {
}
#[derive(Parser, Debug)]
+struct AccountAddArgs {
+ /// Show help information
+ #[arg(short, long)]
+ help: bool,
+
+ /// Account name
+ account_name: String,
+}
+
+#[derive(Parser, Debug)]
+struct AccountRemoveArgs {
+ /// Show help information
+ #[arg(short, long)]
+ help: bool,
+
+ /// Account name
+ account_name: String,
+}
+
+#[derive(Parser, Debug)]
+struct AccountListArgs {
+ /// Show help information
+ #[arg(short, long)]
+ help: bool,
+}
+
+#[derive(Parser, Debug)]
+struct SetLocalWorkspaceAccountArgs {
+ /// Show help information
+ #[arg(short, long)]
+ help: bool,
+
+ /// Account name
+ account_name: String,
+}
+
+#[derive(Parser, Debug)]
+struct MoveKeyToAccountArgs {
+ /// Show help information
+ #[arg(short, long)]
+ help: bool,
+
+ /// Account name
+ account_name: String,
+
+ /// Private key file path
+ key_path: PathBuf,
+}
+
+#[derive(Parser, Debug)]
struct TrackFileArgs {
/// Show help information
#[arg(short, long)]
@@ -161,6 +268,13 @@ struct DirectArgs {
/// Show help information
#[arg(short, long)]
help: bool,
+
+ /// Upstream vault address
+ upstream: String,
+
+ /// Whether to skip confirmation
+ #[arg(short = 'C', long)]
+ confirm: bool,
}
#[derive(Parser, Debug)]
@@ -168,6 +282,10 @@ struct UnstainArgs {
/// Show help information
#[arg(short, long)]
help: bool,
+
+ /// Whether to skip confirmation
+ #[arg(short = 'C', long)]
+ confirm: bool,
}
#[derive(Parser, Debug)]
@@ -187,33 +305,85 @@ async fn main() {
colored::control::set_virtual_terminal(true).unwrap();
let Ok(parser) = JustEnoughVcsWorkspace::try_parse() else {
- println!("{}", md(t!("jv.help")));
+ println!("{}", md(t!("jv.fail.parse.parser_failed")));
return;
};
match parser.command {
- JustEnoughVcsWorkspaceCommand::Account(account_manage) => match account_manage {
- AccountManage::Help => {
- println!("{}", md(t!("jv.account")));
+ JustEnoughVcsWorkspaceCommand::Help => {
+ println!("{}", md(t!("jv.help")));
+ }
+
+ JustEnoughVcsWorkspaceCommand::Account(account_manage) => {
+ let user_dir = match UserDirectory::current_doc_dir() {
+ Some(dir) => dir,
+ None => {
+ eprintln!("{}", t!("jv.fail.account.no_user_dir"));
+ return;
+ }
+ };
+
+ match account_manage {
+ AccountManage::Help => {
+ println!("{}", md(t!("jv.account")));
+ }
+ AccountManage::Add(account_add_args) => {
+ if account_add_args.help {
+ println!("{}", md(t!("jv.account")));
+ return;
+ }
+ jv_account_add(user_dir, account_add_args).await;
+ }
+ AccountManage::Remove(account_remove_args) => {
+ if account_remove_args.help {
+ println!("{}", md(t!("jv.account")));
+ return;
+ }
+ jv_account_remove(user_dir, account_remove_args).await;
+ }
+ AccountManage::List(account_list_args) => {
+ if account_list_args.help {
+ println!("{}", md(t!("jv.account")));
+ return;
+ }
+ jv_account_list(user_dir, account_list_args).await;
+ }
+ AccountManage::As(set_local_workspace_account_args) => {
+ if set_local_workspace_account_args.help {
+ println!("{}", md(t!("jv.account")));
+ return;
+ }
+ jv_account_as(user_dir, set_local_workspace_account_args).await;
+ }
+ AccountManage::MoveKey(move_key_to_account_args) => {
+ if move_key_to_account_args.help {
+ println!("{}", md(t!("jv.account")));
+ return;
+ }
+ jv_account_move_key(user_dir, move_key_to_account_args).await;
+ }
}
- },
+ }
JustEnoughVcsWorkspaceCommand::Create(create_workspace_args) => {
if create_workspace_args.help {
println!("{}", md(t!("jv.create")));
return;
}
+ jv_create(create_workspace_args).await;
}
JustEnoughVcsWorkspaceCommand::Init(init_workspace_args) => {
if init_workspace_args.help {
println!("{}", md(t!("jv.init")));
return;
}
+ jv_init(init_workspace_args).await;
}
JustEnoughVcsWorkspaceCommand::Here(here_args) => {
if here_args.help {
println!("{}", md(t!("jv.here")));
return;
}
+ jv_here(here_args).await;
}
JustEnoughVcsWorkspaceCommand::Sheet(sheet_manage) => match sheet_manage {
SheetManage::Help => {
@@ -226,54 +396,394 @@ async fn main() {
println!("{}", md(t!("jv.track")));
return;
}
+ jv_track(track_file_args).await;
}
JustEnoughVcsWorkspaceCommand::Hold(hold_file_args) => {
if hold_file_args.help {
println!("{}", md(t!("jv.hold")));
return;
}
+ jv_hold(hold_file_args).await;
}
JustEnoughVcsWorkspaceCommand::Throw(throw_file_args) => {
if throw_file_args.help {
println!("{}", md(t!("jv.throw")));
return;
}
+ jv_throw(throw_file_args).await;
}
JustEnoughVcsWorkspaceCommand::Move(move_file_args) => {
if move_file_args.help {
println!("{}", md(t!("jv.move")));
return;
}
+ jv_move(move_file_args).await;
}
JustEnoughVcsWorkspaceCommand::Export(export_file_args) => {
if export_file_args.help {
println!("{}", md(t!("jv.export")));
return;
}
+ jv_export(export_file_args).await;
}
JustEnoughVcsWorkspaceCommand::Import(import_file_args) => {
if import_file_args.help {
println!("{}", md(t!("jv.import")));
return;
}
+ jv_import(import_file_args).await;
}
JustEnoughVcsWorkspaceCommand::Direct(direct_args) => {
if direct_args.help {
println!("{}", md(t!("jv.direct")));
return;
}
+ jv_direct(direct_args).await;
}
JustEnoughVcsWorkspaceCommand::Unstain(unstain_args) => {
if unstain_args.help {
println!("{}", md(t!("jv.unstain")));
return;
}
+ jv_unstain(unstain_args).await;
}
JustEnoughVcsWorkspaceCommand::Docs(docs_args) => {
if docs_args.help {
println!("{}", md(t!("jv.docs")));
return;
}
+ jv_docs(docs_args).await;
+ }
+ }
+}
+
+async fn jv_create(args: CreateWorkspaceArgs) {
+ let path = args.path;
+
+ if !args.force && path.exists() && !is_directory_empty(&path).await {
+ eprintln!("{}", t!("jv.fail.init_create_dir_not_empty").trim());
+ return;
+ }
+
+ match LocalWorkspace::setup_local_workspace(path).await {
+ Ok(_) => {
+ println!("{}", t!("jv.success.create"));
+ }
+ Err(e) => {
+ eprintln!("{}", t!("jv.fail.create", error = e.to_string()));
+ }
+ }
+}
+
+async fn jv_init(args: InitWorkspaceArgs) {
+ let path = match current_dir() {
+ Ok(path) => path,
+ Err(e) => {
+ eprintln!("{}", t!("jv.fail.get_current_dir", error = e.to_string()));
+ return;
+ }
+ };
+
+ if !args.force && path.exists() && !is_directory_empty(&path).await {
+ eprintln!("{}", t!("jv.fail.init_create_dir_not_empty").trim());
+ return;
+ }
+
+ match LocalWorkspace::setup_local_workspace(path).await {
+ Ok(_) => {
+ println!("{}", t!("jv.success.init"));
+ }
+ Err(e) => {
+ eprintln!("{}", t!("jv.fail.init", error = e.to_string()));
+ }
+ }
+}
+
+async fn is_directory_empty(path: &PathBuf) -> bool {
+ match fs::read_dir(path).await {
+ Ok(mut entries) => entries.next_entry().await.unwrap().is_none(),
+ Err(_) => false,
+ }
+}
+
+async fn jv_here(_args: HereArgs) {
+ todo!()
+}
+
+async fn jv_track(_args: TrackFileArgs) {
+ todo!()
+}
+
+async fn jv_hold(_args: HoldFileArgs) {
+ todo!()
+}
+
+async fn jv_throw(_args: ThrowFileArgs) {
+ todo!()
+}
+
+async fn jv_move(_args: MoveFileArgs) {
+ todo!()
+}
+
+async fn jv_export(_args: ExportFileArgs) {
+ todo!()
+}
+
+async fn jv_import(_args: ImportFileArgs) {
+ todo!()
+}
+
+async fn jv_account_add(user_dir: UserDirectory, args: AccountAddArgs) {
+ let member = Member::new(args.account_name.clone());
+
+ match user_dir.register_account(member).await {
+ Ok(_) => {
+ println!(
+ "{}",
+ t!("jv.success.account.add", account = args.account_name)
+ );
+ }
+ Err(_) => {
+ eprintln!("{}", t!("jv.fail.account.add", account = args.account_name));
+ }
+ }
+}
+
+async fn jv_account_remove(user_dir: UserDirectory, args: AccountRemoveArgs) {
+ match user_dir.remove_account(&args.account_name) {
+ Ok(_) => {
+ println!(
+ "{}",
+ t!("jv.success.account.remove", account = args.account_name)
+ );
+ }
+ Err(_) => {
+ eprintln!(
+ "{}",
+ t!("jv.fail.account.remove", account = args.account_name)
+ );
+ }
+ }
+}
+
+async fn jv_account_list(user_dir: UserDirectory, _args: AccountListArgs) {
+ match user_dir.account_ids() {
+ Ok(account_ids) => {
+ println!(
+ "{}",
+ md(t!(
+ "jv.success.account.list.header",
+ num = account_ids.len()
+ ))
+ );
+
+ let mut i = 0;
+ for account_id in account_ids {
+ println!("{}. {} {}", i + 1, &account_id, {
+ if user_dir.has_private_key(&account_id) {
+ t!("jv.success.account.list.status_has_key")
+ } else {
+ std::borrow::Cow::Borrowed("")
+ }
+ });
+ i += 1;
+ }
+ }
+ Err(_) => {
+ eprintln!("{}", t!("jv.fail.account.list"));
}
}
}
+
+async fn jv_account_as(user_dir: UserDirectory, args: SetLocalWorkspaceAccountArgs) {
+ // Account exist
+ let Ok(member) = user_dir.account(&args.account_name).await else {
+ eprintln!(
+ "{}",
+ t!("jv.fail.account.not_found", account = args.account_name)
+ );
+ return;
+ };
+
+ let Some(_local_dir) = current_local_path() else {
+ eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
+ return;
+ };
+
+ let Ok(mut local_cfg) = LocalConfig::read().await else {
+ eprintln!("{}", md(t!("jv.fail.read_cfg")));
+ return;
+ };
+
+ local_cfg.set_current_account(member.id());
+
+ let Ok(_) = LocalConfig::write(&local_cfg).await else {
+ eprintln!("{}", t!("jv.fail.write_cfg").trim());
+ return;
+ };
+
+ println!(
+ "{}",
+ t!("jv.success.account.as", account = member.id()).trim()
+ );
+}
+
+async fn jv_account_move_key(user_dir: UserDirectory, args: MoveKeyToAccountArgs) {
+ // Key file exist
+ if !args.key_path.exists() {
+ eprintln!(
+ "{}",
+ t!("jv.fail.path_not_found", path = args.key_path.display())
+ );
+ return;
+ }
+
+ // Account exist
+ let Ok(_member) = user_dir.account(&args.account_name).await else {
+ eprintln!(
+ "{}",
+ t!("jv.fail.account.not_found", account = args.account_name)
+ );
+ return;
+ };
+
+ // Rename key file
+ match fs::rename(
+ args.key_path,
+ user_dir.account_private_key_path(&args.account_name),
+ )
+ .await
+ {
+ Ok(_) => println!("{}", t!("jv.success.account.move_key")),
+ Err(_) => eprintln!("{}", t!("jv.fail.account.move_key")),
+ }
+}
+
+async fn jv_direct(args: DirectArgs) {
+ if !args.confirm {
+ println!(
+ "{}",
+ t!("jv.confirm.direct", upstream = args.upstream).trim()
+ );
+ confirm_hint_or(t!("common.confirm"), || exit(1)).await;
+ }
+
+ let pool = client_registry::client_action_pool();
+ let upstream = match socket_addr_helper::get_socket_addr(&args.upstream, PORT).await {
+ Ok(result) => result,
+ Err(e) => {
+ eprintln!(
+ "{}",
+ md(t!(
+ "jv.fail.parse.str_to_sockaddr",
+ str = &args.upstream.trim(),
+ err = e
+ ))
+ );
+ return;
+ }
+ };
+
+ let Some(instance) = connect(upstream).await else {
+ // Since connect() function already printed error messages, we only handle the return here
+ return;
+ };
+
+ let ctx = ActionContext::local().insert_instance(instance);
+
+ match proc_set_upstream_vault_action(&pool, ctx, upstream).await {
+ Err(e) => handle_err(e),
+ Ok(result) => match result {
+ SetUpstreamVaultActionResult::DirectedAndStained => {
+ println!(
+ "{}",
+ md(t!(
+ "jv.result.direct.directed_and_stained",
+ upstream = upstream
+ ))
+ )
+ }
+ SetUpstreamVaultActionResult::AlreadyStained => {
+ eprintln!("{}", md(t!("jv.result.direct.already_stained")))
+ }
+ SetUpstreamVaultActionResult::AuthorizeFailed(e) => {
+ println!(
+ "{}",
+ md(t!("jv.result.direct.directed_and_stained", err = e))
+ )
+ }
+ },
+ };
+}
+
+async fn jv_unstain(args: UnstainArgs) {
+ let Some(_local_dir) = current_local_path() else {
+ eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
+ return;
+ };
+
+ let Ok(mut local_cfg) = LocalConfig::read().await else {
+ eprintln!("{}", md(t!("jv.fail.read_cfg")));
+ return;
+ };
+
+ if !local_cfg.stained() {
+ eprintln!("{}", md(t!("jv.fail.unstain")));
+ return;
+ }
+
+ if !args.confirm {
+ println!(
+ "{}",
+ md(t!("jv.warn.unstain", upstream = local_cfg.vault_addr()))
+ );
+ confirm_hint_or(t!("common.confirm"), || exit(1)).await;
+ }
+
+ local_cfg.unstain();
+
+ let Ok(_) = LocalConfig::write(&local_cfg).await else {
+ eprintln!("{}", t!("jv.fail.write_cfg").trim());
+ return;
+ };
+
+ println!("{}", md(t!("jv.success.unstain")));
+}
+
+async fn jv_docs(_args: DocsArgs) {
+ todo!()
+}
+
+pub fn handle_err(err: TcpTargetError) {
+ eprintln!("{}", md(t!("jv.fail.from_just_version_control", err = err)))
+}
+
+async fn connect(upstream: SocketAddr) -> Option<ConnectionInstance> {
+ // Create Socket
+ let socket = if upstream.is_ipv4() {
+ match TcpSocket::new_v4() {
+ Ok(socket) => socket,
+ Err(_) => {
+ eprintln!("{}", t!("jv.fail.create_socket").trim());
+ return None;
+ }
+ }
+ } else {
+ match TcpSocket::new_v6() {
+ Ok(socket) => socket,
+ Err(_) => {
+ eprintln!("{}", t!("jv.fail.create_socket").trim());
+ return None;
+ }
+ }
+ };
+
+ // Connect
+ let Ok(stream) = socket.connect(upstream).await else {
+ eprintln!("{}", t!("jv.fail.connection_failed").trim());
+ return None;
+ };
+
+ Some(ConnectionInstance::from(stream))
+}
diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs
index 50dc3b3..8480029 100644
--- a/src/bin/jvv.rs
+++ b/src/bin/jvv.rs
@@ -19,7 +19,7 @@ use just_enough_vcs::{
use just_enough_vcs_cli::utils::{
build_env_logger::build_env_logger, lang_selector::current_locales, md_colored::md,
};
-use log::info;
+use log::{error, info};
use rust_i18n::{set_locale, t};
use tokio::fs::{self};
@@ -41,32 +41,42 @@ struct JustEnoughVcsVault {
#[derive(Subcommand, Debug)]
enum JustEnoughVcsVaultCommand {
/// Get vault info in the current directory
+ #[command(alias = "-H")]
Here(HereArgs),
/// Create a new directory and initialize a vault
+ #[command(alias = "-c")]
Create(CreateVaultArgs),
/// Create a vault in the current directory
+ #[command(alias = "-i")]
Init(InitVaultArgs),
/// Member manage
- #[command(subcommand)]
+ #[command(subcommand, alias = "-m")]
Member(MemberManage),
/// Manage service
#[command(subcommand)]
Service(ServiceManage),
+
+ // Short commands
+ #[command(alias = "-l", alias = "listen")]
+ ServiceListen(ListenArgs),
}
#[derive(Subcommand, Debug)]
enum MemberManage {
/// Register a member to the vault
+ #[command(alias = "+")]
Register(MemberRegisterArgs),
/// Remove a member from the vault
+ #[command(alias = "-")]
Remove(MemberRemoveArgs),
/// List all members in the vault
+ #[command(alias = "ls")]
List(MemberListArgs),
/// Show help information
@@ -144,6 +154,10 @@ struct ListenArgs {
/// Disable logging
#[arg(short, long)]
no_log: bool,
+
+ /// Custom port
+ #[arg(short, long)]
+ port: Option<u16>,
}
#[tokio::main]
@@ -153,7 +167,10 @@ async fn main() {
// Init colored
#[cfg(windows)]
- colored::control::set_virtual_terminal(true).unwrap();
+ if let Err(err) = colored::control::set_virtual_terminal(true) {
+ eprintln!("{}", t!("jvv.fail.colored_control", err = err.to_string()));
+ return;
+ }
let Ok(parser) = JustEnoughVcsVault::try_parse() else {
println!("{}", md(t!("jvv.help")));
@@ -183,9 +200,13 @@ async fn main() {
jvv_init(init_vault_args).await;
}
JustEnoughVcsVaultCommand::Member(member_manage) => {
- let vault_cfg = VaultConfig::read()
- .await
- .unwrap_or_else(|_| panic!("{}", t!("jvv.fail.no_vault_here").trim().to_string()));
+ let vault_cfg = match VaultConfig::read().await {
+ Ok(cfg) => cfg,
+ Err(_) => {
+ eprintln!("{}", t!("jvv.fail.no_vault_here").trim());
+ return;
+ }
+ };
let vault = match Vault::init_current_dir(vault_cfg) {
Some(vault) => vault,
@@ -239,6 +260,14 @@ async fn main() {
return;
}
},
+ // Short commands
+ JustEnoughVcsVaultCommand::ServiceListen(listen_args) => {
+ if listen_args.help {
+ println!("{}", md(t!("jvv.service")));
+ return;
+ }
+ jvv_service_listen(listen_args).await;
+ }
}
}
@@ -282,11 +311,11 @@ async fn jvv_here(_args: HereArgs) {
if let Ok(mut entries) = fs::read_dir(&virtual_file_root).await {
while let Ok(Some(entry)) = entries.next_entry().await {
- if let Ok(metadata) = entry.metadata().await {
- if metadata.is_file() {
- num_vf += 1;
- total_size += metadata.len();
- }
+ if let Ok(metadata) = entry.metadata().await
+ && metadata.is_file()
+ {
+ num_vf += 1;
+ total_size += metadata.len();
}
}
}
@@ -358,19 +387,28 @@ async fn jvv_here(_args: HereArgs) {
}
async fn jvv_init(_args: InitVaultArgs) {
- let current_dir = std::env::current_dir()
- .unwrap_or_else(|_| panic!("{}", t!("jvv.fail.std.current_dir").trim().to_string()));
- if current_dir.read_dir().unwrap().next().is_some() {
+ let current_dir = match std::env::current_dir() {
+ Ok(dir) => dir,
+ Err(_) => {
+ eprintln!("{}", t!("jvv.fail.std.current_dir").trim());
+ return;
+ }
+ };
+ if let Ok(mut entries) = current_dir.read_dir()
+ && entries.next().is_some()
+ {
eprintln!("{}", t!("jvv.fail.init.not_empty"));
return;
}
// Setup vault
- let vault_name = current_dir
- .file_name()
- .unwrap_or_else(|| panic!("{}", t!("jvv.fail.std.current_dir_name").trim().to_string()))
- .to_string_lossy()
- .to_string();
+ let vault_name = match current_dir.file_name() {
+ Some(name) => name.to_string_lossy().to_string(),
+ None => {
+ eprintln!("{}", t!("jvv.fail.std.current_dir_name").trim());
+ return;
+ }
+ };
let vault_name = pascal_case!(vault_name);
if let Err(err) = Vault::setup_vault(current_dir.clone()).await {
@@ -401,8 +439,13 @@ async fn jvv_init(_args: InitVaultArgs) {
}
async fn jvv_create(args: CreateVaultArgs) {
- let current_dir = std::env::current_dir()
- .unwrap_or_else(|_| panic!("{}", t!("jvv.fail.std.current_dir").trim().to_string()));
+ let current_dir = match std::env::current_dir() {
+ Ok(dir) => dir,
+ Err(_) => {
+ eprintln!("{}", t!("jvv.fail.std.current_dir").trim());
+ return;
+ }
+ };
let target_dir = current_dir.join(args.vault_name.clone());
// Create directory
@@ -417,7 +460,9 @@ async fn jvv_create(args: CreateVaultArgs) {
return;
}
- if target_dir.read_dir().unwrap().next().is_some() {
+ if let Ok(mut entries) = target_dir.read_dir()
+ && entries.next().is_some()
+ {
eprintln!("{}", t!("jvv.fail.create.not_empty"));
return;
}
@@ -490,9 +535,13 @@ async fn jvv_member_remove(vault: Vault, args: MemberRemoveArgs) {
async fn jvv_member_list(vault: Vault, _args: MemberListArgs) {
// Get id list
- let ids = vault
- .member_ids()
- .unwrap_or_else(|_| panic!("{}", t!("jvv.fail.member.list").trim().to_string()));
+ let ids = match vault.member_ids() {
+ Ok(ids) => ids,
+ Err(_) => {
+ eprintln!("{}", t!("jvv.fail.member.list").trim());
+ return;
+ }
+ };
// Print header
println!(
@@ -559,11 +608,25 @@ async fn jvv_service_listen(args: ListenArgs) {
info!(
"{}",
t!(
- "jvv.success.service.listen",
- path = current_vault.file_name().unwrap().display()
+ "jvv.success.service.listen_start",
+ path = match current_vault.file_name() {
+ Some(name) => name.to_string_lossy(),
+ None => std::borrow::Cow::Borrowed("unknown"),
+ }
)
)
}
- let _ = server_entry(current_vault).await;
+ let port = args.port.unwrap_or_default();
+ match server_entry(current_vault, port).await {
+ Ok(_) => {
+ info!("{}", t!("jvv.success.service.listen_done").trim());
+ }
+ Err(e) => {
+ error!(
+ "{}",
+ t!("jvv.fail.service.listen_done", error = e.to_string()).trim()
+ );
+ }
+ }
}
diff --git a/src/utils.rs b/src/utils.rs
index 87cc1b4..dfa6926 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,3 +1,5 @@
pub mod build_env_logger;
+pub mod input;
pub mod lang_selector;
pub mod md_colored;
+pub mod socket_addr_helper;
diff --git a/src/utils/input.rs b/src/utils/input.rs
new file mode 100644
index 0000000..a728c77
--- /dev/null
+++ b/src/utils/input.rs
@@ -0,0 +1,105 @@
+use tokio::{fs, process::Command};
+
+/// Confirm the current operation
+/// Waits for user input of 'y' or 'n'
+pub async fn confirm_hint(text: impl Into<String>) -> bool {
+ use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
+
+ let prompt = text.into().trim().to_string();
+
+ let mut stdout = io::stdout();
+ let mut stdin = BufReader::new(io::stdin());
+
+ stdout
+ .write_all(prompt.as_bytes())
+ .await
+ .expect("Failed to write prompt");
+ stdout.flush().await.expect("Failed to flush stdout");
+
+ let mut input = String::new();
+ stdin
+ .read_line(&mut input)
+ .await
+ .expect("Failed to read input");
+
+ input.trim().eq_ignore_ascii_case("y")
+}
+
+/// Confirm the current operation, or execute a closure if rejected
+/// Waits for user input of 'y' or 'n'
+/// If 'n' is entered, executes the provided closure and returns false
+pub async fn confirm_hint_or<F>(text: impl Into<String>, on_reject: F) -> bool
+where
+ F: FnOnce(),
+{
+ let confirmed = confirm_hint(text).await;
+ if !confirmed {
+ on_reject();
+ }
+ confirmed
+}
+
+/// Confirm the current operation, and execute a closure if confirmed
+/// Waits for user input of 'y' or 'n'
+/// If 'y' is entered, executes the provided closure and returns true
+pub async fn confirm_hint_then<F>(text: impl Into<String>, on_confirm: F) -> bool
+where
+ F: FnOnce(),
+{
+ let confirmed = confirm_hint(text).await;
+ if confirmed {
+ on_confirm();
+ }
+ confirmed
+}
+
+/// Input text using the system editor
+/// Opens the system editor (from EDITOR environment variable) with default text in a cache file,
+/// then reads back the modified content after the editor closes, removing comment lines
+pub async fn input_with_editor(
+ default_text: impl AsRef<str>,
+ cache_file: impl AsRef<std::path::Path>,
+ comment_char: impl AsRef<str>,
+) -> Result<String, std::io::Error> {
+ let cache_path = cache_file.as_ref();
+ let default_content = default_text.as_ref();
+ let comment_prefix = comment_char.as_ref();
+
+ // Write default text to cache file
+ fs::write(cache_path, default_content).await?;
+
+ // Get editor from environment variable
+ let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
+
+ // Open editor with cache file
+ let status = Command::new(editor).arg(cache_path).status().await?;
+
+ if !status.success() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "Editor exited with non-zero status",
+ ));
+ }
+
+ // Read the modified content
+ let content = fs::read_to_string(cache_path).await?;
+
+ // Remove comment lines and trim
+ let processed_content: String = content
+ .lines()
+ .filter_map(|line| {
+ let trimmed = line.trim();
+ if trimmed.starts_with(comment_prefix) {
+ None
+ } else {
+ Some(line)
+ }
+ })
+ .collect::<Vec<&str>>()
+ .join("\n");
+
+ // Delete the cache file
+ let _ = fs::remove_file(cache_path).await;
+
+ Ok(processed_content)
+}
diff --git a/src/utils/socket_addr_helper.rs b/src/utils/socket_addr_helper.rs
new file mode 100644
index 0000000..fd7b346
--- /dev/null
+++ b/src/utils/socket_addr_helper.rs
@@ -0,0 +1,184 @@
+use std::net::SocketAddr;
+use tokio::net::lookup_host;
+
+/// Helper function to parse a string into a SocketAddr with optional default port
+pub async fn get_socket_addr(
+ address_str: impl AsRef<str>,
+ default_port: u16,
+) -> Result<SocketAddr, std::io::Error> {
+ let address = address_str.as_ref().trim();
+
+ // Check if the address contains a port
+ if let Some((host, port_str)) = parse_host_and_port(address) {
+ let port = port_str.parse::<u16>().map_err(|e| {
+ std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ format!("Invalid port number '{}': {}", port_str, e),
+ )
+ })?;
+
+ return resolve_to_socket_addr(host, port).await;
+ }
+
+ // No port specified, use default port
+ resolve_to_socket_addr(address, default_port).await
+}
+
+/// Parse host and port from address string
+fn parse_host_and_port(address: &str) -> Option<(&str, &str)> {
+ if address.starts_with('[')
+ && let Some(close_bracket) = address.find(']')
+ && close_bracket + 1 < address.len() && address.as_bytes()[close_bracket + 1] == b':' {
+ let host = &address[1..close_bracket];
+ let port = &address[close_bracket + 2..];
+ return Some((host, port));
+ }
+
+ // Handle IPv4 addresses and hostnames with ports
+ if let Some(colon_pos) = address.rfind(':') {
+ // Check if this is not part of an IPv6 address without brackets
+ if !address.contains('[') && !address.contains(']') {
+ let host = &address[..colon_pos];
+ let port = &address[colon_pos + 1..];
+
+ // Basic validation to avoid false positives
+ if !host.is_empty() && !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) {
+ return Some((host, port));
+ }
+ }
+ }
+
+ None
+}
+
+/// Resolve host to SocketAddr, handling both IP addresses and domain names
+async fn resolve_to_socket_addr(host: &str, port: u16) -> Result<SocketAddr, std::io::Error> {
+ // First try to parse as IP address (IPv4 or IPv6)
+ if let Ok(ip_addr) = host.parse() {
+ return Ok(SocketAddr::new(ip_addr, port));
+ }
+
+ // If it's not a valid IP address, treat it as a domain name and perform DNS lookup
+ let lookup_addr = format!("{}:{}", host, port);
+ let mut addrs = lookup_host(&lookup_addr).await?;
+
+ if let Some(addr) = addrs.next() {
+ Ok(addr)
+ } else {
+ Err(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ format!("Could not resolve host '{}'", host),
+ ))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn test_ipv4_with_port() {
+ let result = get_socket_addr("127.0.0.1:8080", 80).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 8080);
+ assert_eq!(addr.ip().to_string(), "127.0.0.1");
+ }
+
+ #[tokio::test]
+ async fn test_ipv4_without_port() {
+ let result = get_socket_addr("192.168.1.1", 443).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 443);
+ assert_eq!(addr.ip().to_string(), "192.168.1.1");
+ }
+
+ #[tokio::test]
+ async fn test_ipv6_with_port() {
+ let result = get_socket_addr("[::1]:8080", 80).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 8080);
+ assert_eq!(addr.ip().to_string(), "::1");
+ }
+
+ #[tokio::test]
+ async fn test_ipv6_without_port() {
+ let result = get_socket_addr("[::1]", 443).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 443);
+ assert_eq!(addr.ip().to_string(), "::1");
+ }
+
+ #[tokio::test]
+ async fn test_invalid_port() {
+ let result = get_socket_addr("127.0.0.1:99999", 80).await;
+ assert!(result.is_err());
+ }
+
+ #[tokio::test]
+ async fn test_empty_string() {
+ let result = get_socket_addr("", 80).await;
+ assert!(result.is_err());
+ }
+
+ #[tokio::test]
+ async fn test_whitespace_trimming() {
+ let result = get_socket_addr(" 127.0.0.1:8080 ", 80).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 8080);
+ }
+
+ #[tokio::test]
+ async fn test_domain_name_with_port() {
+ // This test will only pass if localhost resolves
+ let result = get_socket_addr("localhost:8080", 80).await;
+ if result.is_ok() {
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 8080);
+ // localhost should resolve to 127.0.0.1 or ::1
+ assert!(addr.ip().is_loopback());
+ }
+ }
+
+ #[tokio::test]
+ async fn test_domain_name_without_port() {
+ // This test will only pass if localhost resolves
+ let result = get_socket_addr("localhost", 443).await;
+ if result.is_ok() {
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 443);
+ // localhost should resolve to 127.0.0.1 or ::1
+ assert!(addr.ip().is_loopback());
+ }
+ }
+
+ #[tokio::test]
+ async fn test_parse_host_and_port() {
+ // IPv4 with port
+ assert_eq!(
+ parse_host_and_port("192.168.1.1:8080"),
+ Some(("192.168.1.1", "8080"))
+ );
+
+ // IPv6 with port
+ assert_eq!(parse_host_and_port("[::1]:8080"), Some(("::1", "8080")));
+
+ // Hostname with port
+ assert_eq!(
+ parse_host_and_port("example.com:443"),
+ Some(("example.com", "443"))
+ );
+
+ // No port
+ assert_eq!(parse_host_and_port("192.168.1.1"), None);
+ assert_eq!(parse_host_and_port("example.com"), None);
+
+ // Invalid cases
+ assert_eq!(parse_host_and_port(":"), None);
+ assert_eq!(parse_host_and_port("192.168.1.1:"), None);
+ }
+}