Files
keep/src/modes/list.rs
Andrew Phillips 30d7836bcf refactor: deduplicate ItemInfo, improve error handling, fix pre-existing bugs
- Move ItemInfo to services/types.rs for sharing between client and server
- Replace .expect() in compression_service with proper error handling
- Add CoreError::PayloadTooLarge variant for semantic error handling
- Export CoreError from lib.rs for library users
- Unify get_item_meta_name/value to take &str instead of String
- Extract item_path() helper in ItemService to reduce duplication
- Add warning logs for silent errors in list.rs
- Fix pre-existing borrow errors: tx moved in export handler,
  item_with_meta partial move in TryFrom implementation
- Fix unused data_dir variables in server code
2026-03-21 10:43:26 -03:00

315 lines
11 KiB
Rust

/// List mode implementation.
///
/// This module provides the functionality to list stored items with customizable
/// formatting, filtering by tags, and support for different output formats
/// including table, JSON, and YAML.
use crate::config;
use crate::modes::common::ColumnType;
use crate::modes::common::{OutputFormat, apply_color, apply_table_attribute, format_size};
use crate::services::item_service::ItemService;
use crate::services::types::ItemWithMeta;
use anyhow::{Context, Result};
use comfy_table::CellAlignment;
use comfy_table::{Attribute, Cell, Color, Row};
use serde::{Deserialize, Serialize};
use serde_json;
use serde_yaml;
/// Structure representing a list item for structured output formats.
///
/// This struct holds all the information needed to serialize an item for JSON or
/// YAML output in list mode.
#[derive(Serialize, Deserialize)]
struct ListItem {
/// Item ID.
///
/// The unique identifier for the item.
id: Option<i64>,
/// Timestamp.
///
/// The formatted timestamp string for the item.
time: String,
/// Size in bytes.
///
/// The raw size of the item content.
size: Option<u64>,
/// Formatted size.
///
/// Human-readable size string.
size_formatted: String,
/// Compression type.
///
/// The compression algorithm used for the item.
compression: String,
/// File size in bytes.
///
/// The size of the stored file on disk.
file_size: Option<u64>,
/// Formatted file size.
///
/// Human-readable file size string.
file_size_formatted: String,
/// File path.
///
/// The full path to the item's storage file.
file_path: String,
/// Tags.
///
/// Vector of tag names associated with the item.
tags: Vec<String>,
/// Metadata.
///
/// HashMap of metadata key-value pairs.
meta: std::collections::HashMap<String, String>,
}
/// Main list mode function.
///
/// This function handles the listing of items based on tags, applying formatting
/// and output options from settings. It supports table, JSON, and YAML output formats.
///
/// # Arguments
///
/// * `cmd` - Mutable reference to the Clap command for error handling.
/// * `settings` - Reference to application settings.
/// * `ids` - Mutable vector of item IDs (should be empty for list mode).
/// * `tags` - Reference to vector of tags for filtering.
/// * `conn` - Mutable reference to database connection.
/// * `data_path` - Path to the data directory.
///
/// # Returns
///
/// * `Result<()>` - Success or error if listing fails.
pub fn mode_list(
_cmd: &mut clap::Command,
settings: &config::Settings,
ids: &mut [i64],
tags: &[String],
conn: &mut rusqlite::Connection,
data_path: std::path::PathBuf,
) -> Result<()> {
let item_service = ItemService::new(data_path.clone());
let items_with_meta = item_service.get_items(conn, ids, tags, &settings.meta_filter())?;
if settings.ids_only {
for item_with_meta in &items_with_meta {
if let Some(id) = item_with_meta.item.id {
println!("{id}");
}
}
return Ok(());
}
let output_format = crate::modes::common::settings_output_format(settings);
if output_format != OutputFormat::Table {
return show_list_structured(items_with_meta, data_path, settings, output_format);
}
let mut table = crate::modes::common::create_table_with_config(&settings.table_config);
// Create header row
let mut header_cells = Vec::new();
for column in &settings.list_format {
header_cells.push(Cell::new(&column.label).add_attribute(Attribute::Bold));
}
table.set_header(header_cells);
for item_with_meta in items_with_meta {
let tags = item_with_meta.tag_names();
let meta = item_with_meta.meta_as_map();
let item = item_with_meta.item;
let mut item_path = data_path.clone();
item_path.push(item.id.context("Item missing ID")?.to_string());
let mut table_row = Row::new();
for column in &settings.list_format {
let column_type = column
.name
.parse::<ColumnType>()
.with_context(|| format!("Unknown column type {:?} in list format", column.name))?;
let mut meta_name: Option<&str> = None;
if let ColumnType::Meta = column_type {
let parts: Vec<&str> = column.name.split(':').collect();
if parts.len() > 1 {
meta_name = Some(parts[1]);
}
}
let cell_content = match column_type {
ColumnType::Id => item.id.unwrap_or(0).to_string(),
ColumnType::Time => item
.ts
.with_timezone(&chrono::Local)
.format("%F %T")
.to_string(),
ColumnType::Size => match item.uncompressed_size {
Some(size) => format_size(size as u64, settings.human_readable),
None => match item_path.metadata() {
Ok(_) => "Unknown".to_string(),
Err(e) => {
log::warn!("File missing or inaccessible: {}", e);
"Missing".to_string()
}
},
},
ColumnType::Compression => item.compression.to_string(),
ColumnType::FileSize => match item_path.metadata() {
Ok(metadata) => format_size(metadata.len(), settings.human_readable),
Err(e) => {
log::warn!("File missing or inaccessible: {}", e);
"Missing".to_string()
}
},
ColumnType::FilePath => item_path
.clone()
.into_os_string()
.into_string()
.unwrap_or_else(|os| os.to_string_lossy().into_owned()),
ColumnType::Tags => tags.join(" "),
ColumnType::Meta => match meta_name {
Some(meta_name) => match meta.get(meta_name) {
Some(meta_value) => meta_value.to_string(),
None => "".to_string(),
},
None => "".to_string(),
},
};
// Truncate content to max 3 lines
let mut cell_lines: Vec<String> =
cell_content.split('\n').map(|s| s.to_string()).collect();
if cell_lines.len() > 3 {
cell_lines.truncate(3);
// Add ellipsis to the last line if we truncated
if let Some(last_line) = cell_lines.last_mut() {
if last_line.len() > 3 {
last_line.truncate(last_line.len() - 3);
}
last_line.push_str("...");
}
}
let truncated_content = cell_lines.join("\n");
let mut cell = Cell::new(truncated_content);
// Apply column-specific styling
if let Some(fg_color) = &column.fg_color {
cell = apply_color(cell, fg_color, true);
}
if let Some(bg_color) = &column.bg_color {
cell = apply_color(cell, bg_color, false);
}
for attribute in &column.attributes {
cell = apply_table_attribute(cell, attribute);
}
// Apply padding if specified
if let Some((_left_padding, _right_padding)) = column.padding {
// Note: comfy-table doesn't directly support padding, so we'd need to handle this
// by adding spaces to the content, or use a different approach
}
// Apply styling for specific cases
match column_type {
ColumnType::Size => {
if item.uncompressed_size.is_none() {
if item_path.metadata().is_ok() {
cell = cell
.fg(comfy_table::Color::Yellow)
.add_attribute(Attribute::Bold);
} else {
cell = cell
.fg(comfy_table::Color::Red)
.add_attribute(Attribute::Bold);
}
}
}
ColumnType::FileSize => {
if item_path.metadata().is_err() {
cell = cell
.fg(comfy_table::Color::Red)
.add_attribute(Attribute::Bold);
}
}
_ => {}
}
// Apply alignment
cell = match column.align {
crate::config::ColumnAlignment::Right => cell.set_alignment(CellAlignment::Right),
crate::config::ColumnAlignment::Left => cell.set_alignment(CellAlignment::Left),
crate::config::ColumnAlignment::Center => cell.set_alignment(CellAlignment::Center),
};
table_row.add_cell(cell);
}
table.add_row(table_row);
}
println!(
"{}",
crate::modes::common::trim_lines_end(&table.trim_fmt())
);
Ok(())
}
fn show_list_structured(
items_with_meta: Vec<ItemWithMeta>,
data_path: std::path::PathBuf,
settings: &config::Settings,
output_format: OutputFormat,
) -> Result<()> {
let mut list_items = Vec::new();
for item_with_meta in items_with_meta {
let tags = item_with_meta.tag_names();
let meta = item_with_meta.meta_as_map();
let item = item_with_meta.item;
let item_id = item.id.context("Item missing ID")?;
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, settings.human_readable),
None => "Missing".to_string(),
};
let size_formatted = match item.uncompressed_size {
Some(size) => crate::modes::common::format_size(size as u64, settings.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.uncompressed_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);
}
crate::modes::common::print_serialized(&list_items, &output_format)?;
Ok(())
}