- 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>
366 lines
11 KiB
Rust
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();
|
|
}
|
|
}
|
|
}
|