Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ Use **`bdstorage --help`** and **`bdstorage <subcommand> --help`** for the full
| 3 | `bdstorage dedupe /path/to/tree` | Vaults one copy per duplicate group and replaces the rest with reflinks (or hard links if allowed). |
| 4 (optional) | `bdstorage daemon run /path/to/tree --interval-secs 3600` | Repeats step 3 on an interval; see [Background Daemon](#background-daemon-linux-only). |
| If you need originals back | `bdstorage restore /path/to/tree` | Copies data back from the vault and breaks links; see restore flags below. |
| Check vault status | `bdstorage status` | Prints a summary of the current vault state, including space savings, tracked paths, and deduplication ratio (no filesystem scan required). |

Run **`restore`** when you want independent file copies again (for example before migrating data off the machine or when you no longer want shared extents).

Expand Down Expand Up @@ -204,6 +205,26 @@ bdstorage restore /path/to/directory

When a vault object’s refcount hits zero during restore, it is **removed** (garbage collection).

**Status** (passive vault inspection):

```bash
bdstorage status [--json]
```

| Flag | Meaning |
|:---|:---|
| `--json` | Print status report as a machine-readable JSON object (also respects global `--output-format json`). |

Example output:
```text
Vault location : ~/.imprint/store/
Objects in vault : 124
Total vault size : 1.2 GB
Tracked paths : 342
Estimated savings: 840.5 MB
Deduplication ratio: 1.70×
```

## Background Daemon (Linux Only)

### Overview
Expand Down
107 changes: 97 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ enum Commands {
#[arg(long, short = 'n')]
dry_run: bool,
},
#[command(
about = "Print a summary of the current vault state and space savings (no scan required)."
)]
Status {
#[arg(long, action = clap::ArgAction::SetTrue)]
json: bool,
},
}

#[derive(Subcommand, Debug)]
Expand Down Expand Up @@ -175,6 +182,27 @@ fn run() -> Result<()> {
};
restore_pipeline(&path, &state, dry_run, &vault_dir)?;
}
Commands::Status { json } => {
let db_path = state::db_path(&vault_dir);
if !db_path.exists() {
anyhow::bail!(
"{} No vault found.\n{}",
"[ERROR]".bold().red(),
"Run 'bdstorage dedupe <path>' first to create the vault."
);
}
let state = state::State::open_readonly_if_exists(&vault_dir)?;
let vault_path = vault::vault_root(&vault_dir);
let summary = state.compute_summary(&vault_path)?;

if json || args.output_format == OutputFormat::Json {
println!("{}", serde_json::to_string_pretty(&summary)?);
return Ok(());
}

print_vault_status(&summary);
return Ok(());
}
}

if args.output_format == OutputFormat::Json {
Expand Down Expand Up @@ -417,16 +445,6 @@ fn scan_pipeline(

let _ = db_writer_handle.join();

let mut refcount_ops = Vec::new();
for (hash, paths) in &results {
if paths.len() > 1 {
refcount_ops.push(DbOp::SetCasRefcount(*hash, paths.len() as u64));
}
}
if !refcount_ops.is_empty() {
state.batch_write(refcount_ops)?;
}

Ok(results)
}

Expand Down Expand Up @@ -840,6 +858,75 @@ fn print_summary(mode: &str, groups: &HashMap<Hash, Vec<PathBuf>>) {
println!("{mode} complete. duplicate groups: {duplicates}");
}

fn format_number(val: usize) -> String {
let s = val.to_string();
let chars: Vec<char> = s.chars().rev().collect();
let mut result = Vec::new();
for (i, c) in chars.iter().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(' ');
}
result.push(*c);
}
result.into_iter().rev().collect()
}

fn format_bytes(bytes: u64) -> String {
if bytes == 0 {
return "0 B".to_string();
}
let units = ["B", "KB", "MB", "GB", "TB", "PB"];
let mut size = bytes as f64;
let mut unit_idx = 0;
while size >= 1024.0 && unit_idx < units.len() - 1 {
size /= 1024.0;
unit_idx += 1;
}
format!("{:.1} {}", size, units[unit_idx])
}

fn print_vault_status(summary: &crate::state::VaultSummary) {
let mut vault_loc = summary.vault_location.clone();
if let Ok(home) = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE"))
&& vault_loc.starts_with(&home)
{
vault_loc = vault_loc.replacen(&home, "~", 1);
}
let mut display_loc = vault_loc.replace('\\', "/");
if !display_loc.ends_with('/') {
display_loc.push('/');
}

println!("{:<17}: {}", "Vault location", display_loc.white().bold());
println!(
"{:<17}: {}",
"Objects in vault",
format_number(summary.objects_in_vault).white().bold()
);
println!(
"{:<17}: {}",
"Total vault size",
format_bytes(summary.total_vault_size).green().bold()
);
println!(
"{:<17}: {}",
"Tracked paths",
format_number(summary.tracked_paths).white().bold()
);
println!(
"{:<17}: {}",
"Estimated savings",
format_bytes(summary.estimated_savings).green().bold()
);
println!(
"{:<17}: {}",
"Deduplication ratio",
format!("{:.2}×", summary.deduplication_ratio)
.yellow()
.bold()
);
}

fn restore_pipeline(
path: &Path,
state: &state::State,
Expand Down
Loading
Loading