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>
318 lines
9.9 KiB
Rust
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,
|
|
}))
|
|
})
|
|
}
|