#[path = "support/mod.rs"]
mod support;
use anyhow::{Context, Result};
use clap::Parser;
use sproto::actions::list::{FileType, NodeInfo};
#[derive(Parser)]
#[command(name = "sproto-ls", about = "List files on a Synology Drive share")]
#[allow(
clippy::struct_excessive_bools,
reason = "CLI flags are naturally boolean"
)]
struct Args {
#[arg(default_value = "/")]
path: String,
#[arg(short, long)]
long: bool,
#[arg(short = 'H', long)]
human_readable: bool,
#[arg(short = 'S', long)]
sort_size: bool,
#[arg(short = 't', long)]
sort_time: bool,
#[arg(short = 'r', long)]
reverse: bool,
#[arg(short = 'R', long)]
recursive: bool,
#[command(flatten)]
conn: support::ConnectionArgs,
}
#[tokio::main]
async fn main() -> Result<()> {
support::init_tracing();
let args = Args::parse();
let client = args.conn.connect().await?;
let view_id = args.conn.resolve_view(&client).await?;
if args.recursive {
client.warm_pool(5).await?;
list_recursive(&client, &args, view_id, &args.path).await?;
} else {
let nodes = client
.list_dir(view_id, &args.path)
.await
.with_context(|| format!("failed to list '{}'", args.path))?;
print_nodes(&args, &nodes);
}
Ok(())
}
#[allow(
clippy::cast_precision_loss,
reason = "file sizes don't need u64 precision for display"
)]
fn format_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "K", "M", "G", "T"];
if bytes < 1024 {
return format!("{bytes:>4}");
}
let mut size = bytes as f64;
for unit in &UNITS[1..] {
size /= 1024.0;
if size < 1024.0 {
return if size >= 10.0 {
format!("{size:>3.0}{unit}")
} else {
format!("{size:>3.1}{unit}")
};
}
}
format!("{size:>.0}T")
}
fn format_date(mtime: u64) -> String {
let secs = mtime.cast_signed();
let days = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let (year, month, day) = epoch_days_to_date(days);
format!("{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}")
}
#[allow(
clippy::cast_possible_truncation,
reason = "date algorithm operates within safe ranges for valid unix timestamps"
)]
const fn epoch_days_to_date(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097) as u32;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = (yoe as i64) + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
fn print_nodes(args: &Args, nodes: &[NodeInfo]) {
let mut filtered: Vec<_> = nodes.iter().filter(|n| !n.is_removed).collect();
if args.sort_size {
filtered.sort_by_key(|n| n.file_size);
} else if args.sort_time {
filtered.sort_by_key(|n| n.mtime);
} else {
filtered.sort_by_key(|n| &n.name);
}
if args.reverse {
filtered.reverse();
}
for node in &filtered {
let suffix = match node.file_type {
FileType::Dir => "/",
FileType::Symlink => "@",
FileType::File => "",
};
if args.long {
let type_char = match node.file_type {
FileType::Dir => 'd',
FileType::Symlink => 'l',
FileType::File => '-',
};
let size = if args.human_readable {
format_size(node.file_size)
} else {
format!("{:>10}", node.file_size)
};
let date = format_date(node.mtime);
println!("{type_char} {size} {date} {}{suffix}", node.name);
} else {
println!("{}{suffix}", node.name);
}
}
}
async fn list_recursive(
client: &sproto::Client,
args: &Args,
view_id: u64,
root: &str,
) -> Result<()> {
let mut results: Vec<(String, Vec<NodeInfo>)> = Vec::new();
let mut dirs_to_list = vec![root.to_string()];
while !dirs_to_list.is_empty() {
let mut handles = Vec::new();
for dir in std::mem::take(&mut dirs_to_list) {
let client = client.clone();
handles.push(tokio::spawn(async move {
let nodes = client.list_dir(view_id, &dir).await;
(dir, nodes)
}));
}
for handle in handles {
let (dir, nodes) = handle
.await
.map_err(|e| anyhow::anyhow!("task failed: {e}"))?;
let nodes = nodes.with_context(|| format!("failed to list '{dir}'"))?;
for node in &nodes {
if !node.is_removed && node.file_type == FileType::Dir {
let child = format!("{}/{}", dir.trim_end_matches('/'), node.name);
dirs_to_list.push(child);
}
}
results.push((dir, nodes));
}
}
results.sort_by(|a, b| a.0.cmp(&b.0));
for (i, (path, nodes)) in results.iter().enumerate() {
if results.len() > 1 && (i > 0 || *path != root) {
if i > 0 {
println!();
}
println!("{path}:");
}
print_nodes(args, nodes);
}
Ok(())
}