use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, Result};
use crate::domain::{parse_root_spec, parse_root_spec_any, Root};
use crate::repo::{self, Db};
pub fn list(db: &Db, scope: Option<&Path>, suspended_only: bool) -> Result<()> {
let conn = db.conn();
let scope_str = match scope {
Some(p) => Some(
fs::canonicalize(p)
.map_err(|e| anyhow::anyhow!("Failed to resolve path '{}': {}", p.display(), e))?
.to_string_lossy()
.to_string(),
),
None => None,
};
let all_roots = repo::root::fetch_all(conn)?;
let filtered_roots: Vec<&Root> = all_roots
.iter()
.filter(|r| {
if suspended_only {
r.is_suspended()
} else {
r.is_active()
}
})
.filter(|r| {
match &scope_str {
Some(scope) => r.matches_scope(scope),
None => true,
}
})
.collect();
if filtered_roots.is_empty() {
if scope.is_some() {
println!("No roots at or beneath this path.");
} else {
println!("No roots registered. Use `canon scan --add --role <source|archive> <path>` to add one.");
}
return Ok(());
}
let root_ids: Vec<i64> = filtered_roots.iter().map(|r| r.id).collect();
let file_counts = repo::root::fetch_file_counts(conn, &root_ids)?;
println!(
"{:<4} {:<8} {:>8} {:<16} PATH",
"ID", "ROLE", "FILES", "LAST SCAN"
);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
for root in filtered_roots {
let file_count = file_counts.get(&root.id).copied().unwrap_or(0);
let scan_ago = format_time_ago(root.last_scanned_at, now);
let suspended_marker = if root.is_suspended() {
" [suspended]"
} else {
""
};
let path_with_info = match &root.comment {
Some(c) => format!("{}{} ({})", root.path, suspended_marker, c),
None => format!("{}{}", root.path, suspended_marker),
};
println!(
"{:<4} {:<8} {:>8} {:<16} {}",
root.id, root.role, file_count, scan_ago, path_with_info
);
}
Ok(())
}
fn format_time_ago(timestamp: Option<i64>, now: i64) -> String {
match timestamp {
None => "never".to_string(),
Some(ts) => {
let secs = now - ts;
if secs < 0 {
"just now".to_string()
} else if secs < 60 {
format!("{secs}s ago")
} else if secs < 3600 {
format!("{}m ago", secs / 60)
} else if secs < 86400 {
format!("{}h ago", secs / 3600)
} else {
format!("{}d ago", secs / 86400)
}
}
}
}
pub fn remove(db: &Db, spec: &str, yes: bool) -> Result<()> {
let conn = db.conn();
let roots = repo::root::fetch_all(conn)?;
let root_id = parse_root_spec(&roots, spec, None)?;
let root = roots.iter().find(|r| r.id == root_id).unwrap();
let sources = repo::source::batch_fetch_by_roots(conn, &[root_id])?;
let source_count = sources.len() as i64;
let object_ids: Vec<i64> = sources.iter().filter_map(|s| s.object_id).collect();
let archived_objects = repo::object::batch_check_archived(conn, &object_ids, None)?;
let in_archive_count = sources
.iter()
.filter(|s| {
s.object_id
.map(|id| archived_objects.contains(&id))
.unwrap_or(false)
})
.count() as i64;
let not_in_archive = source_count - in_archive_count;
if !yes {
eprintln!("About to remove {} root: {}", root.role, root.path);
eprintln!(
"This will forget {source_count} sources ({in_archive_count} in archive, {not_in_archive} not in archive)."
);
eprintln!("Files on disk will NOT be deleted.");
eprintln!();
eprintln!("To see which sources will be forgotten:");
eprintln!(" canon ls {}", root.path);
eprintln!();
eprint!("Proceed? [y/N] ");
io::stderr().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
bail!("Aborted");
}
}
let deleted_sources = repo::root::remove(conn, root_id)?;
println!("Removed root {root_id} and {deleted_sources} sources");
Ok(())
}
pub fn set_comment(db: &Db, spec: &str, comment: Option<&str>) -> Result<()> {
let conn = db.conn();
let roots = repo::root::fetch_all(conn)?;
let root_id = parse_root_spec(&roots, spec, None)?;
repo::root::set_comment(conn, root_id, comment)?;
match comment {
Some(c) => println!("Set comment on root {root_id}: {c}"),
None => println!("Cleared comment on root {root_id}"),
}
Ok(())
}
pub fn suspend(db: &Db, spec: &str) -> Result<()> {
let conn = db.conn();
let roots = repo::root::fetch_all(conn)?;
let root_id = parse_root_spec_any(&roots, spec)?;
let root = roots.iter().find(|r| r.id == root_id).unwrap();
if root.is_suspended() {
println!("Root {} is already suspended: {}", root_id, root.path);
return Ok(());
}
repo::root::set_suspended(conn, root_id, true)?;
println!("Suspended root {}: {}", root_id, root.path);
Ok(())
}
pub fn unsuspend(db: &Db, spec: &str) -> Result<()> {
let conn = db.conn();
let roots = repo::root::fetch_all(conn)?;
let root_id = parse_root_spec_any(&roots, spec)?;
let root = roots.iter().find(|r| r.id == root_id).unwrap();
if !root.is_suspended() {
println!("Root {} is not suspended: {}", root_id, root.path);
return Ok(());
}
repo::root::set_suspended(conn, root_id, false)?;
println!("Unsuspended root {}: {}", root_id, root.path);
Ok(())
}