Files
mdrs-client-rust/src/commands/ls.rs
T
orrisrootandCopilot 769a5a68e2 refactor: unify error handling with anyhow and add From conversions
Phase 5: Replace all Box<dyn Error> return types with anyhow::Result<T>
throughout the codebase. Replace string-based Err("msg".into()) and
format!().into() patterns with bail!() and anyhow!() macros. Fix
dirs::home_dir().unwrap() in settings.rs to use a fallback path instead
of panicking when HOME is unset. Remove stray use std::error::Error
imports no longer needed.

Phase 6: Add From<&User> for CacheUser in models/user.rs and
From<&Laboratory>/From<&Laboratories> for CacheLaboratory/CacheLabsWrapper
in models/laboratory.rs. Simplify commands/login.rs to use .into()
conversions, removing the redundant to_cache_user() and to_cache_labs()
helper functions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 14:19:10 +09:00

318 lines
9.9 KiB
Rust

use crate::models::file::File;
use crate::models::folder::{FolderDetail, FolderSimple};
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_folder, find_lab_in_cache, fmt_datetime, parse_remote_path,
};
use crate::connection::MDRSConnection;
use serde_json::{json, Value};
use std::future::Future;
use std::pin::Pin;
pub async fn ls(
remote_path: &str,
password: Option<&str>,
is_json: bool,
is_recursive: bool,
is_quiet: bool,
) -> Result<(), anyhow::Error> {
let (remote, labname, path) = parse_remote_path(remote_path)?;
let cache = load_cache_with_token_refresh(&remote).await?;
let conn = create_authenticated_conn(&remote, &cache)?;
let lab = find_lab_in_cache(&cache, &labname)?;
let folder = find_folder(&conn, lab.id, &path, password).await?;
if is_json {
let output = if is_recursive {
build_folder_json_recursive(&conn, folder, &labname).await?
} else {
build_folder_json_flat(&conn, &folder, &labname).await?
};
println!("{}", serde_json::to_string(&output)?);
} else if is_recursive {
let prefix = format!("{}:/{}", remote, labname);
ls_plain_recursive(&conn, folder, &labname, &prefix, password).await?;
} else {
let files = conn.list_all_files(&folder.id).await?;
let mut sub_folders = folder.sub_folders.clone();
sub_folders.sort_by(|a, b| a.name.cmp(&b.name));
let mut files_sorted = files;
files_sorted.sort_by(|a, b| a.name.cmp(&b.name));
print_folder_plain(
&sub_folders,
&files_sorted,
folder.access_level_name(),
&labname,
!is_quiet,
);
}
Ok(())
}
// ---------------------------------------------------------------------------
// Plain-text output helpers
// ---------------------------------------------------------------------------
fn print_folder_plain(
sub_folders: &[FolderSimple],
files: &[File],
folder_access: &str,
labname: &str,
show_header: bool,
) {
let header = ("Type", "Access", "Laboratory", "Size", "Date", "Name");
let mut w_type = header.0.len();
let mut w_access = header.1.len();
let w_lab = header.2.len().max(labname.len());
let mut w_size = header.3.len();
let mut w_date = header.4.len();
let mut w_name = header.5.len();
// [d], [l], [f] are each 3 chars; ensure w_type accommodates them
w_type = w_type.max(3);
for sf in sub_folders {
w_access = w_access.max(sf.access_level_name().len());
w_size = w_size.max(sf.size.to_string().len());
w_date = w_date.max(fmt_datetime(&sf.updated_at).len());
w_name = w_name.max(sf.name.len());
}
for f in files {
w_access = w_access.max(folder_access.len());
w_size = w_size.max(f.size.to_string().len());
w_date = w_date.max(fmt_datetime(&f.updated_at).len());
w_name = w_name.max(f.name.len());
}
if show_header {
println!(
"{:<w_type$} {:<w_access$} {:<w_lab$} {:>w_size$} {:<w_date$} {:<w_name$}",
header.0,
header.1,
header.2,
header.3,
header.4,
header.5,
w_type = w_type,
w_access = w_access,
w_lab = w_lab,
w_size = w_size,
w_date = w_date,
w_name = w_name,
);
let line_len = w_type + 2 + w_access + 2 + w_lab + 2 + w_size + 2 + w_date + 2 + w_name;
println!("{}", "-".repeat(line_len));
}
for sf in sub_folders {
let folder_type = if sf.lock { "[l]" } else { "[d]" };
println!(
"{:<w_type$} {:<w_access$} {:<w_lab$} {:>w_size$} {:<w_date$} {:<w_name$}",
folder_type,
sf.access_level_name(),
labname,
sf.size,
fmt_datetime(&sf.updated_at),
sf.name,
w_type = w_type,
w_access = w_access,
w_lab = w_lab,
w_size = w_size,
w_date = w_date,
w_name = w_name,
);
}
for f in files {
println!(
"{:<w_type$} {:<w_access$} {:<w_lab$} {:>w_size$} {:<w_date$} {:<w_name$}",
"[f]",
folder_access,
labname,
f.size,
fmt_datetime(&f.updated_at),
f.name,
w_type = w_type,
w_access = w_access,
w_lab = w_lab,
w_size = w_size,
w_date = w_date,
w_name = w_name,
);
}
}
/// Recursively list folder contents in plain-text mode (no header, path prefix per folder).
fn ls_plain_recursive<'a>(
conn: &'a MDRSConnection,
folder: FolderDetail,
labname: &'a str,
prefix: &'a str,
password: Option<&'a str>,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + 'a>> {
Box::pin(async move {
let files = conn.list_all_files(&folder.id).await?;
let total_size: u64 = files.iter().map(|f| f.size).sum();
println!("{}{}:", prefix, folder.path);
println!("total {}", total_size);
let access = folder.access_level_name();
let mut sub_folders = folder.sub_folders.clone();
sub_folders.sort_by(|a, b| a.name.cmp(&b.name));
let mut files_sorted = files;
files_sorted.sort_by(|a, b| a.name.cmp(&b.name));
print_folder_plain(&sub_folders, &files_sorted, access, labname, false);
println!();
for sf in sub_folders {
if sf.lock {
match password {
None => continue,
Some(pw) => {
if conn.folder_auth(&sf.id, pw).await.is_err() {
continue;
}
}
}
}
match conn.retrieve_folder(&sf.id).await {
Ok(sub_detail) => {
ls_plain_recursive(conn, sub_detail, labname, prefix, password).await?;
}
Err(_) => continue,
}
}
Ok(())
})
}
// ---------------------------------------------------------------------------
// JSON output helpers
// ---------------------------------------------------------------------------
async fn get_folder_metadata(
conn: &MDRSConnection,
folder_id: &str,
) -> Result<Value, anyhow::Error> {
let resp = conn
.get(&format!("v3/folders/{}/metadata/", folder_id))
.await?;
if resp.status().is_success() {
Ok(resp.json::<Value>().await?)
} else {
Ok(json!({}))
}
}
fn file_to_json(f: &File, base_url: &str) -> Value {
let download_url = if f.download_url.starts_with("http") {
f.download_url.clone()
} else {
format!(
"{}{}",
base_url.trim_end_matches('/'),
f.download_url
)
};
json!({
"id": f.id,
"name": f.name,
"type": f.r#type,
"size": f.size,
"description": f.description,
"metadata": f.metadata,
"download_url": download_url,
"created_at": f.created_at,
"updated_at": f.updated_at,
})
}
fn folder_simple_to_json(sf: &FolderSimple) -> Value {
json!({
"id": sf.id,
"pid": sf.pid,
"name": sf.name,
"access_level": sf.access_level_name(),
"lock": sf.lock,
"size": sf.size,
"laboratory_id": sf.laboratory_id,
"description": sf.description,
"created_at": sf.created_at,
"updated_at": sf.updated_at,
})
}
/// Build JSON for the top-level folder without recursing into sub-folders.
async fn build_folder_json_flat(
conn: &MDRSConnection,
folder: &FolderDetail,
labname: &str,
) -> Result<Value, anyhow::Error> {
let metadata = get_folder_metadata(conn, &folder.id).await?;
let files = conn.list_all_files(&folder.id).await?;
let files_json: Vec<Value> = files.iter().map(|f| file_to_json(f, &conn.url)).collect();
let sub_folders_json: Vec<Value> = folder
.sub_folders
.iter()
.map(folder_simple_to_json)
.collect();
Ok(json!({
"id": folder.id,
"pid": folder.pid,
"name": folder.name,
"size": folder.size,
"access_level": folder.access_level_name(),
"lock": folder.lock,
"laboratory": labname,
"description": folder.description,
"created_at": folder.created_at,
"updated_at": folder.updated_at,
"metadata": metadata,
"sub_folders": sub_folders_json,
"files": files_json,
}))
}
/// Build JSON for a folder, recursively expanding every sub-folder.
fn build_folder_json_recursive<'a>(
conn: &'a MDRSConnection,
folder: FolderDetail,
labname: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Value, anyhow::Error>> + 'a>> {
Box::pin(async move {
let metadata = get_folder_metadata(conn, &folder.id).await?;
let files = conn.list_all_files(&folder.id).await?;
let files_json: Vec<Value> = files.iter().map(|f| file_to_json(f, &conn.url)).collect();
let mut sub_folders_json = Vec::new();
for sf in &folder.sub_folders {
let sf_detail = conn.retrieve_folder(&sf.id).await?;
let sf_json = build_folder_json_recursive(conn, sf_detail, labname).await?;
sub_folders_json.push(sf_json);
}
Ok(json!({
"id": folder.id,
"pid": folder.pid,
"name": folder.name,
"size": folder.size,
"access_level": folder.access_level_name(),
"lock": folder.lock,
"laboratory": labname,
"description": folder.description,
"created_at": folder.created_at,
"updated_at": folder.updated_at,
"metadata": metadata,
"sub_folders": sub_folders_json,
"files": files_json,
}))
})
}