use std::path::Path;
use anyhow::Result;
use crate::cli::formats::OutputFormat;
use crate::cli::formats::ProgressFormat;
use crate::cli::output::print_json_line;
use crate::cli::presentation::emit_json_or_text;
use crate::cli::presentation::{emit_image_optimization_output, emit_storage_compact_output};
use crate::cli::schema::{
StorageArgs, StorageCommand, StorageCompactArgs, StorageImageCandidatesArgs,
StorageOptimizeImagesArgs,
};
use crate::db::{ImageOptimizationCandidateSummary, ImageOptimizationReport};
use super::mutation_support::require_text_or_json;
use super::notify::notify_app_refresh;
use super::runtime::{open_read_only_db, open_read_write_db};
pub(in crate::cli) fn storage(db_path: &Path, args: &StorageArgs) -> Result<()> {
match &args.command {
StorageCommand::Compact(args) => storage_compact(db_path, args),
StorageCommand::ImageCandidates(args) => storage_image_candidates(db_path, args),
StorageCommand::OptimizeImages(args) => storage_optimize_images(db_path, args),
StorageCommand::BuildPreviews(args) => storage_optimize_images(db_path, args),
StorageCommand::PreviewStatus(output) => {
let format = require_text_or_json(output.resolved()?, "storage preview-status")?;
let db = open_read_only_db(db_path)?;
let value = db.image_preview_status()?;
emit_json_or_text(matches!(format, OutputFormat::Json), &value, |status| {
format!(
"ready={} skipped={} bytes={}\n",
status.ready, status.skipped, status.bytes
)
})
}
}
}
fn storage_compact(db_path: &Path, args: &StorageCompactArgs) -> Result<()> {
let format = require_text_or_json(args.output.resolved()?, "storage compact")?;
let mut db = open_read_write_db(db_path)?;
let report = db.compact_storage(args.dry_run)?;
emit_storage_compact_output(format, &report)?;
if !report.dry_run && report.completed {
notify_app_refresh();
}
Ok(())
}
fn storage_optimize_images(db_path: &Path, args: &StorageOptimizeImagesArgs) -> Result<()> {
if matches!(args.progress, Some(ProgressFormat::Jsonl)) {
let mut db = open_read_write_db(db_path)?;
db.optimize_images_with_progress(args.dry_run, args.limit, !args.no_compact, |event| {
print_json_line(&event)
})?;
if !args.dry_run {
notify_app_refresh();
}
return Ok(());
}
let format = require_text_or_json(args.output.resolved()?, "storage optimize-images")?;
let mut db = open_read_write_db(db_path)?;
let report = db.optimize_images(args.dry_run, args.limit, !args.no_compact)?;
emit_image_optimization_output(format, &report)?;
if should_notify_after_image_optimization(&report) {
notify_app_refresh();
}
Ok(())
}
fn should_notify_after_image_optimization(report: &ImageOptimizationReport) -> bool {
!report.dry_run && report.compressed_rows > 0
}
fn storage_image_candidates(db_path: &Path, args: &StorageImageCandidatesArgs) -> Result<()> {
let format = require_text_or_json(args.output.resolved()?, "storage image-candidates")?;
let db = open_read_only_db(db_path)?;
let candidates = db.image_optimization_candidate_summaries(args.limit)?;
match format {
OutputFormat::Json => {
emit_json_or_text(true, &candidates, |rows| render_image_candidates_text(rows))
}
OutputFormat::Human | OutputFormat::Text => {
print!("{}", render_image_candidates_text(&candidates));
Ok(())
}
_ => unreachable!("unsupported storage image-candidates format should be rejected earlier"),
}
}
fn render_image_candidates_text(candidates: &[ImageOptimizationCandidateSummary]) -> String {
if candidates.is_empty() {
return "image candidates=0\n".to_string();
}
let mut out = format!("image candidates={}\n", candidates.len());
for candidate in candidates {
out.push_str(&format!(
"snapshot={} item={} uti={} bytes={}\n",
candidate.snapshot_id(),
candidate.item_index(),
candidate.uti(),
candidate.byte_len()
));
}
out
}
#[cfg(test)]
mod tests {
use super::should_notify_after_image_optimization;
use crate::db::ImageOptimizationReport;
fn report(dry_run: bool, compressed_rows: usize, compact_run: bool) -> ImageOptimizationReport {
ImageOptimizationReport {
dry_run,
format: "webp_lossless",
scanned_rows: 0,
compressed_rows,
skipped_rows: 0,
conflict_count: 0,
original_bytes: 0,
optimized_bytes: 0,
logical_saved_bytes: 0,
compact_run,
compact: None,
compact_error: None,
filesystem_saved_bytes: 0,
filesystem_growth_bytes: 0,
compact_recommended: false,
}
}
#[test]
fn image_optimization_notify_predicate_requires_a_new_preview() {
assert!(!should_notify_after_image_optimization(&report(
false, 0, true
)));
assert!(should_notify_after_image_optimization(&report(
false, 1, false
)));
assert!(!should_notify_after_image_optimization(&report(
true, 1, true
)));
assert!(!should_notify_after_image_optimization(&report(
false, 0, false
)));
}
}