v0.2.0: big refactor and improvements.

This commit is contained in:
Eduard Tolosa 2024-02-05 01:27:12 -05:00
parent b19e7579c6
commit c32742b355
12 changed files with 507 additions and 1562 deletions

1448
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,20 @@
[package]
name = "rusnapshot"
version = "0.1.0"
version = "0.2.0"
authors = ["edu4rdshl"]
edition = "2018"
description = "Simple and handy btrfs snapshoting tool."
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "2.34.0", features = ["yaml"] }
reqwest = { version = "0.11.18", features = ["default-tls"] }
openssl = { version = "0.10.54", features = ["vendored"] }
config = { version = "0.11.0", features = ["yaml", "json", "toml", "hjson", "ini"] }
failure = "0.1.8"
sqlite = "0.26.0"
clap = { version = "4.4.18", features = ["derive", "env"] }
serde = { version = "1.0.188", features = ["derive"] }
sqlite = "0.33.0"
md5 = "0.7.0"
chrono = "0.4.26"
prettytable-rs = "0.8.0"
chrono = "0.4.33"
prettytable-rs = "0.10.0"
anyhow = "1.0.79"
[profile.release]

View file

@ -1,155 +1,97 @@
use {
crate::structs::Args,
clap::{load_yaml, value_t, App},
std::{collections::HashMap, path::Path},
crate::{database, operations, structs::ExtraArgs},
anyhow::Result,
clap::Parser,
serde::{Deserialize, Serialize},
sqlite::Connection,
};
pub fn get_args() -> Args {
let yaml = load_yaml!("cli.yml");
let matches = App::from_yaml(yaml)
.version(clap::crate_version!())
.get_matches();
let settings: HashMap<String, String> =
return_settings(&matches, &mut config::Config::default());
Args {
create_snapshot: matches.is_present("create-snapshot"),
delete_snapshot: matches.is_present("delete-snapshot"),
list_snapshots: matches.is_present("list-snapshots"),
clean_snapshots: matches.is_present("clean-snapshots") || matches.is_present("keep-only"),
restore_snapshot: matches.is_present("restore-snapshot"),
rw_snapshots: matches.is_present("read-write"),
dest_dir: if matches.is_present("dest-dir") {
value_t!(matches, "dest-dir", String).unwrap_or_else(|_| String::new())
} else {
return_value_or_default(&settings, "dest_dir", String::new())
},
source_dir: if matches.is_present("source-dir") {
value_t!(matches, "source-dir", String).unwrap_or_else(|_| String::new())
} else {
return_value_or_default(&settings, "source_dir", String::new())
},
database_connection: {
let db_file_path = if matches.is_present("database-file") {
value_t!(matches, "database-file", String).unwrap_or_else(|_| String::new())
} else {
return_value_or_default(&settings, "database_file", String::new())
};
if db_file_path.is_empty() {
eprintln!(
"Please specify a database file with -d/--dfile or the database_file option in the config file, it's required for all the operations, leaving."
);
std::process::exit(1)
} else {
match sqlite::open(&db_file_path) {
Ok(mut connection) => {
let timeout = value_t!(matches, "timeout", usize).unwrap_or_else(|_| 5000);
connection
.set_busy_timeout(timeout)
.expect("Failed to set database timeout");
connection
}
Err(e) => {
eprintln!(
"Error while trying to stablish the database connection. Error: {}",
e
);
std::process::exit(1)
}
}
}
},
snapshot_name: String::new(),
snapshot_id: value_t!(matches, "snapshot-id", String).unwrap_or_else(|_| String::new()),
snapshot_prefix: if matches.is_present("snapshot-prefix") {
value_t!(matches, "snapshot-prefix", String)
.unwrap_or_else(|_| String::from("snapshot"))
} else {
return_value_or_default(&settings, "snapshot_prefix", String::from("snapshot"))
},
snapshot_kind: if matches.is_present("snapshot-kind") {
value_t!(matches, "snapshot-kind", String)
.unwrap_or_else(|_| String::from("rusnapshot"))
} else {
return_value_or_default(&settings, "snapshot_kind", String::from("rusnapshot"))
},
snapshot_ro_rw: if matches.is_present("read-write") {
"read-write".to_string()
} else {
"read-only".to_string()
},
keep_only: if matches.is_present("keep-only") {
value_t!(matches, "keep-only", usize).unwrap_or_else(|_| 0)
} else {
return_value_or_default(&settings, "keep_only", 0.to_string())
.parse()
.unwrap_or_default()
},
}
/// Simple and handy btrfs snapshoting tool.
#[derive(Parser, Debug, Default, Serialize, Deserialize, Clone)]
#[clap(author, version, about, long_about = None, arg_required_else_help = true)]
pub struct Args {
/// Path to configuration file.
#[clap(short = 'c', long = "config")]
pub config_file: Option<String>,
/// Directory where snapshots should be saved.
#[clap(long = "to", requires_all = &["create_snapshot"], default_value = "/.rusnapshot")]
pub dest_dir: String,
/// Directory from where snapshots should be created. It can also be used to specify the directory where a snapshot will be restored.
#[clap(long = "from", requires_all = &["create_snapshot"], default_value = "")]
pub source_dir: String,
/// Snapshot id or name to work with.
#[clap(long = "id", default_value = "")]
pub snapshot_id: String,
/// Path to the SQLite database file.
#[clap(
short = 'd',
long = "dfile",
env = "RUSNAPSHOT_DB_FILE",
default_value = "/var/lib/rusnapshot/rusnapshot.db"
)]
pub database_file: String,
/// Prefix for the snapshot name.
#[clap(short = 'p', long = "prefix", default_value = "rusnapshot")]
pub snapshot_prefix: String,
/// Used to specify a differentiator between snapshots with the same prefix.
#[clap(long = "kind", default_value = "rusnapshot")]
pub snapshot_kind: String,
/// Keep only the last X items.
#[clap(short = 'k', long = "keep", default_value = "10")]
pub keep_only: usize,
/// Time in milliseconds until SQLite can return a timeout. Do not touch if you don't know what you are doing.
#[clap(long = "timeout", default_value = "5000")]
pub timeout: usize,
/// Create a read-only/ro snapshot.
#[clap(long = "create", conflicts_with_all = &["restore_snapshot", "delete_snapshot", "list_snapshots", "clean_snapshots"])]
pub create_snapshot: bool,
/// Enable snapshots cleaning, will keep only the last X snapshots specified with -k/--keep.
#[clap(long = "clean")]
pub clean_snapshots: bool,
/// Delete a snapshot.
#[clap(long = "del", requires_all = &["snapshot_id"])]
pub delete_snapshot: bool,
/// Restore a specific snapshot.
#[clap(short = 'r', long = "restore", requires_all = &["snapshot_id"])]
pub restore_snapshot: bool,
/// List the snapshots tracked in the database.
#[clap(short = 'l', long = "list")]
pub list_snapshots: bool,
/// Create read-write/rw snapshots.
#[clap(short = 'w', long = "rw")]
pub read_write: bool,
/// Init the Rusnapshot database and directory structure.
#[clap(long = "init")]
pub init: bool,
}
fn return_settings(
matches: &clap::ArgMatches,
settings: &mut config::Config,
) -> HashMap<String, String> {
if matches.is_present("config-file") {
match settings.merge(config::File::with_name(
&value_t!(matches, "config-file", String).unwrap(),
)) {
Ok(settings) => match settings.merge(config::Environment::with_prefix("RUSNAPSHOT")) {
Ok(settings) => settings
.clone()
.try_into::<HashMap<String, String>>()
.unwrap(),
Err(e) => {
eprintln!("Error merging environment variables into settings: {}", e);
std::process::exit(1)
}
},
Err(e) => {
eprintln!("Error reading config file: {}", e);
std::process::exit(1)
impl Args {
/// Get the database connection string.
#[must_use]
pub fn database_connection(&self) -> Connection {
match sqlite::open(&self.database_file) {
Ok(mut connection) => {
connection
.set_busy_timeout(self.timeout)
.expect("Failed to set database timeout");
connection
}
}
} else if Path::new("rusnapshot.toml").exists()
|| Path::new("rusnapshot.json").exists()
|| Path::new("rusnapshot.hjson").exists()
|| Path::new("rusnapshot.ini").exists()
|| Path::new("rusnapshot.yml").exists()
{
match settings.merge(config::File::with_name("rusnapshot")) {
Ok(settings) => match settings.merge(config::Environment::with_prefix("RUSNAPSHOT")) {
Ok(settings) => settings
.clone()
.try_into::<HashMap<String, String>>()
.unwrap(),
Err(e) => {
eprintln!("Error merging environment variables into settings: {}", e);
std::process::exit(1)
}
},
Err(e) => {
eprintln!("Error reading config file: {}", e);
std::process::exit(1)
}
}
} else {
match settings.merge(config::Environment::with_prefix("RUSNAPSHOT")) {
Ok(settings) => settings
.clone()
.try_into::<HashMap<String, String>>()
.unwrap(),
Err(e) => {
eprintln!("Error merging environment variables into settings: {}", e);
eprintln!("Error while trying to stablish the database connection. Error: {e}");
std::process::exit(1)
}
}
}
}
fn return_value_or_default(
settings: &HashMap<String, String>,
value: &str,
default_value: String,
) -> String {
settings.get(value).unwrap_or(&default_value).to_string()
/// Initialize the database and directory structure.
pub fn init(&self, extra_args: &ExtraArgs) -> Result<()> {
if !std::path::Path::new(&self.dest_dir).exists() {
println!("Setting up the directory structure.");
operations::setup_directory_structure(self)?;
}
database::setup_initial_database(&extra_args.database_connection)?;
Ok(())
}
}

View file

@ -1,107 +0,0 @@
name: Rusnapshot
author: Eduard Tolosa <edu4rdshl@protonmail.com>
settings:
- ArgRequiredElseHelp
- StrictUtf8
about: Simple and handy btrfs snapshoting tool.
args:
- config-file:
help: Path to configuration file.
short: c
long: config
takes_value: true
multiple: false
- dest-dir:
help: Directory where snapshots should be saved.
long: to
takes_value: true
multiple: false
- source-dir:
help: Directory from where snapshots should be created. It can also be used to specify the directory where a snapshot will be restored.
long: from
takes_value: true
multiple: false
- snapshot-id:
help: Snapshot id or name to work with.
long: id
takes_value: true
multiple: false
- database-file:
help: Path to the SQLite database file.
short: d
long: dfile
takes_value: true
multiple: false
- snapshot-prefix:
help: "Prefix for the snapshot name. Default: snapshot"
short: p
long: prefix
takes_value: true
multiple: false
- snapshot-kind:
help: "Used to specify a differentiator between snapshots with the same prefix. Default: rusnapshot"
long: kind
takes_value: true
multiple: false
- keep-only:
help: Keep only the last X items.
short: k
long: keep
takes_value: true
multiple: false
- timeout:
help: "Time in milliseconds until SQLite can return a timeout (Do not touch if you don't know what you are doing). Default: 5000"
long: timeout
takes_value: true
multiple: false
- create-snapshot:
help: Create an snapshot.
long: cr
takes_value: false
multiple: false
- clean-snapshots:
help: "Enable snapshots cleaning, will keep only the last X snapshots specified with -k/--keep. WARNING: if -k/--keep is not specified, IT WILL DELETE ALL THE SNAPSHOTS."
long: clean
takes_value: false
multiple: false
- delete-snapshot:
help: Delete an snapshot.
long: del
takes_value: false
multiple: false
requires:
- snapshot-id
- restore-snapshot:
help: Restore an specific snapshot.
short: r
long: restore
takes_value: false
multiple: false
requires:
- snapshot-id
- list-snapshots:
help: List the snapshots tracked in the database.
short: l
long: list
takes_value: false
multiple: false
- read-write:
help: Create read-write/rw snapshots.
short: w
long: rw
takes_value: false
multiple: false

View file

@ -1,66 +1,70 @@
use {
crate::{database, errors::*, operations, structs::Args},
chrono::Utc,
crate::{args::Args, database, operations, structs::ExtraArgs},
anyhow::Result,
md5,
prettytable::{cell, row, Table},
prettytable::{row, Table},
sqlite::Connection,
};
pub fn manage_creation(args: &mut Args) -> Result<()> {
args.snapshot_name = format!(
"{}-{}",
args.snapshot_prefix,
Utc::now().format("%Y-%m-%d-%H-%M-%S-%6f")
);
args.snapshot_id = format!("{:?}", md5::compute(&args.snapshot_name));
if operations::take_snapshot(args) {
database::commit_to_database(&args.database_connection, args)?
pub fn manage_creation(args: &mut Args, extra_args: &ExtraArgs) -> Result<()> {
args.snapshot_id = format!("{:?}", md5::compute(&extra_args.snapshot_name));
if operations::take_snapshot(args, &extra_args.snapshot_name) {
database::commit_to_database(args, extra_args)?;
}
Ok(())
}
pub fn manage_deletion(args: &mut Args) -> Result<()> {
let snapshot_data = database::return_snapshot_data(&args.database_connection, args)?;
if !snapshot_data.snap_id.is_empty() {
args.snapshot_name = snapshot_data.destination + &snapshot_data.name;
} else {
pub fn manage_deletion(args: &Args, extra_args: &mut ExtraArgs) -> Result<()> {
let snapshot_data = database::return_snapshot_data(&extra_args.database_connection, args)?;
if snapshot_data.snap_id.is_empty() {
eprintln!(
"Snapshot ID {} does not returned any data. Please double check the ID.",
args.snapshot_id
)
);
std::process::exit(1);
} else {
extra_args.snapshot_name = snapshot_data.destination + &snapshot_data.name;
}
if !args.snapshot_name.is_empty() && operations::del_snapshot(args) {
database::delete_from_database(&args.database_connection, args)?
if !extra_args.snapshot_name.is_empty() && operations::del_snapshot(&extra_args.snapshot_name) {
database::delete_from_database(&extra_args.database_connection, args)?;
}
Ok(())
}
pub fn manage_restoring(args: &mut Args) -> Result<()> {
let snapshot_data = database::return_snapshot_data(&args.database_connection, args)?;
if !snapshot_data.snap_id.is_empty() {
args.snapshot_name = snapshot_data.destination + &snapshot_data.name;
pub fn manage_restoring(args: &mut Args, extra_args: &mut ExtraArgs) -> Result<()> {
let snapshot_data = database::return_snapshot_data(&extra_args.database_connection, args)?;
if snapshot_data.snap_id.is_empty() {
eprintln!(
"Snapshot ID {} does not returned any data. Please double check the ID.",
args.snapshot_id
);
std::process::exit(1);
} else {
extra_args.snapshot_name = snapshot_data.destination + &snapshot_data.name;
if args.source_dir.is_empty() {
args.source_dir = snapshot_data.source;
}
} else {
eprintln!(
"Snapshot ID {} does not returned any data. Please double check the ID.",
args.snapshot_id
)
}
if !args.snapshot_name.is_empty() && operations::restore_snapshot(args) {
println!("Restoring the snapshot with ID {}", args.snapshot_id);
println!("Name of the snapshot: {}", extra_args.snapshot_name);
println!("Source directory: {}", args.source_dir);
if !extra_args.snapshot_name.is_empty()
&& operations::restore_snapshot(args, &extra_args.snapshot_name)
{
println!(
"The snapshot with ID {} was successfully restored to {}",
args.snapshot_id, args.source_dir
)
);
}
Ok(())
}
pub fn manage_listing(args: &mut Args) -> Result<()> {
let snaps_data = database::return_all_data(&args.database_connection)?;
pub fn manage_listing(database_connection: &Connection) -> Result<()> {
let snaps_data = database::return_all_data(database_connection)?;
let mut table = Table::new();
table.set_titles(row![
@ -89,12 +93,12 @@ pub fn manage_listing(args: &mut Args) -> Result<()> {
Ok(())
}
pub fn keep_only_x(args: &mut Args) -> Result<()> {
let snaps_data = database::return_only_x_items(&args.database_connection, args)?;
pub fn keep_only_x(args: &mut Args, extra_args: &mut ExtraArgs) -> Result<()> {
let snaps_data = database::return_only_x_items(&extra_args.database_connection, args)?;
for data in &snaps_data {
args.snapshot_name = data.destination.clone() + &data.name;
extra_args.snapshot_name = data.destination.clone() + &data.name;
args.snapshot_id = data.snap_id.clone();
manage_deletion(args)?
manage_deletion(args, extra_args)?;
}
Ok(())

View file

@ -1,8 +1,9 @@
use {
crate::{
errors::*,
structs::{Args, Database},
args::Args,
structs::{Database, ExtraArgs},
},
anyhow::Result,
sqlite::{Connection, State, Statement},
};
@ -13,12 +14,12 @@ pub fn setup_initial_database(connection: &Connection) -> Result<()> {
Ok(())
}
pub fn commit_to_database(connection: &Connection, args: &Args) -> Result<()> {
pub fn commit_to_database(args: &Args, extra_args: &ExtraArgs) -> Result<()> {
let statement = format!(
"INSERT INTO snapshots (name, snap_id, kind, source, destination, ro_rw) VALUES ('{}', '{}', '{}', '{}', '{}', '{}')",
args.snapshot_name, args.snapshot_id, args.snapshot_kind, args.source_dir, args.dest_dir, args.snapshot_ro_rw
&extra_args.snapshot_name, args.snapshot_id, args.snapshot_kind, args.source_dir, args.dest_dir, args.read_write
);
connection.execute(statement)?;
extra_args.database_connection.execute(statement)?;
Ok(())
}
@ -40,7 +41,7 @@ pub fn return_snapshot_data(connection: &Connection, args: &Args) -> Result<Data
))?;
let mut snap_data = Database::default();
while let State::Row = statement.next()? {
while statement.next()? == State::Row {
snap_data = populate_db_struct(&statement, snap_data)?;
}
@ -53,7 +54,7 @@ pub fn return_all_data(connection: &Connection) -> Result<Vec<Database>> {
let mut statement = connection
.prepare("SELECT name,snap_id,kind,source,destination,ro_rw,datetime(date, 'localtime') FROM snapshots ORDER BY date DESC")?;
while let State::Row = statement.next()? {
while statement.next()? == State::Row {
let db_struct = Database::default();
snapshots_data.push(populate_db_struct(&statement, db_struct)?);
}
@ -64,9 +65,9 @@ pub fn return_all_data(connection: &Connection) -> Result<Vec<Database>> {
pub fn return_only_x_items(connection: &Connection, args: &Args) -> Result<Vec<Database>> {
let mut snapshots_data: Vec<Database> = Vec::new();
let mut statement = connection.prepare(&format!("SELECT name,snap_id,kind,source,destination,ro_rw,date FROM (SELECT row_number() over(ORDER BY date DESC) n,* from snapshots WHERE name like '{}%' AND kind = '{}' AND ro_rw = '{}') WHERE n > {}", args.snapshot_prefix, args.snapshot_kind, args.snapshot_ro_rw, args.keep_only))?;
let mut statement = connection.prepare(&format!("SELECT name,snap_id,kind,source,destination,ro_rw,date FROM (SELECT row_number() over(ORDER BY date DESC) n,* from snapshots WHERE name like '{}%' AND kind = '{}' AND ro_rw = '{}') WHERE n > {}", args.snapshot_prefix, args.snapshot_kind, args.read_write, args.keep_only))?;
while let State::Row = statement.next()? {
while statement.next()? == State::Row {
let db_struct = Database::default();
snapshots_data.push(populate_db_struct(&statement, db_struct)?);
}
@ -75,12 +76,15 @@ pub fn return_only_x_items(connection: &Connection, args: &Args) -> Result<Vec<D
}
fn populate_db_struct(row: &Statement, mut db_struct: Database) -> Result<Database> {
db_struct.name = row.read::<String>(0)?;
db_struct.snap_id = row.read::<String>(1)?;
db_struct.kind = row.read::<String>(2)?;
db_struct.source = row.read::<String>(3)?;
db_struct.destination = row.read::<String>(4)?;
db_struct.ro_rw = row.read::<String>(5)?;
db_struct.date = row.read::<String>(6)?;
// I'll improve this later.
db_struct.name = row.read(0)?;
db_struct.snap_id = row.read(1)?;
db_struct.kind = row.read(2)?;
db_struct.source = row.read(3)?;
db_struct.destination = row.read(4)?;
db_struct.ro_rw = row.read(5)?;
db_struct.date = row.read(6)?;
Ok(db_struct)
}

View file

@ -1,2 +0,0 @@
pub use failure::{Error, ResultExt};
pub type Result<T> = ::std::result::Result<T, Error>;

View file

@ -1,6 +1,6 @@
pub mod args;
pub mod controller;
pub mod database;
pub mod errors;
mod operations;
mod structs;
pub mod operations;
pub mod structs;
pub mod utils;

View file

@ -1,41 +1,75 @@
use rusnapshot::{args, controller, database, errors::*};
use std::path::Path;
fn run() -> Result<()> {
let mut arguments = args::get_args();
use anyhow::Result;
use chrono::Utc;
use clap::Parser;
use rusnapshot::{args, controller, structs::ExtraArgs};
fn try_run() -> Result<()> {
let mut arguments = args::Args::parse();
let mut extra_args = ExtraArgs {
snapshot_name: format!(
"{}-{}",
arguments.snapshot_prefix,
Utc::now().format("%Y-%m-%d-%H-%M-%S-%6f")
),
database_connection: arguments.database_connection(),
};
arguments.init(&extra_args)?;
// It's required to have a trailing slash for the source and destination directories.
// Otherwise, when we retrieve the snapshot data from the database, we won't be able to
// restore/delete the snapshot because the source/destination paths won't match.
if !arguments.source_dir.is_empty() && !arguments.source_dir.ends_with('/') {
arguments.source_dir += "/"
arguments.source_dir += "/";
}
if !arguments.dest_dir.is_empty() && !arguments.dest_dir.ends_with('/') {
arguments.dest_dir += "/"
arguments.dest_dir += "/";
}
database::setup_initial_database(&arguments.database_connection)?;
// Now we need to make sure that the paths for source and destination are full paths.
// If they are not, we will use the current working directory to build the full path.
if arguments.create_snapshot {
if !Path::new(&arguments.source_dir).is_absolute() {
arguments.source_dir = std::env::current_dir()?
.join(&arguments.source_dir)
.to_str()
.expect("Failed to get the source directory")
.to_string();
}
if !Path::new(&arguments.dest_dir).is_absolute() {
arguments.dest_dir = std::env::current_dir()?
.join(&arguments.dest_dir)
.to_str()
.expect("Failed to get the destination directory")
.to_string();
}
}
if arguments.create_snapshot {
controller::manage_creation(&mut arguments)?
controller::manage_creation(&mut arguments, &extra_args)?;
}
if arguments.delete_snapshot {
controller::manage_deletion(&mut arguments)?
controller::manage_deletion(&arguments, &mut extra_args)?;
}
if arguments.list_snapshots {
controller::manage_listing(&mut arguments)?
controller::manage_listing(&extra_args.database_connection)?;
}
if arguments.clean_snapshots {
controller::keep_only_x(&mut arguments)?
controller::keep_only_x(&mut arguments, &mut extra_args)?;
}
if arguments.restore_snapshot {
controller::manage_restoring(&mut arguments)?
controller::manage_restoring(&mut arguments, &mut extra_args)?;
}
Ok(())
}
fn main() {
if let Err(err) = run() {
eprintln!("\nError: {}", err);
for cause in err.iter_chain().skip(1) {
eprintln!("Error description: {}", cause);
}
if let Err(err) = try_run() {
eprintln!("\nError: {err}");
std::process::exit(1);
}
}

View file

@ -1,13 +1,17 @@
use crate::utils::is_same_character;
use std::path::PathBuf;
use {
crate::structs::Args,
crate::args::Args,
anyhow::Result,
std::{path::Path, process::Command},
};
pub fn take_snapshot(args: &Args) -> bool {
let snapshot_name = format!("{}/{}", args.dest_dir, args.snapshot_name);
pub fn take_snapshot(args: &Args, snapshot_name: &str) -> bool {
let snapshot_name = format!("{}/{}", args.dest_dir, snapshot_name);
let mut btrfs_args = vec!["subvolume", "snapshot", &args.source_dir, &snapshot_name];
if !args.rw_snapshots {
btrfs_args.push("-r")
if !args.read_write {
btrfs_args.push("-r");
}
Command::new("btrfs")
.args(&btrfs_args)
@ -16,15 +20,23 @@ pub fn take_snapshot(args: &Args) -> bool {
.success()
}
pub fn del_snapshot(args: &Args) -> bool {
pub fn del_snapshot(snapshot_name: &str) -> bool {
// Refuse to delete the root subvolume.
if snapshot_name == "/" || is_same_character(snapshot_name, '/') {
println!(
"Snapshot name to delete is: {}. Refusing to delete the root subvolume.",
snapshot_name
);
std::process::exit(1);
}
Command::new("btrfs")
.args(["subvolume", "delete", &args.snapshot_name])
.args(["subvolume", "delete", snapshot_name])
.status()
.expect("Error deleting the snapshot.")
.success()
}
pub fn restore_snapshot(args: &Args) -> bool {
pub fn restore_snapshot(args: &Args, snapshot_name: &str) -> bool {
(!Path::new(&args.source_dir).exists()
|| Command::new("btrfs")
.args(["subvolume", "delete", &args.source_dir])
@ -32,13 +44,19 @@ pub fn restore_snapshot(args: &Args) -> bool {
.expect("Error deleting the subvolume.")
.success())
&& Command::new("btrfs")
.args([
"subvolume",
"snapshot",
&args.snapshot_name,
&args.source_dir,
])
.args(["subvolume", "snapshot", &snapshot_name, &args.source_dir])
.status()
.expect("Error restoring the snapshot.")
.success()
}
pub fn setup_directory_structure(args: &Args) -> Result<()> {
let dest_dir = PathBuf::from(&args.dest_dir);
if !dest_dir.exists() {
Command::new("btrfs")
.args(["subvolume", "create", &args.dest_dir])
.spawn()?;
}
Ok(())
}

View file

@ -1,23 +1,6 @@
use sqlite::Connection;
pub struct Args {
pub create_snapshot: bool,
pub delete_snapshot: bool,
pub list_snapshots: bool,
pub clean_snapshots: bool,
pub restore_snapshot: bool,
pub rw_snapshots: bool,
pub dest_dir: String,
pub source_dir: String,
pub database_connection: Connection,
pub snapshot_name: String,
pub snapshot_id: String,
pub snapshot_prefix: String,
pub snapshot_kind: String,
pub snapshot_ro_rw: String,
pub keep_only: usize,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Database {
pub name: String,
pub snap_id: String,
@ -28,16 +11,7 @@ pub struct Database {
pub date: String,
}
impl Database {
pub fn default() -> Database {
Database {
name: String::new(),
snap_id: String::new(),
kind: String::new(),
source: String::new(),
destination: String::new(),
ro_rw: String::new(),
date: String::new(),
}
}
pub struct ExtraArgs {
pub snapshot_name: String,
pub database_connection: Connection,
}

3
src/utils.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn is_same_character(s: &str, specific_char: char) -> bool {
s.chars().all(|c| c == specific_char)
}