Fixed: - CLI help typo: "metatdata" -> "metadata" - Filter buffer OOM: check size before loading into memory Changed: - #[inline] on HTML escape helpers for hot path performance - Replaced once_cell and lazy_static with std::sync::LazyLock - Removed unused once_cell and lazy_static crate dependencies Refactored: - Added module-level doc to services/ module Documentation: - README.md: zstd is native not external, "none" -> "raw" - DESIGN.md: current schema and meta plugins section - CHANGELOG.md: Unreleased section populated
708 lines
22 KiB
Rust
708 lines
22 KiB
Rust
use crate::common::status::PathInfo;
|
|
use crate::compression_engine::CompressionType;
|
|
/// Common utilities shared across different modes in the Keep application.
|
|
///
|
|
/// This module provides helper functions for formatting, configuration parsing,
|
|
/// table creation, and environment variable handling used by various CLI modes.
|
|
///
|
|
/// # Usage
|
|
///
|
|
/// These utilities are typically used internally by mode implementations:
|
|
///
|
|
/// ```
|
|
/// # use keep::modes::common::{format_size, OutputFormat};
|
|
/// let formatted = format_size(1024, true); // "1.0K"
|
|
/// // let format = OutputFormat::from_str("json")?;
|
|
/// ```
|
|
use crate::config;
|
|
use crate::meta_plugin::MetaPluginType;
|
|
use anyhow::{Result, anyhow};
|
|
use chrono::{DateTime, Utc};
|
|
use clap::Command;
|
|
use clap::error::ErrorKind;
|
|
use comfy_table::{Attribute, Cell, ContentArrangement, Table};
|
|
use log::debug;
|
|
use regex::Regex;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::env;
|
|
use std::io::IsTerminal;
|
|
use std::str::FromStr;
|
|
use strum::IntoEnumIterator;
|
|
|
|
#[derive(Debug, Clone, strum::EnumString, strum::Display, PartialEq)]
|
|
#[strum(ascii_case_insensitive)]
|
|
/// Enum representing supported output formats for structured data.
|
|
///
|
|
/// Used to determine how to display lists, info, and status information in CLI modes.
|
|
/// Defaults to Table for human-readable output; JSON/YAML for machine parsing.
|
|
///
|
|
/// # Variants
|
|
///
|
|
/// * `Table` - Formatted table output (default).
|
|
/// * `Json` - JSON structured output.
|
|
/// * `Yaml` - YAML structured output.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # use keep::modes::common::OutputFormat;
|
|
/// # use std::str::FromStr;
|
|
/// assert_eq!(OutputFormat::from_str("json").unwrap(), OutputFormat::Json);
|
|
/// ```
|
|
pub enum OutputFormat {
|
|
Table,
|
|
Json,
|
|
Yaml,
|
|
}
|
|
|
|
static KEEP_META_RE: std::sync::LazyLock<Regex> =
|
|
std::sync::LazyLock::new(|| Regex::new(r"^KEEP_META_(.+)$").unwrap());
|
|
|
|
pub const IMPORT_FORMAT_ERROR: &str =
|
|
"Unsupported import format: {} (expected .keep.tar or .meta.yml)";
|
|
|
|
pub fn get_meta_from_env() -> HashMap<String, String> {
|
|
debug!("COMMON: Getting meta from KEEP_META_*");
|
|
let mut meta_env: HashMap<String, String> = HashMap::new();
|
|
for (key, value) in env::vars() {
|
|
if let Some(meta_name_caps) = KEEP_META_RE.captures(key.as_str()) {
|
|
let name = meta_name_caps.get(1).map(|m| m.as_str().to_string());
|
|
if let Some(name) = name {
|
|
if name != "PLUGINS" {
|
|
debug!("COMMON: Found meta: {}={}", name, value);
|
|
meta_env.insert(name, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
meta_env
|
|
}
|
|
|
|
/// Formats a file size in bytes to human-readable or raw format.
|
|
///
|
|
/// Uses the humansize crate for human-readable output with decimal units (KB, MB, etc.).
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `size` - Size in bytes as u64.
|
|
/// * `human_readable` - If true, use units like KB, MB; otherwise, raw bytes as string.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `String` - Formatted size string, e.g., "1.0K" or "1024".
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # use keep::modes::common::format_size;
|
|
/// let raw = format_size(1024, false); // "1024"
|
|
/// let human = format_size(1024, true); // "1.0K"
|
|
/// ```
|
|
pub fn format_size(size: u64, human_readable: bool) -> String {
|
|
match human_readable {
|
|
true => humansize::format_size(size, humansize::DECIMAL),
|
|
false => size.to_string(),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Clone, strum::EnumIter, strum::Display)]
|
|
#[strum(ascii_case_insensitive)]
|
|
/// Enum representing column types for table display.
|
|
///
|
|
/// Defines standard and meta columns for list/info modes. Supports "meta:<name>" for specific metadata columns.
|
|
///
|
|
/// # Variants
|
|
///
|
|
/// * `Id` - Item ID column.
|
|
/// * `Time` - Timestamp column.
|
|
/// * `Size` - Content size column.
|
|
/// * `Compression` - Compression type column.
|
|
/// * `FileSize` - On-disk file size column.
|
|
/// * `FilePath` - File path column.
|
|
/// * `Tags` - Tags column.
|
|
/// * `Meta` - Metadata column (with sub-type via string parsing).
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # use keep::modes::common::ColumnType;
|
|
/// # use std::str::FromStr;
|
|
/// assert_eq!(ColumnType::from_str("id").unwrap(), ColumnType::Id);
|
|
/// assert_eq!(ColumnType::from_str("meta:hostname").unwrap(), ColumnType::Meta);
|
|
/// ```
|
|
pub enum ColumnType {
|
|
Id,
|
|
Time,
|
|
Size,
|
|
Compression,
|
|
FileSize,
|
|
FilePath,
|
|
Tags,
|
|
Meta,
|
|
}
|
|
|
|
impl std::str::FromStr for ColumnType {
|
|
type Err = anyhow::Error;
|
|
|
|
fn from_str(s: &str) -> anyhow::Result<Self> {
|
|
let lower_s = s.to_lowercase();
|
|
if s.starts_with("meta:") {
|
|
Ok(ColumnType::Meta)
|
|
} else {
|
|
for variant in ColumnType::iter() {
|
|
if variant.to_string().to_lowercase() == lower_s {
|
|
return Ok(variant);
|
|
}
|
|
}
|
|
Err(anyhow::anyhow!("Invalid column type: {}", s))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Extracts configured meta plugin types from settings and command.
|
|
///
|
|
/// Handles comma-separated plugin names and validates against registered types.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `cmd` - Mutable Clap command for error reporting.
|
|
/// * `settings` - Application settings with plugin config.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `Vec<MetaPluginType>` - List of enabled plugin types.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Exits via Clap error if unknown plugin type specified.
|
|
pub fn settings_meta_plugin_types(
|
|
cmd: &mut Command,
|
|
settings: &config::Settings,
|
|
) -> Vec<MetaPluginType> {
|
|
let mut meta_plugin_types = Vec::new();
|
|
|
|
// Handle comma-separated values in each meta_plugins argument
|
|
for meta_plugin_names_str in &settings.meta_plugins_names() {
|
|
let meta_plugin_names: Vec<&str> = meta_plugin_names_str.split(',').collect();
|
|
|
|
for name in meta_plugin_names {
|
|
let trimmed_name = name.trim();
|
|
if trimmed_name.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Try to find the MetaPluginType by meta name
|
|
let mut found = false;
|
|
for meta_plugin_type in MetaPluginType::iter() {
|
|
if let Ok(meta_plugin) =
|
|
crate::meta_plugin::get_meta_plugin(meta_plugin_type.clone(), None, None)
|
|
&& meta_plugin.meta_type().to_string() == trimmed_name
|
|
{
|
|
meta_plugin_types.push(meta_plugin_type);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
cmd.error(
|
|
ErrorKind::InvalidValue,
|
|
format!("Unknown meta plugin type: {trimmed_name}"),
|
|
)
|
|
.exit();
|
|
}
|
|
}
|
|
}
|
|
|
|
meta_plugin_types
|
|
}
|
|
|
|
/// Determines compression type from settings and command arguments.
|
|
///
|
|
/// Validates the compression name and returns the corresponding enum variant.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `cmd` - Mutable Clap command for error reporting.
|
|
/// * `settings` - Application settings.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `CompressionType` - The resolved compression type.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Exits via Clap error if invalid compression specified.
|
|
pub fn settings_compression_type(
|
|
cmd: &mut Command,
|
|
settings: &config::Settings,
|
|
) -> CompressionType {
|
|
let compression_name = settings
|
|
.compression()
|
|
.unwrap_or(CompressionType::LZ4.to_string());
|
|
|
|
let compression_type_opt = CompressionType::from_str(&compression_name);
|
|
if compression_type_opt.is_err() {
|
|
cmd.error(
|
|
ErrorKind::InvalidValue,
|
|
format!("Invalid compression algorithm '{compression_name}'. Supported algorithms: lz4, gzip, xz, zstd"),
|
|
)
|
|
.exit();
|
|
}
|
|
|
|
compression_type_opt.unwrap()
|
|
}
|
|
|
|
/// Parses output format from settings.
|
|
///
|
|
/// Defaults to `Table` if not specified or invalid. Uses case-insensitive string parsing.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `settings` - Application settings with optional output_format field.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `OutputFormat` - Parsed enum variant or Table as default.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # use keep::modes::common::{settings_output_format, OutputFormat};
|
|
/// // Example usage requires a Settings instance
|
|
/// // let format = settings_output_format(&settings);
|
|
/// ```
|
|
pub fn settings_output_format(settings: &config::Settings) -> OutputFormat {
|
|
settings
|
|
.output_format
|
|
.as_ref()
|
|
.and_then(|s| OutputFormat::from_str(s).ok())
|
|
.unwrap_or(OutputFormat::Table)
|
|
}
|
|
|
|
/// Trims trailing whitespace from each line in a multi-line string.
|
|
///
|
|
/// Useful for cleaning up table output before printing. Preserves newlines but removes spaces/tabs at line ends.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `s` - Input string with potential trailing whitespace, e.g., "line1 \nline2 ".
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `String` - Cleaned string with trimmed lines, e.g., "line1\nline2".
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # use keep::modes::common::trim_lines_end;
|
|
/// let cleaned = trim_lines_end("line1 \nline2 ");
|
|
/// assert_eq!(cleaned, "line1\nline2");
|
|
/// ```
|
|
pub fn trim_lines_end(s: &str) -> String {
|
|
s.lines()
|
|
.map(|line| line.trim_end())
|
|
.collect::<Vec<&str>>()
|
|
.join("\n")
|
|
}
|
|
|
|
/// Creates a new table with styling based on terminal detection.
|
|
///
|
|
/// Loads appropriate preset (UTF8 or ASCII) if styling is enabled.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `use_styling` - If true, apply visual styling.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `Table` - Configured table instance.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # use keep::modes::common::create_table;
|
|
/// let mut table = create_table(true);
|
|
/// table.add_row(vec!["Header1", "Header2"]);
|
|
/// ```
|
|
pub fn create_table(_use_styling: bool) -> Table {
|
|
create_table_with_config(&crate::config::TableConfig::default())
|
|
}
|
|
|
|
/// Creates a table configured from application table settings.
|
|
///
|
|
/// Applies style presets, modifiers, content arrangement, and truncation indicators.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `table_config` - Table configuration from settings.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `Table` - Fully configured table.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # use keep::modes::common::create_table_with_config;
|
|
/// # use keep::config::TableConfig;
|
|
/// let config = TableConfig::default();
|
|
/// let table = create_table_with_config(&config);
|
|
/// ```
|
|
pub fn create_table_with_config(table_config: &crate::config::TableConfig) -> Table {
|
|
let mut table = Table::new();
|
|
|
|
// Set content arrangement
|
|
match table_config.content_arrangement {
|
|
crate::config::ContentArrangement::Dynamic => {
|
|
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic)
|
|
}
|
|
crate::config::ContentArrangement::DynamicFullWidth => {
|
|
table.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth)
|
|
}
|
|
crate::config::ContentArrangement::Disabled => {
|
|
table.set_content_arrangement(comfy_table::ContentArrangement::Disabled)
|
|
}
|
|
};
|
|
|
|
// Set style preset
|
|
match &table_config.style {
|
|
crate::config::TableStyle::Ascii => {
|
|
table.load_preset(comfy_table::presets::ASCII_FULL);
|
|
}
|
|
crate::config::TableStyle::Utf8 => {
|
|
table.load_preset(comfy_table::presets::UTF8_FULL);
|
|
}
|
|
crate::config::TableStyle::Utf8Full => {
|
|
table.load_preset(comfy_table::presets::UTF8_FULL);
|
|
}
|
|
crate::config::TableStyle::Nothing => {
|
|
table.load_preset(comfy_table::presets::NOTHING);
|
|
}
|
|
crate::config::TableStyle::Custom(preset) => {
|
|
// For custom presets, we'd need to parse the string
|
|
// This is a placeholder for custom preset handling
|
|
if preset == "ASCII_FULL" {
|
|
table.load_preset(comfy_table::presets::ASCII_FULL);
|
|
} else if preset == "UTF8_FULL" {
|
|
table.load_preset(comfy_table::presets::UTF8_FULL);
|
|
} else if preset == "NOTHING" {
|
|
table.load_preset(comfy_table::presets::NOTHING);
|
|
}
|
|
// Add more presets as needed
|
|
}
|
|
};
|
|
|
|
// Apply modifiers
|
|
for modifier in &table_config.modifiers {
|
|
match modifier.as_str() {
|
|
"UTF8_SOLID_INNER_BORDERS" => {
|
|
table.apply_modifier(comfy_table::modifiers::UTF8_SOLID_INNER_BORDERS);
|
|
}
|
|
"UTF8_ROUND_CORNERS" => {
|
|
table.apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS);
|
|
}
|
|
_ => {} // Ignore unknown modifiers
|
|
}
|
|
}
|
|
|
|
// Set truncation indicator if specified
|
|
if !table_config.truncation_indicator.is_empty() {
|
|
table.set_truncation_indicator(&table_config.truncation_indicator);
|
|
}
|
|
|
|
if !std::io::stdout().is_terminal() {
|
|
table.force_no_tty();
|
|
}
|
|
|
|
table
|
|
}
|
|
|
|
/// Display data for a single item's detail view (used by --info).
|
|
pub struct DisplayItemInfo {
|
|
pub id: i64,
|
|
pub timestamp: String,
|
|
pub path: String,
|
|
pub stream_size: String,
|
|
pub compression: String,
|
|
pub file_size: String,
|
|
pub tags: Vec<String>,
|
|
pub metadata: Vec<(String, String)>,
|
|
}
|
|
|
|
/// Renders item detail table. Shared by local and client info modes.
|
|
pub fn render_item_info_table(info: &DisplayItemInfo, table_config: &config::TableConfig) {
|
|
use comfy_table::{Attribute, Cell};
|
|
|
|
let mut table = create_table_with_config(table_config);
|
|
|
|
table.add_row(vec![
|
|
Cell::new("ID").add_attribute(Attribute::Bold),
|
|
Cell::new(info.id.to_string()),
|
|
]);
|
|
table.add_row(vec![
|
|
Cell::new("Time").add_attribute(Attribute::Bold),
|
|
Cell::new(&info.timestamp),
|
|
]);
|
|
table.add_row(vec![
|
|
Cell::new("Size").add_attribute(Attribute::Bold),
|
|
Cell::new(&info.stream_size),
|
|
]);
|
|
table.add_row(vec![
|
|
Cell::new("Compression").add_attribute(Attribute::Bold),
|
|
Cell::new(&info.compression),
|
|
]);
|
|
table.add_row(vec![
|
|
Cell::new("Tags").add_attribute(Attribute::Bold),
|
|
Cell::new(info.tags.join(" ")),
|
|
]);
|
|
|
|
for (key, value) in &info.metadata {
|
|
table.add_row(vec![
|
|
Cell::new(format!("Meta: {key}")).add_attribute(Attribute::Bold),
|
|
Cell::new(value),
|
|
]);
|
|
}
|
|
|
|
println!("{}", trim_lines_end(&table.trim_fmt()));
|
|
}
|
|
|
|
/// Renders list table with column format from config. Shared by local and client list modes.
|
|
pub fn render_list_table_with_format(
|
|
columns: &[config::ColumnConfig],
|
|
rows: &[Vec<String>],
|
|
table_config: &config::TableConfig,
|
|
) {
|
|
let mut table = create_table_with_config(table_config);
|
|
|
|
let header_cells: Vec<Cell> = columns
|
|
.iter()
|
|
.map(|col| Cell::new(&col.label).add_attribute(Attribute::Bold))
|
|
.collect();
|
|
table.set_header(header_cells);
|
|
|
|
for row in rows {
|
|
let cells: Vec<Cell> = row
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, val)| {
|
|
let mut cell = Cell::new(val);
|
|
if let Some(col) = columns.get(i) {
|
|
if let Some(ref fg) = col.fg_color {
|
|
cell = apply_color(cell, fg, true);
|
|
}
|
|
if let Some(ref bg) = col.bg_color {
|
|
cell = apply_color(cell, bg, false);
|
|
}
|
|
for attr in &col.attributes {
|
|
cell = apply_table_attribute(cell, attr);
|
|
}
|
|
}
|
|
cell
|
|
})
|
|
.collect();
|
|
table.add_row(cells);
|
|
}
|
|
|
|
println!("{}", trim_lines_end(&table.trim_fmt()));
|
|
}
|
|
|
|
/// Applies config TableColor to a comfy-table Cell.
|
|
pub fn apply_color(mut cell: Cell, color: &config::TableColor, is_foreground: bool) -> Cell {
|
|
use comfy_table::Color;
|
|
|
|
let comfy_color = match color {
|
|
config::TableColor::Black => Color::Black,
|
|
config::TableColor::Red => Color::Red,
|
|
config::TableColor::Green => Color::Green,
|
|
config::TableColor::Yellow => Color::Yellow,
|
|
config::TableColor::Blue => Color::Blue,
|
|
config::TableColor::Magenta => Color::Magenta,
|
|
config::TableColor::Cyan => Color::Cyan,
|
|
config::TableColor::White => Color::White,
|
|
config::TableColor::Gray => Color::Grey,
|
|
config::TableColor::DarkRed => Color::DarkRed,
|
|
config::TableColor::DarkGreen => Color::DarkGreen,
|
|
config::TableColor::DarkYellow => Color::DarkYellow,
|
|
config::TableColor::DarkBlue => Color::DarkBlue,
|
|
config::TableColor::DarkMagenta => Color::DarkMagenta,
|
|
config::TableColor::DarkCyan => Color::DarkCyan,
|
|
config::TableColor::Rgb(r, g, b) => Color::Rgb {
|
|
r: *r,
|
|
g: *g,
|
|
b: *b,
|
|
},
|
|
};
|
|
|
|
if is_foreground {
|
|
cell = cell.fg(comfy_color);
|
|
} else {
|
|
cell = cell.bg(comfy_color);
|
|
}
|
|
|
|
cell
|
|
}
|
|
|
|
/// Ensures tags has at least one entry, adding "none" if empty.
|
|
pub fn ensure_default_tag(tags: &mut Vec<String>) {
|
|
if tags.is_empty() {
|
|
tags.push("none".to_string());
|
|
}
|
|
}
|
|
|
|
/// Prints a serializable value in JSON or YAML format based on output format.
|
|
///
|
|
/// Only handles Json and Yaml variants; Table should be handled separately.
|
|
pub fn print_serialized<T: serde::Serialize>(
|
|
value: &T,
|
|
format: &OutputFormat,
|
|
) -> anyhow::Result<()> {
|
|
match format {
|
|
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(value)?),
|
|
OutputFormat::Yaml => println!("{}", serde_yaml::to_string(value)?),
|
|
OutputFormat::Table => unreachable!(),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Applies config TableAttribute to a comfy-table Cell.
|
|
pub fn apply_table_attribute(mut cell: Cell, attribute: &config::TableAttribute) -> Cell {
|
|
match attribute {
|
|
config::TableAttribute::Bold => cell = cell.add_attribute(Attribute::Bold),
|
|
config::TableAttribute::Dim => cell = cell.add_attribute(Attribute::Dim),
|
|
config::TableAttribute::Italic => cell = cell.add_attribute(Attribute::Italic),
|
|
config::TableAttribute::Underlined => cell = cell.add_attribute(Attribute::Underlined),
|
|
config::TableAttribute::SlowBlink => cell = cell.add_attribute(Attribute::SlowBlink),
|
|
config::TableAttribute::RapidBlink => cell = cell.add_attribute(Attribute::RapidBlink),
|
|
config::TableAttribute::Reverse => cell = cell.add_attribute(Attribute::Reverse),
|
|
config::TableAttribute::Hidden => cell = cell.add_attribute(Attribute::Hidden),
|
|
config::TableAttribute::CrossedOut => cell = cell.add_attribute(Attribute::CrossedOut),
|
|
}
|
|
|
|
cell
|
|
}
|
|
|
|
/// Builds a table showing data and database path information.
|
|
pub fn build_path_table(path_info: &PathInfo, table_config: &config::TableConfig) -> Table {
|
|
let mut path_table = create_table_with_config(table_config);
|
|
|
|
path_table.set_header(vec![
|
|
Cell::new("Type").add_attribute(Attribute::Bold),
|
|
Cell::new("Path").add_attribute(Attribute::Bold),
|
|
]);
|
|
|
|
path_table.add_row(vec!["Data", &path_info.data]);
|
|
path_table.add_row(vec!["Database", &path_info.database]);
|
|
|
|
path_table
|
|
}
|
|
|
|
/// Sanitize tags for use in filenames.
|
|
///
|
|
/// Replaces non-alphanumeric characters with underscores and joins with `_`.
|
|
/// Empty tags are filtered out to avoid double underscores.
|
|
pub fn sanitize_tags(tags: &[String]) -> String {
|
|
tags.iter()
|
|
.filter(|t| !t.is_empty())
|
|
.map(|t| {
|
|
t.chars()
|
|
.map(|c| if c.is_alphanumeric() { c } else { '_' })
|
|
.collect::<String>()
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("_")
|
|
}
|
|
|
|
/// Metadata structure for export to YAML. Shared by local and client export modes.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ExportMeta {
|
|
pub ts: DateTime<Utc>,
|
|
pub compression: String,
|
|
pub uncompressed_size: Option<i64>,
|
|
pub tags: Vec<String>,
|
|
pub metadata: HashMap<String, String>,
|
|
}
|
|
|
|
/// Metadata structure for import from YAML. Shared by local and client import modes.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ImportMeta {
|
|
pub ts: DateTime<Utc>,
|
|
pub compression: String,
|
|
#[serde(default, alias = "size")]
|
|
pub uncompressed_size: Option<i64>,
|
|
#[serde(default)]
|
|
pub tags: Vec<String>,
|
|
#[serde(default)]
|
|
pub metadata: HashMap<String, String>,
|
|
}
|
|
|
|
/// Resolve a single item ID from explicit IDs, tags, or latest item.
|
|
///
|
|
/// Returns the first ID if provided, the newest item matching tags,
|
|
/// or the newest item overall if neither is specified.
|
|
pub fn resolve_item_id(
|
|
client: &crate::client::KeepClient,
|
|
ids: &[i64],
|
|
tags: &[String],
|
|
) -> Result<i64> {
|
|
if !ids.is_empty() {
|
|
Ok(ids[0])
|
|
} else if !tags.is_empty() {
|
|
let items = client.list_items(&[], tags, "newest", 0, 1, &HashMap::new())?;
|
|
if items.is_empty() {
|
|
return Err(anyhow!("No items found matching tags: {:?}", tags));
|
|
}
|
|
Ok(items[0].id)
|
|
} else {
|
|
let items = client.list_items(&[], &[], "newest", 0, 1, &HashMap::new())?;
|
|
if items.is_empty() {
|
|
return Err(anyhow!("No items found"));
|
|
}
|
|
Ok(items[0].id)
|
|
}
|
|
}
|
|
|
|
/// Resolve item IDs from explicit IDs or tags (multi-item variant).
|
|
pub fn resolve_item_ids(
|
|
client: &crate::client::KeepClient,
|
|
ids: &[i64],
|
|
tags: &[String],
|
|
) -> Result<Vec<i64>> {
|
|
if !ids.is_empty() {
|
|
Ok(ids.to_vec())
|
|
} else if !tags.is_empty() {
|
|
let items = client.list_items(&[], tags, "newest", 0, 0, &HashMap::new())?;
|
|
if items.is_empty() {
|
|
return Err(anyhow!("No items found matching tags: {:?}", tags));
|
|
}
|
|
Ok(items.into_iter().map(|i| i.id).collect())
|
|
} else {
|
|
let items = client.list_items(&[], &[], "newest", 0, 1, &HashMap::new())?;
|
|
if items.is_empty() {
|
|
return Err(anyhow!("No items found"));
|
|
}
|
|
Ok(vec![items[0].id])
|
|
}
|
|
}
|
|
|
|
/// Check if binary content should be blocked from TTY output.
|
|
///
|
|
/// Uses metadata `text` field as fast path, then falls back to byte sampling.
|
|
/// Returns Err if content is binary and should not be displayed.
|
|
pub fn check_binary_tty(
|
|
metadata: &HashMap<String, String>,
|
|
data_sample: &[u8],
|
|
force: bool,
|
|
) -> Result<()> {
|
|
if force || !std::io::stdout().is_terminal() {
|
|
return Ok(());
|
|
}
|
|
if crate::common::is_binary::is_content_binary_from_metadata(metadata, data_sample) {
|
|
return Err(anyhow!(
|
|
"Refusing to output binary data to TTY, use --force to override"
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|