Skip to content

Commit 9760c6c

Browse files
authored
Backing up database file via the cli #704 (#712)
## Description Feature]: Backing up database file via the CLI #704 ## Related Issues #704 Closes # ## Checklist when merging to main <!-- Mark items with "x" when completed --> - [x] No compiler warnings (if applicable) - [x] Code is formatted with `rustfmt` - [x] No useless or dead code (if applicable) - [x] Code is easy to understand - [x] Doc comments are used for all functions, enums, structs, and fields (where appropriate) - [x] All tests pass - [x] Performance has not regressed (assuming change was not to fix a bug) - [x] Version number has been updated in `helix-cli/Cargo.toml` and `helixdb/Cargo.toml` ## Additional Notes Implemented a backup CLI to back up the selected instance using the command: cargo run --bin helix backup dev --output <output-path>. This addresses the issue mentioned in the feature request: "Backing up database file via the CLI" (#704). If a path is not provided, the CLI will create a directory in the current path with a timestamp. If the file size exceeds 10GB, the CLI will prompt for confirmation before proceeding. Additionally, if read/write permissions are unavailable, it will print a permission error message. <!-- greptile_comment --> <h3>Greptile Summary</h3> - Implements `helix backup` CLI command to copy instance database files (`data.mdb` and `lock.mdb`) to a backup directory with optional custom output path - Adds size warnings for backups over 10GB and permission checks before copying files <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | helix-cli/src/commands/backup.rs | New backup command implementation with permission checks and size warnings; missing lock_file existence check before metadata call | </details> </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant User participant CLI as "helix backup" participant ProjectContext participant FileSystem User->>CLI: "helix backup <instance> --output <path>" CLI->>ProjectContext: "find_and_load()" ProjectContext-->>CLI: "project context" CLI->>ProjectContext: "get_instance(instance_name)" ProjectContext-->>CLI: "instance config" CLI->>FileSystem: "check data_file.exists()" FileSystem-->>CLI: "exists" CLI->>FileSystem: "metadata(data_file)" FileSystem-->>CLI: "file size" CLI->>FileSystem: "metadata(lock_file)" FileSystem-->>CLI: "file size" alt size > 10GB CLI->>User: "Warn: size is X GB" CLI->>User: "Confirm: continue?" User-->>CLI: "yes/no" alt user cancels CLI->>User: "Backup aborted" end end CLI->>FileSystem: "check_read_write_permission(data_file)" FileSystem-->>CLI: "permissions ok" CLI->>FileSystem: "check_read_write_permission(lock_file)" FileSystem-->>CLI: "permissions ok" CLI->>FileSystem: "copy(data_file)" FileSystem-->>CLI: "copied" CLI->>FileSystem: "copy(lock_file)" FileSystem-->>CLI: "copied" CLI->>User: "Backup created successfully" ``` </details> <!-- greptile_other_comments_section --> <!-- /greptile_comment -->
2 parents c3fec0a + bbf7c1e commit 9760c6c

File tree

5 files changed

+129
-1
lines changed

5 files changed

+129
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

helix-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dotenvy = "0.15.7"
2525
tokio-tungstenite = "0.27.0"
2626
futures-util = "0.3.31"
2727
regex = "1.11.2"
28+
heed3 = "0.22.0"
2829
open = "5.3"
2930

3031
[dev-dependencies]

helix-cli/src/commands/backup.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use crate::project::ProjectContext;
2+
use crate::utils::{print_confirm, print_status, print_success, print_warning};
3+
use eyre::Result;
4+
use heed3::{CompactionOption, EnvFlags, EnvOpenOptions};
5+
use std::fs;
6+
use std::fs::create_dir_all;
7+
use std::path::Path;
8+
use std::path::PathBuf;
9+
10+
pub async fn run(output: Option<PathBuf>, instance_name: String) -> Result<()> {
11+
// Load project context
12+
let project = ProjectContext::find_and_load(None)?;
13+
14+
// Get instance config
15+
let _instance_config = project.config.get_instance(&instance_name)?;
16+
17+
print_status("BACKUP", &format!("Backing up instance '{instance_name}'"));
18+
19+
// Get the instance volume
20+
let volumes_dir = project
21+
.root
22+
.join(".helix")
23+
.join(".volumes")
24+
.join(&instance_name)
25+
.join("user");
26+
27+
let data_file = volumes_dir.join("data.mdb");
28+
let lock_file = volumes_dir.join("lock.mdb");
29+
30+
let env_path = Path::new(&volumes_dir);
31+
32+
// Validate existence of environment
33+
if !env_path.exists() {
34+
return Err(eyre::eyre!(
35+
"Instance LMDB environment not found at {:?}",
36+
env_path
37+
));
38+
}
39+
40+
// Check existence of data_file before calling metadata()
41+
if !data_file.exists() {
42+
return Err(eyre::eyre!(
43+
"instance data file not found at {:?}",
44+
data_file
45+
));
46+
}
47+
48+
// Check existence of lock_file before calling metadata()
49+
if !lock_file.exists() {
50+
return Err(eyre::eyre!(
51+
"instance lock file not found at {:?}",
52+
lock_file
53+
));
54+
}
55+
56+
// Get path to backup instance
57+
let backup_dir = match output {
58+
Some(path) => path,
59+
None => {
60+
let ts = chrono::Local::now()
61+
.format("backup-%Y%m%d-%H%M%S")
62+
.to_string();
63+
project.root.join("backups").join(ts)
64+
}
65+
};
66+
67+
create_dir_all(&backup_dir)?;
68+
69+
// Get the size of the data
70+
let total_size = fs::metadata(&data_file)?.len() + std::fs::metadata(&lock_file)?.len();
71+
72+
const TEN_GB: u64 = 10 * 1024 * 1024 * 1024;
73+
74+
// Check and warn if file is greater than 10 GB
75+
76+
if total_size > TEN_GB {
77+
let size_gb = (total_size as f64) / (1024.0 * 1024.0 * 1024.0);
78+
print_warning(&format!(
79+
"Backup size is {:.2} GB. Taking atomic snapshot… this may take time depending on DB size",
80+
size_gb
81+
));
82+
let confirmed = print_confirm("Do you want to continue?");
83+
if !confirmed? {
84+
print_status("CANCEL", "Backup aborted by user");
85+
return Ok(());
86+
}
87+
}
88+
89+
// Open LMDB read-only snapshot environment
90+
let env = unsafe {
91+
EnvOpenOptions::new()
92+
.flags(EnvFlags::READ_ONLY)
93+
.max_dbs(200)
94+
.max_readers(200)
95+
.open(env_path)?
96+
};
97+
98+
println!("Copying {:?} → {:?}", &data_file, &backup_dir);
99+
100+
// backup database to given database
101+
env.copy_to_path(backup_dir.join("data.mdb"), CompactionOption::Disabled)?;
102+
env.copy_to_path(backup_dir.join("lock.mdb"), CompactionOption::Disabled)?;
103+
104+
print_success(&format!(
105+
"Backup for '{instance_name}' created at {:?}",
106+
backup_dir
107+
));
108+
109+
Ok(())
110+
}

helix-cli/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod add;
22
pub mod auth;
3+
pub mod backup;
34
pub mod build;
45
pub mod check;
56
pub mod compile;

helix-cli/src/main.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use clap::{Parser, Subcommand};
22
use eyre::Result;
3+
use std::path::PathBuf;
34
use helix_cli::{AuthAction, CloudDeploymentTypeCommand, DashboardAction, MetricsAction};
45

6+
57
mod cleanup;
68
mod commands;
79
mod config;
@@ -163,6 +165,16 @@ enum Commands {
163165
#[clap(long)]
164166
no_backup: bool,
165167
},
168+
169+
/// Backup instance at the given path
170+
Backup {
171+
/// Instance name to backup
172+
instance: String,
173+
174+
/// Output directory for the backup. If omitted, ./backups/backup-<ts>/ will be used
175+
#[arg(short, long)]
176+
output: Option<PathBuf>,
177+
},
166178
}
167179

168180
#[tokio::main]
@@ -212,7 +224,10 @@ async fn main() -> Result<()> {
212224
port,
213225
dry_run,
214226
no_backup,
215-
} => commands::migrate::run(path, queries_dir, instance_name, port, dry_run, no_backup).await,
227+
} => {
228+
commands::migrate::run(path, queries_dir, instance_name, port, dry_run, no_backup).await
229+
}
230+
Commands::Backup { instance, output } => commands::backup::run(output, instance).await,
216231
};
217232

218233
// Shutdown metrics sender

0 commit comments

Comments
 (0)