mirror of
https://github.com/PR0M3TH3AN/Marlin.git
synced 2025-09-08 07:08:44 +00:00
Merge pull request #45 from PR0M3TH3AN/codex/review-and-update-roadmap
Implement backup prune CLI
This commit is contained in:
@@ -21,3 +21,4 @@
|
||||
| `version diff` | — |
|
||||
| `event add` | — |
|
||||
| `event timeline` | — |
|
||||
| `backup run` | --dir, --prune, --verify, --file |
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// src/cli.rs
|
||||
|
||||
pub mod annotate;
|
||||
pub mod backup;
|
||||
pub mod coll;
|
||||
pub mod event;
|
||||
pub mod link;
|
||||
@@ -73,8 +74,8 @@ pub enum Commands {
|
||||
exec: Option<String>,
|
||||
},
|
||||
|
||||
/// Create a timestamped backup of the database
|
||||
Backup,
|
||||
/// Create or manage database backups
|
||||
Backup(backup::BackupOpts),
|
||||
|
||||
/// Restore from a backup file (overwrites current DB)
|
||||
Restore { backup_path: std::path::PathBuf },
|
||||
|
67
cli-bin/src/cli/backup.rs
Normal file
67
cli-bin/src/cli/backup.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// src/cli/backup.rs
|
||||
use crate::cli::Format;
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Args;
|
||||
use libmarlin::backup::BackupManager;
|
||||
use rusqlite::Connection;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Options for the `backup` command
|
||||
#[derive(Args, Debug)]
|
||||
pub struct BackupOpts {
|
||||
/// Directory to store backups (defaults next to DB)
|
||||
#[arg(long)]
|
||||
pub dir: Option<PathBuf>,
|
||||
|
||||
/// Keep only N newest backups
|
||||
#[arg(long)]
|
||||
pub prune: Option<usize>,
|
||||
|
||||
/// Verify a backup file
|
||||
#[arg(long)]
|
||||
pub verify: bool,
|
||||
|
||||
/// Backup file to verify (used with --verify)
|
||||
#[arg(long)]
|
||||
pub file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn run(opts: &BackupOpts, db_path: &Path, _conn: &mut Connection, _fmt: Format) -> Result<()> {
|
||||
let backups_dir = opts
|
||||
.dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| db_path.parent().unwrap().join("backups"));
|
||||
let manager = BackupManager::new(db_path, &backups_dir)?;
|
||||
|
||||
if opts.verify {
|
||||
let file = opts
|
||||
.file
|
||||
.as_ref()
|
||||
.context("--file required with --verify")?;
|
||||
let name = file
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.context("invalid backup file name")?;
|
||||
let ok = manager.verify_backup(name)?;
|
||||
if ok {
|
||||
println!("Backup OK: {}", name);
|
||||
} else {
|
||||
println!("Backup corrupted: {}", name);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(n) = opts.prune {
|
||||
let result = manager.prune(n)?;
|
||||
println!(
|
||||
"Pruned {} old backups, kept {}",
|
||||
result.removed.len(),
|
||||
result.kept.len()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let info = manager.create_backup()?;
|
||||
println!("Created backup {}", info.id);
|
||||
Ok(())
|
||||
}
|
@@ -79,3 +79,9 @@ event:
|
||||
add:
|
||||
args: [file, date, description]
|
||||
timeline: {}
|
||||
|
||||
backup:
|
||||
description: "Create, prune or verify backups"
|
||||
actions:
|
||||
run:
|
||||
flags: ["--dir", "--prune", "--verify", "--file"]
|
||||
|
@@ -41,7 +41,7 @@ fn main() -> Result<()> {
|
||||
let cfg = config::Config::load()?; // resolves DB path
|
||||
|
||||
match &args.command {
|
||||
Commands::Init | Commands::Backup | Commands::Restore { .. } => {}
|
||||
Commands::Init | Commands::Backup(_) | Commands::Restore { .. } => {}
|
||||
_ => match db::backup(&cfg.db_path) {
|
||||
Ok(p) => info!("Pre-command auto-backup created at {}", p.display()),
|
||||
Err(e) => error!("Failed to create pre-command auto-backup: {e}"),
|
||||
@@ -100,9 +100,8 @@ fn main() -> Result<()> {
|
||||
Commands::Search { query, exec } => run_search(&conn, &query, exec)?,
|
||||
|
||||
/* ---- maintenance ---------------------------------------- */
|
||||
Commands::Backup => {
|
||||
let p = db::backup(&cfg.db_path)?;
|
||||
println!("Backup created: {}", p.display());
|
||||
Commands::Backup(opts) => {
|
||||
cli::backup::run(&opts, &cfg.db_path, &mut conn, args.format)?;
|
||||
}
|
||||
|
||||
Commands::Restore { backup_path } => {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Marlin ― Delivery Road-map **v3**
|
||||
# Marlin ― Delivery Road-map **v3.2**
|
||||
|
||||
*Engineering-ready version — updated 2025-05-17*
|
||||
*Engineering-ready version — updated 2025-05-18*
|
||||
|
||||
> **Legend**
|
||||
> **△** = engineering artefact (spec / ADR / perf target) **✦** = user-visible deliverable
|
||||
@@ -39,15 +39,20 @@
|
||||
|
||||
### 2 · Feature cross-matrix (quick look-ups)
|
||||
|
||||
| Capability | Sprint / Phase | CLI flag or GUI element | Linked DP |
|
||||
| ------------------------------------- | -------------- | ---------------------------------- | --------- |
|
||||
| Relationship **templates** | P7 | `template new`, `template apply` | DP-008 |
|
||||
| Positive / negative filter combinator | P6 | DSL `+tag:foo -tag:bar date>=2025` | DP-007 |
|
||||
| ~~Dirty-scan optimisation~~ | ~~E1~~ | ~~`scan --dirty`~~ | ~~DP-002~~ |
|
||||
| Watch-mode | E2 | `marlin watch .` | DP-003 |
|
||||
| Grep snippets | P3 | `search -C3 "foo"` | DP-004 |
|
||||
| Hash / dedupe | P4 | `scan --rehash` | DP-005 |
|
||||
|
||||
| Capability | Sprint / Phase | CLI / GUI element | Linked DP |
|
||||
| -------------------------- | -------------- | -------------------- | --------- |
|
||||
| Crate split & docs autogen | S0 | — | – |
|
||||
| Tarpaulin coverage gate | S0 | — | – |
|
||||
| Watch mode (FS events) | Epic 1 | `marlin watch .` | DP‑002 |
|
||||
| Backup auto‑prune | Epic 1 | `backup --prune N` | – |
|
||||
| Dirty‑scan | Epic 2 | `scan --dirty` | DP‑002 |
|
||||
| Grep snippets | Phase 3 | `search -C3 …` | DP‑004 |
|
||||
| Hash / dedupe | Phase 4 | `scan --rehash` | DP‑005 |
|
||||
| Tag aliases | Phase 5 | `tag alias` commands | DP‑006 |
|
||||
| Search DSL v2 | Phase 6 | new grammar, `--legacy-search` flag | DP‑007 |
|
||||
| Relationship templates | Phase 7 | `template new/apply` | DP‑008 |
|
||||
| TUI v1 | Phase 8 | `marlin‑tui` | DP‑009 |
|
||||
---
|
||||
|
||||
## 3 · Milestone acceptance checklist
|
||||
@@ -65,8 +70,11 @@ Before a milestone is declared “shipped”:
|
||||
|
||||
### 4 · Next immediate actions
|
||||
|
||||
~~1. **Write DP-001 (Schema v1.1)** — owner @alice, due 21 May~~
|
||||
~~2. **Set up Tarpaulin & Hyperfine jobs** — @bob, due 23 May~~
|
||||
~~3. **Spike dirty-flag logic** — @carol 2-day time-box, outcome in DP-002~~
|
||||
| # | Task | Owner | Due |
|
||||
| - | ------------------------------ | ------ | ------------- |
|
||||
| 1 | Crate split + CI baseline | @alice | **26 May 25** |
|
||||
| 2 | Tarpaulin + Hyperfine jobs | @bob | **26 May 25** |
|
||||
| 3 | **DP‑001 Schema v1.1** draft | @carol | **30 May 25** |
|
||||
| 4 | backup prune CLI + nightly job | @dave | **05 Jun 25** |
|
||||
|
||||
> *This roadmap now contains both product-level “what” and engineering-level “how/when/prove it”. It should allow a new contributor to jump in, pick the matching DP, and know exactly the bar they must clear for their code to merge.*
|
||||
|
@@ -216,6 +216,19 @@ impl BackupManager {
|
||||
Ok(PruneResult { kept, removed })
|
||||
}
|
||||
|
||||
pub fn verify_backup(&self, backup_id: &str) -> Result<bool> {
|
||||
let backup_file_path = self.backups_dir.join(backup_id);
|
||||
if !backup_file_path.exists() || !backup_file_path.is_file() {
|
||||
return Err(anyhow::Error::new(marlin_error::Error::NotFound(format!(
|
||||
"Backup file not found or is not a file: {}",
|
||||
backup_file_path.display()
|
||||
))));
|
||||
}
|
||||
let conn = rusqlite::Connection::open(&backup_file_path)?;
|
||||
let res: String = conn.query_row("PRAGMA integrity_check", [], |r| r.get(0))?;
|
||||
Ok(res == "ok")
|
||||
}
|
||||
|
||||
pub fn restore_from_backup(&self, backup_id: &str) -> Result<()> {
|
||||
let backup_file_path = self.backups_dir.join(backup_id);
|
||||
if !backup_file_path.exists() || !backup_file_path.is_file() {
|
||||
@@ -532,4 +545,18 @@ mod tests {
|
||||
assert_eq!(info.id, "backup_badformat.db");
|
||||
assert_eq!(info.timestamp, expected_ts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_backup_ok() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let live_db = tmp.path().join("live_verify.db");
|
||||
let _conn = create_valid_live_db(&live_db);
|
||||
|
||||
let backups_dir = tmp.path().join("ver_backups");
|
||||
let manager = BackupManager::new(&live_db, &backups_dir).unwrap();
|
||||
let info = manager.create_backup().unwrap();
|
||||
|
||||
let ok = manager.verify_backup(&info.id).unwrap();
|
||||
assert!(ok, "expected integrity check to pass");
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user