mirror of
https://github.com/PR0M3TH3AN/Marlin.git
synced 2025-09-09 15:48:43 +00:00
Add backup prune CLI and update roadmap
This commit is contained in:
@@ -21,3 +21,4 @@
|
|||||||
| `version diff` | — |
|
| `version diff` | — |
|
||||||
| `event add` | — |
|
| `event add` | — |
|
||||||
| `event timeline` | — |
|
| `event timeline` | — |
|
||||||
|
| `backup run` | --dir, --prune, --verify, --file |
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
// src/cli.rs
|
// src/cli.rs
|
||||||
|
|
||||||
pub mod annotate;
|
pub mod annotate;
|
||||||
|
pub mod backup;
|
||||||
pub mod coll;
|
pub mod coll;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod link;
|
pub mod link;
|
||||||
@@ -73,8 +74,8 @@ pub enum Commands {
|
|||||||
exec: Option<String>,
|
exec: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Create a timestamped backup of the database
|
/// Create or manage database backups
|
||||||
Backup,
|
Backup(backup::BackupOpts),
|
||||||
|
|
||||||
/// Restore from a backup file (overwrites current DB)
|
/// Restore from a backup file (overwrites current DB)
|
||||||
Restore { backup_path: std::path::PathBuf },
|
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:
|
add:
|
||||||
args: [file, date, description]
|
args: [file, date, description]
|
||||||
timeline: {}
|
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
|
let cfg = config::Config::load()?; // resolves DB path
|
||||||
|
|
||||||
match &args.command {
|
match &args.command {
|
||||||
Commands::Init | Commands::Backup | Commands::Restore { .. } => {}
|
Commands::Init | Commands::Backup(_) | Commands::Restore { .. } => {}
|
||||||
_ => match db::backup(&cfg.db_path) {
|
_ => match db::backup(&cfg.db_path) {
|
||||||
Ok(p) => info!("Pre-command auto-backup created at {}", p.display()),
|
Ok(p) => info!("Pre-command auto-backup created at {}", p.display()),
|
||||||
Err(e) => error!("Failed to create pre-command auto-backup: {e}"),
|
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)?,
|
Commands::Search { query, exec } => run_search(&conn, &query, exec)?,
|
||||||
|
|
||||||
/* ---- maintenance ---------------------------------------- */
|
/* ---- maintenance ---------------------------------------- */
|
||||||
Commands::Backup => {
|
Commands::Backup(opts) => {
|
||||||
let p = db::backup(&cfg.db_path)?;
|
cli::backup::run(&opts, &cfg.db_path, &mut conn, args.format)?;
|
||||||
println!("Backup created: {}", p.display());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands::Restore { backup_path } => {
|
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**
|
> **Legend**
|
||||||
> **△** = engineering artefact (spec / ADR / perf target) **✦** = user-visible deliverable
|
> **△** = engineering artefact (spec / ADR / perf target) **✦** = user-visible deliverable
|
||||||
@@ -39,15 +39,20 @@
|
|||||||
|
|
||||||
### 2 · Feature cross-matrix (quick look-ups)
|
### 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
|
## 3 · Milestone acceptance checklist
|
||||||
@@ -65,8 +70,11 @@ Before a milestone is declared “shipped”:
|
|||||||
|
|
||||||
### 4 · Next immediate actions
|
### 4 · Next immediate actions
|
||||||
|
|
||||||
~~1. **Write DP-001 (Schema v1.1)** — owner @alice, due 21 May~~
|
| # | Task | Owner | Due |
|
||||||
~~2. **Set up Tarpaulin & Hyperfine jobs** — @bob, due 23 May~~
|
| - | ------------------------------ | ------ | ------------- |
|
||||||
~~3. **Spike dirty-flag logic** — @carol 2-day time-box, outcome in DP-002~~
|
| 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.*
|
> *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 })
|
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<()> {
|
pub fn restore_from_backup(&self, backup_id: &str) -> Result<()> {
|
||||||
let backup_file_path = self.backups_dir.join(backup_id);
|
let backup_file_path = self.backups_dir.join(backup_id);
|
||||||
if !backup_file_path.exists() || !backup_file_path.is_file() {
|
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.id, "backup_badformat.db");
|
||||||
assert_eq!(info.timestamp, expected_ts);
|
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