Files
mdrs-client-rust/src/main.rs
T
orrisrootandCopilot 65c0626910 fix(ls): rename --quick to --quiet; add version command; bump to 0.1.1
- Fix ls -q long option name: --quick → --quiet (typo fix)
- Bump version 0.1.0 → 0.1.1
- Add `version` subcommand (prints "mdrs <version>")
- Document `version` command in README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:41:37 +09:00

366 lines
11 KiB
Rust

pub mod api;
mod commands;
mod connection;
mod models;
mod settings;
mod token;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "mdrs")]
#[command(about = "MDRS Rust CLI client", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
use commands::config_subcommand::*;
#[derive(Subcommand)]
enum Commands {
/// Config management (create, update, list, delete)
#[command(subcommand)]
Config(ConfigSubcommand),
Login {
#[arg(short, long)]
username: Option<String>,
#[arg(short, long)]
password: Option<String>,
remote: String,
},
/// Logout and remove cached credentials for a remote
Logout {
remote: String,
},
Upload {
#[arg(short, long)]
recursive: bool,
#[arg(short = 's', long)]
skip_if_exists: bool,
local_path: String,
remote_path: String,
},
Download {
#[arg(short, long)]
recursive: bool,
#[arg(short = 's', long)]
skip_if_exists: bool,
#[arg(short = 'p', long)]
password: Option<String>,
#[arg(long)]
exclude: Vec<String>,
remote_path: String,
local_path: String,
},
Ls {
remote_path: String,
#[arg(short = 'p', long)]
password: Option<String>,
#[arg(short = 'J', long = "json")]
json: bool,
#[arg(short = 'r', long)]
recursive: bool,
#[arg(short = 'q', long)]
quiet: bool,
},
Whoami {
remote: String,
},
Labs {
remote: String,
},
Chacl {
/// Access level key: private, public, pw_open, cbs_open, 5kikan_open,
/// cbs_or_pw_open, 5kikan_or_pw_open, storage
access_level_key: String,
#[arg(short, long)]
recursive: bool,
#[arg(short = 'p', long)]
password: Option<String>,
remote_path: String,
},
Metadata {
#[arg(short = 'p', long)]
password: Option<String>,
remote_path: String,
},
Mkdir {
remote_path: String,
},
Rm {
#[arg(short, long)]
recursive: bool,
remote_path: String,
},
Mv {
src_path: String,
dest_path: String,
},
Cp {
#[arg(short, long)]
recursive: bool,
src_path: String,
dest_path: String,
},
/// Show metadata for a remote file
FileMetadata {
#[arg(short = 'p', long)]
password: Option<String>,
remote_path: String,
},
/// Show the version of this tool
Version,
}
/// Print the error message in Python-compatible format and exit with code 2.
/// JSON deserialization errors show a friendlier message matching Python's JSONDecodeError handling.
fn handle_error(e: Box<dyn std::error::Error>) -> ! {
if is_json_error(&*e) {
eprintln!(
"Unexpected response returned. Please check the configuration or the server's operational status."
);
} else {
eprintln!("Error: {}", e);
}
std::process::exit(2);
}
/// Walk the error source chain to detect serde_json parse errors.
fn is_json_error(e: &(dyn std::error::Error + 'static)) -> bool {
let mut source: Option<&(dyn std::error::Error + 'static)> = Some(e);
while let Some(err) = source {
if err.downcast_ref::<serde_json::Error>().is_some() {
return true;
}
source = err.source();
}
false
}
fn main() {
// Load .env file from current directory (silently ignore if not present).
dotenvy::dotenv().ok();
// Exit with code 130 on Ctrl+C, matching Python's KeyboardInterrupt handling.
ctrlc::set_handler(|| {
std::process::exit(130);
})
.ok();
let cli = Cli::parse();
match &cli.command {
Commands::Config(subcmd) => match subcmd {
ConfigSubcommand::Create(args) => {
if let Err(e) = crate::commands::config::config_create(&args.remote, &args.url) {
handle_error(e);
}
}
ConfigSubcommand::Update(args) => {
if let Err(e) = crate::commands::config::config_update(&args.remote, &args.url) {
handle_error(e);
}
}
ConfigSubcommand::List(args) => {
if let Err(e) = crate::commands::config::config_list(args.long) {
handle_error(e);
}
}
ConfigSubcommand::Delete(args) => {
if let Err(e) = crate::commands::config::config_delete(&args.remote) {
handle_error(e);
}
}
},
Commands::Login {
username,
password,
remote,
} => {
let remote = remote.trim_end_matches(':');
use std::io::{self, Write};
let username_val: String = match username {
Some(u) => u.clone(),
None => {
print!("Username: ");
io::stdout().flush().unwrap();
let mut s = String::new();
io::stdin().read_line(&mut s).unwrap();
s.trim().to_string()
}
};
let password_val: String = match password {
Some(p) => p.clone(),
None => {
rpassword::prompt_password("Password: ").unwrap()
}
};
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) =
rt.block_on(commands::login::login(&username_val, &password_val, remote))
{
handle_error(e);
}
}
Commands::Upload {
local_path,
remote_path,
recursive,
skip_if_exists,
} => {
if let Err(e) = tokio::runtime::Runtime::new()
.unwrap()
.block_on(commands::upload::upload(
local_path,
remote_path,
*recursive,
*skip_if_exists,
))
{
handle_error(e);
}
}
Commands::Download {
remote_path,
local_path,
recursive,
skip_if_exists,
password,
exclude,
} => {
let excludes: Vec<String> = exclude
.iter()
.map(|e| e.trim_end_matches('/').to_lowercase())
.collect();
if let Err(e) = tokio::runtime::Runtime::new()
.unwrap()
.block_on(commands::download::download(
remote_path,
local_path,
*recursive,
*skip_if_exists,
password.as_deref(),
excludes,
))
{
handle_error(e);
}
}
Commands::Ls {
remote_path,
password,
json,
recursive,
quiet,
} => {
if let Err(e) = tokio::runtime::Runtime::new()
.unwrap()
.block_on(commands::ls::ls(
remote_path,
password.as_deref(),
*json,
*recursive,
*quiet,
))
{
handle_error(e);
}
}
Commands::Whoami { remote } => {
let remote = remote.trim_end_matches(':');
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(commands::whoami::whoami(remote)) {
handle_error(e);
}
}
Commands::Labs { remote } => {
let remote = remote.trim_end_matches(':');
match crate::commands::config::get_remote_url(remote) {
Ok(Some(url)) => {
let conn = std::sync::Arc::new(crate::connection::MDRSConnection::new(&url));
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(crate::commands::labs::labs(conn.clone(), remote)) {
handle_error(e);
}
}
Ok(None) => {
eprintln!("Error: Remote host `{}` is not configured", remote);
std::process::exit(2);
}
Err(e) => {
handle_error(e);
}
}
}
Commands::Chacl {
access_level_key,
recursive,
password,
remote_path,
} => {
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(crate::commands::chacl::chacl(
remote_path,
access_level_key,
*recursive,
password.as_deref(),
)) {
handle_error(e);
}
}
Commands::Metadata { remote_path, password } => {
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(crate::commands::metadata::metadata(remote_path, password.as_deref())) {
handle_error(e);
}
}
Commands::Mkdir { remote_path } => {
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(crate::commands::mkdir::mkdir(remote_path)) {
handle_error(e);
}
}
Commands::Rm {
recursive,
remote_path,
} => {
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(crate::commands::rm::rm(remote_path, *recursive)) {
handle_error(e);
}
}
Commands::Logout { remote } => {
let remote = remote.trim_end_matches(':');
if let Err(e) = crate::commands::logout::logout(remote) {
handle_error(e);
}
}
Commands::Mv { src_path, dest_path } => {
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(crate::commands::mv::mv(src_path, dest_path)) {
handle_error(e);
}
}
Commands::Cp {
src_path,
dest_path,
recursive,
} => {
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(crate::commands::cp::cp(src_path, dest_path, *recursive)) {
handle_error(e);
}
}
Commands::FileMetadata { remote_path, password } => {
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(crate::commands::file_metadata::file_metadata(remote_path, password.as_deref())) {
handle_error(e);
}
}
Commands::Version => {
commands::version::version();
}
}
}