use crate::util;
use nu_ansi_term::Color;
use tame_gcs::{
common::StandardQueryParameters,
objects::{ListOptional, ListResponse, Metadata},
};
#[derive(clap::Parser, Debug)]
pub struct Args {
#[structopt(short = 'R', long)]
recurse: bool,
#[structopt(short, long)]
long: bool,
url: url::Url,
}
pub async fn cmd(ctx: &util::RequestContext, args: Args) -> anyhow::Result<()> {
let oid = util::gs_url_to_object_id(&args.url)?;
let delimiter = if args.recurse { None } else { Some("/") };
let mut prefix = oid.object().map_or("", |on| on.as_ref()).to_owned();
if !prefix.is_empty() && !prefix.ends_with('/') {
prefix.push('/');
}
let prefix_len = prefix.len();
let prefix = Some(prefix);
let display = if args.long {
Display::Long
} else {
Display::Normal
};
let mut recurse = if args.recurse {
Some(RecursePrinter {
display,
prefix_len,
items: Vec::new(),
current_year: time::OffsetDateTime::now_utc().year(),
})
} else {
None
};
let normal = if !args.recurse {
Some(NormalPrinter {
display,
prefix_len,
})
} else {
None
};
let fields = match display {
Display::Normal => "items(name), prefixes, nextPageToken",
Display::Long => "items(name, updated, size), prefixes, nextPageToken",
};
let mut page_token: Option<String> = None;
loop {
let ls_req = ctx.obj.list(
oid.bucket(),
Some(ListOptional {
delimiter,
page_token: page_token.as_ref().map(|pt| pt.as_ref()),
prefix: prefix.as_ref().map(|s| s.as_ref()),
standard_params: StandardQueryParameters {
fields: Some(fields),
..Default::default()
},
..Default::default()
}),
)?;
let ls_res: ListResponse = util::execute(ctx, ls_req).await?;
if let Some(ref np) = normal {
np.print(ls_res.objects, ls_res.prefixes);
} else if let Some(ref mut rec) = recurse {
rec.append(ls_res.objects);
}
page_token = ls_res.page_token;
if page_token.is_none() {
break;
}
}
if let Some(ref rec) = recurse {
rec.print();
}
Ok(())
}
#[derive(Copy, Clone)]
enum Display {
Normal,
Long,
}
struct NormalPrinter {
display: Display,
prefix_len: usize,
}
fn print_dir(display: Display, dir: &str) {
match display {
Display::Normal => println!("{}", Color::Blue.bold().paint(dir)),
Display::Long => println!(
" {} {} {} {}",
Color::White.dimmed().paint("-"),
Color::White.dimmed().paint(" -"),
Color::White.dimmed().paint("-- --- --:--"),
Color::Blue.bold().paint(dir),
),
}
}
impl NormalPrinter {
fn print(&self, items: Vec<Metadata>, prefixes: Vec<String>) {
let indices = {
let mut indices = Vec::with_capacity(prefixes.len());
for prefix in &prefixes {
if let Err(i) = items.binary_search_by(|om| om.name.as_ref().unwrap().cmp(prefix)) {
indices.push(i);
}
}
indices
};
let mut next_dir_iter = indices.iter().enumerate();
let mut next_dir = next_dir_iter.next();
let current_year = time::OffsetDateTime::now_utc().year();
for (i, item) in items.into_iter().enumerate() {
if let Some(nd) = next_dir {
if *nd.1 == i {
let dir = &(&prefixes[nd.0])[self.prefix_len..];
let dir = &dir[..dir.len() - 1];
print_dir(self.display, dir);
next_dir = next_dir_iter.next();
}
}
let filename = &item.name.unwrap()[self.prefix_len..];
match self.display {
Display::Normal => println!("{}", Color::White.paint(filename)),
Display::Long => {
use number_prefix::NumberPrefix;
let size_str = match NumberPrefix::decimal(item.size.unwrap_or_default() as f64)
{
NumberPrefix::Standalone(b) => b.to_string(),
NumberPrefix::Prefixed(p, n) => {
if n < 10f64 {
format!("{:.1}{}", n, p.symbol())
} else {
format!("{:.0}{}", n, p.symbol())
}
}
};
let updated = item.updated.unwrap();
let updated_str = timestamp_str(updated, current_year);
println!(
" {}{} {} {} {}",
if size_str.len() < 4 { " " } else { "" },
Color::Green.paint(size_str),
Color::Yellow.paint("gcs"),
Color::Blue.paint(updated_str),
Color::White.paint(filename),
);
}
}
}
while let Some(nd) = next_dir {
let dir = &(&prefixes[nd.0])[self.prefix_len..];
let dir = &dir[..dir.len() - 1];
print_dir(self.display, dir);
next_dir = next_dir_iter.next();
}
}
}
fn timestamp_str(ts: time::OffsetDateTime, current_year: i32) -> String {
const RECENT: &[time::format_description::FormatItem<'_>] =
time::macros::format_description!("[day] [month repr:short] [hour]:[minute]");
const OLD: &[time::format_description::FormatItem<'_>] =
time::macros::format_description!("[day] [month repr:short] [year]");
if ts.year() == current_year {
ts.format(&RECENT)
} else {
ts.format(&OLD)
}
.unwrap()
}
struct SimpleMetadata {
name: String,
size: u64,
updated: String,
}
struct RecursePrinter {
display: Display,
prefix_len: usize,
items: Vec<Vec<SimpleMetadata>>,
current_year: i32,
}
use std::io::Write;
impl RecursePrinter {
fn append(&mut self, items: Vec<Metadata>) {
let items = items
.into_iter()
.map(|md| SimpleMetadata {
name: String::from(&md.name.unwrap()[self.prefix_len..]),
size: md.size.unwrap_or_default(),
updated: md
.updated
.map(|dt| timestamp_str(dt, self.current_year))
.unwrap_or_default(),
})
.collect();
self.items.push(items);
}
fn print(&self) {
let mut stdout = std::io::stdout().lock();
let mut dirs = vec![String::new()];
while let Some(dir) = dirs.pop() {
if !dir.is_empty() {
writeln!(stdout, "\n{}:", &dir[..dir.len() - 1]).unwrap();
}
dirs.extend(self.print_dir(dir, &mut stdout));
}
}
fn print_dir(&self, dir: String, out: &mut std::io::StdoutLock<'static>) -> Vec<String> {
let mut new_dirs = Vec::new();
for set in &self.items {
for item in set {
if item.name.starts_with(&dir) {
let scoped_name = &item.name[dir.len()..];
match scoped_name.find('/') {
Some(sep) => {
let dir_name = &scoped_name[..=sep];
if new_dirs
.iter()
.any(|d: &String| &d[dir.len()..] == dir_name)
{
continue;
}
match self.display {
Display::Normal => writeln!(
out,
"{}",
Color::Blue.bold().paint(&dir_name[..dir_name.len() - 1])
)
.unwrap(),
Display::Long => writeln!(
out,
" {} {} {} {}",
Color::White.dimmed().paint("-"),
Color::White.dimmed().paint(" -"),
Color::White.dimmed().paint("-- --- --:--"),
Color::Blue.bold().paint(&dir_name[..dir_name.len() - 1]),
)
.unwrap(),
}
new_dirs.push(format!("{}{}", dir, dir_name));
}
None => match self.display {
Display::Normal => {
writeln!(out, "{}", Color::White.paint(scoped_name)).unwrap();
}
Display::Long => {
use number_prefix::NumberPrefix;
let size_str = match NumberPrefix::decimal(item.size as f64) {
NumberPrefix::Standalone(b) => b.to_string(),
NumberPrefix::Prefixed(p, n) => {
if n < 10f64 {
format!("{:.1}{}", n, p.symbol())
} else {
format!("{:.0}{}", n, p.symbol())
}
}
};
writeln!(
out,
" {}{} {} {} {}",
if size_str.len() < 4 { " " } else { "" },
Color::Green.paint(size_str),
Color::Yellow.paint("gcs"),
Color::Blue.paint(&item.updated),
Color::White.paint(scoped_name),
)
.unwrap();
}
},
}
}
}
}
new_dirs.reverse();
new_dirs
}
}