use crate::db::{get_items, get_items_matching}; use crate::modes::common::ColumnType; use crate::modes::common::{size_column, string_column, get_output_format, OutputFormat}; use serde::{Deserialize, Serialize}; use serde_json; use serde_yaml; use anyhow::anyhow; use log::debug; use prettytable::color; use prettytable::row; use prettytable::format::Alignment; use prettytable::{Attr, Cell, Row, Table}; #[derive(Serialize, Deserialize)] struct ListItem { id: Option, time: String, size: Option, size_formatted: String, compression: String, file_size: Option, file_size_formatted: String, file_path: String, tags: Vec, meta: std::collections::HashMap, } pub fn mode_list( cmd: &mut clap::Command, args: &crate::Args, ids: &mut Vec, tags: &Vec, conn: &mut rusqlite::Connection, data_path: std::path::PathBuf, ) -> anyhow::Result<()> { if !ids.is_empty() { cmd.error( clap::error::ErrorKind::InvalidValue, "ID given, you can only supply tags when using --list", ) .exit(); } let mut meta: std::collections::HashMap = std::collections::HashMap::new(); for item in args.item.meta.iter() { let item = item.clone(); meta.insert(item.key, item.value); } let items = match tags.is_empty() && meta.is_empty() { true => get_items(conn)?, false => get_items_matching(conn, tags, &meta)?, }; debug!("MAIN: Items: {:?}", items); // Collect all item IDs for batch queries let item_ids: Vec = items.iter().map(|item| item.id.unwrap()).collect(); // Fetch all tags for all items in a single query let all_tags = crate::db::get_tags_for_items(conn, &item_ids)?; let mut tags_by_item: std::collections::HashMap> = std::collections::HashMap::new(); // Convert Tag structs to just names for (item_id, tags) in all_tags { let tag_names: Vec = tags.into_iter().map(|tag| tag.name).collect(); tags_by_item.insert(item_id, tag_names); } // Fetch all metadata for all items in a single query let meta_by_item = crate::db::get_meta_for_items(conn, &item_ids)?; let output_format = get_output_format(args); if output_format != OutputFormat::Table { return show_list_structured(items, tags_by_item, meta_by_item, data_path, args, output_format); } let mut table = Table::new(); table.set_format(*prettytable::format::consts::FORMAT_CLEAN); let list_format = args.options.list_format.split(","); let mut title_row = row!(); for column in list_format.clone() { let mut column_format = column.split(":"); let column_name = column_format.next().expect("Unable to parse column name"); let column_type = ColumnType::from_str(column_name) .map_err(|_| anyhow!("Unknown column {:?}", column_name))?; if column_type == ColumnType::Meta { let meta_name = column_format .next() .expect("Unable to parse metadata name for meta column"); title_row.add_cell(Cell::new(meta_name).with_style(Attr::Bold)); } else { title_row.add_cell(Cell::new(&column_type.to_string()).with_style(Attr::Bold)); } } table.set_titles(title_row); for item in items { let item_id = item.id.unwrap(); let tags = tags_by_item.get(&item_id).unwrap(); let meta = meta_by_item.get(&item_id).unwrap(); let mut item_path = data_path.clone(); item_path.push(item.id.unwrap().to_string()); let mut table_row = Row::new(vec![]); for column in list_format.clone() { let mut column_format = column.split(":"); let column_name = column_format.next().expect("Unable to parse column name"); let column_type = ColumnType::from_str(column_name) .unwrap_or_else(|_| panic!("Unknown column {:?}", column_name)); let mut meta_name: Option<&str> = None; if column_type == ColumnType::Meta { meta_name = column_format.next(); } let column_width: usize = match column_format.next() { Some(len) => len.parse().unwrap_or(0), None => 0, }; let cell = match column_type { ColumnType::Id => Cell::new_align( &string_column(item.id.unwrap_or(0).to_string(), column_width), Alignment::RIGHT, ), ColumnType::Time => Cell::new(&string_column( item.ts .with_timezone(&chrono::Local) .format("%F %T") .to_string(), column_width, )), ColumnType::Size => match item.size { Some(size) => Cell::new_align( &size_column(size as u64, args.options.human_readable, column_width), Alignment::RIGHT, ), None => match item_path.metadata() { Ok(_) => Cell::new_align("Unknown", Alignment::RIGHT) .with_style(Attr::ForegroundColor(color::YELLOW)) .with_style(Attr::Bold), Err(_) => Cell::new_align("Missing", Alignment::RIGHT) .with_style(Attr::ForegroundColor(color::RED)) .with_style(Attr::Bold), }, }, ColumnType::Compression => { Cell::new(&string_column(item.compression.to_string(), column_width)) }, ColumnType::FileSize => match item_path.metadata() { Ok(metadata) => Cell::new_align( &size_column(metadata.len(), args.options.human_readable, column_width), Alignment::RIGHT, ), Err(_) => Cell::new_align("Missing", Alignment::RIGHT) .with_style(Attr::ForegroundColor(color::RED)) .with_style(Attr::Bold), }, ColumnType::FilePath => Cell::new(&string_column( item_path.clone().into_os_string().into_string().unwrap(), column_width, )), ColumnType::Tags => Cell::new(&string_column(tags.join(" "), column_width)), ColumnType::Meta => match meta_name { Some(meta_name) => match meta.get(meta_name) { Some(meta_value) => { Cell::new(&string_column(meta_value.to_string(), column_width)) } None => Cell::new(""), }, None => Cell::new(""), }, }; table_row.add_cell(cell); } table.add_row(table_row); } table.printstd(); Ok(()) } fn show_list_structured( items: Vec, tags_by_item: std::collections::HashMap>, meta_by_item: std::collections::HashMap>, data_path: std::path::PathBuf, args: &crate::Args, output_format: OutputFormat, ) -> anyhow::Result<()> { let mut list_items = Vec::new(); for item in items { let item_id = item.id.unwrap(); let tags = tags_by_item.get(&item_id).cloned().unwrap_or_default(); let meta = meta_by_item.get(&item_id).cloned().unwrap_or_default(); let mut item_path = data_path.clone(); item_path.push(item_id.to_string()); let file_size = item_path.metadata().map(|m| m.len()).ok(); let file_size_formatted = match file_size { Some(size) => crate::modes::common::format_size(size, args.options.human_readable), None => "Missing".to_string(), }; let size_formatted = match item.size { Some(size) => crate::modes::common::format_size(size as u64, args.options.human_readable), None => "Unknown".to_string(), }; let list_item = ListItem { id: item.id, time: item.ts.with_timezone(&chrono::Local).format("%F %T").to_string(), size: item.size.map(|s| s as u64), size_formatted, compression: item.compression, file_size, file_size_formatted, file_path: item_path.into_os_string().into_string().unwrap_or_default(), tags, meta, }; list_items.push(list_item); } match output_format { OutputFormat::Json => { println!("{}", serde_json::to_string_pretty(&list_items)?); } OutputFormat::Yaml => { println!("{}", serde_yaml::to_string(&list_items)?); } OutputFormat::Table => unreachable!(), } Ok(()) }