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

View file

@ -1,155 +1,97 @@
use { use {
crate::structs::Args, crate::{database, operations, structs::ExtraArgs},
clap::{load_yaml, value_t, App}, anyhow::Result,
std::{collections::HashMap, path::Path}, clap::Parser,
serde::{Deserialize, Serialize},
sqlite::Connection,
}; };
pub fn get_args() -> Args { /// Simple and handy btrfs snapshoting tool.
let yaml = load_yaml!("cli.yml"); #[derive(Parser, Debug, Default, Serialize, Deserialize, Clone)]
let matches = App::from_yaml(yaml) #[clap(author, version, about, long_about = None, arg_required_else_help = true)]
.version(clap::crate_version!()) pub struct Args {
.get_matches(); /// Path to configuration file.
let settings: HashMap<String, String> = #[clap(short = 'c', long = "config")]
return_settings(&matches, &mut config::Config::default()); pub config_file: Option<String>,
Args { /// Directory where snapshots should be saved.
create_snapshot: matches.is_present("create-snapshot"), #[clap(long = "to", requires_all = &["create_snapshot"], default_value = "/.rusnapshot")]
delete_snapshot: matches.is_present("delete-snapshot"), pub dest_dir: String,
list_snapshots: matches.is_present("list-snapshots"), /// Directory from where snapshots should be created. It can also be used to specify the directory where a snapshot will be restored.
clean_snapshots: matches.is_present("clean-snapshots") || matches.is_present("keep-only"), #[clap(long = "from", requires_all = &["create_snapshot"], default_value = "")]
restore_snapshot: matches.is_present("restore-snapshot"), pub source_dir: String,
rw_snapshots: matches.is_present("read-write"), /// Snapshot id or name to work with.
dest_dir: if matches.is_present("dest-dir") { #[clap(long = "id", default_value = "")]
value_t!(matches, "dest-dir", String).unwrap_or_else(|_| String::new()) pub snapshot_id: String,
} else { /// Path to the SQLite database file.
return_value_or_default(&settings, "dest_dir", String::new()) #[clap(
}, short = 'd',
source_dir: if matches.is_present("source-dir") { long = "dfile",
value_t!(matches, "source-dir", String).unwrap_or_else(|_| String::new()) env = "RUSNAPSHOT_DB_FILE",
} else { default_value = "/var/lib/rusnapshot/rusnapshot.db"
return_value_or_default(&settings, "source_dir", String::new()) )]
}, pub database_file: String,
database_connection: { /// Prefix for the snapshot name.
let db_file_path = if matches.is_present("database-file") { #[clap(short = 'p', long = "prefix", default_value = "rusnapshot")]
value_t!(matches, "database-file", String).unwrap_or_else(|_| String::new()) pub snapshot_prefix: String,
} else { /// Used to specify a differentiator between snapshots with the same prefix.
return_value_or_default(&settings, "database_file", String::new()) #[clap(long = "kind", default_value = "rusnapshot")]
}; pub snapshot_kind: String,
if db_file_path.is_empty() { /// Keep only the last X items.
eprintln!( #[clap(short = 'k', long = "keep", default_value = "10")]
"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." 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.
std::process::exit(1) #[clap(long = "timeout", default_value = "5000")]
} else { pub timeout: usize,
match sqlite::open(&db_file_path) { /// Create a read-only/ro snapshot.
Ok(mut connection) => { #[clap(long = "create", conflicts_with_all = &["restore_snapshot", "delete_snapshot", "list_snapshots", "clean_snapshots"])]
let timeout = value_t!(matches, "timeout", usize).unwrap_or_else(|_| 5000); pub create_snapshot: bool,
connection /// Enable snapshots cleaning, will keep only the last X snapshots specified with -k/--keep.
.set_busy_timeout(timeout) #[clap(long = "clean")]
.expect("Failed to set database timeout"); pub clean_snapshots: bool,
connection /// Delete a snapshot.
} #[clap(long = "del", requires_all = &["snapshot_id"])]
Err(e) => { pub delete_snapshot: bool,
eprintln!( /// Restore a specific snapshot.
"Error while trying to stablish the database connection. Error: {}", #[clap(short = 'r', long = "restore", requires_all = &["snapshot_id"])]
e pub restore_snapshot: bool,
); /// List the snapshots tracked in the database.
std::process::exit(1) #[clap(short = 'l', long = "list")]
} pub list_snapshots: bool,
} /// Create read-write/rw snapshots.
} #[clap(short = 'w', long = "rw")]
}, pub read_write: bool,
snapshot_name: String::new(), /// Init the Rusnapshot database and directory structure.
snapshot_id: value_t!(matches, "snapshot-id", String).unwrap_or_else(|_| String::new()), #[clap(long = "init")]
snapshot_prefix: if matches.is_present("snapshot-prefix") { pub init: bool,
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()
},
}
} }
fn return_settings( impl Args {
matches: &clap::ArgMatches, /// Get the database connection string.
settings: &mut config::Config, #[must_use]
) -> HashMap<String, String> { pub fn database_connection(&self) -> Connection {
if matches.is_present("config-file") { match sqlite::open(&self.database_file) {
match settings.merge(config::File::with_name( Ok(mut connection) => {
&value_t!(matches, "config-file", String).unwrap(), connection
)) { .set_busy_timeout(self.timeout)
Ok(settings) => match settings.merge(config::Environment::with_prefix("RUSNAPSHOT")) { .expect("Failed to set database timeout");
Ok(settings) => settings
.clone() connection
.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 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) => { Err(e) => {
eprintln!("Error reading config file: {}", e); eprintln!("Error while trying to stablish the database connection. Error: {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);
std::process::exit(1) std::process::exit(1)
} }
} }
} }
}
fn return_value_or_default( /// Initialize the database and directory structure.
settings: &HashMap<String, String>, pub fn init(&self, extra_args: &ExtraArgs) -> Result<()> {
value: &str, if !std::path::Path::new(&self.dest_dir).exists() {
default_value: String, println!("Setting up the directory structure.");
) -> String { operations::setup_directory_structure(self)?;
settings.get(value).unwrap_or(&default_value).to_string() }
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 { use {
crate::{database, errors::*, operations, structs::Args}, crate::{args::Args, database, operations, structs::ExtraArgs},
chrono::Utc, anyhow::Result,
md5, md5,
prettytable::{cell, row, Table}, prettytable::{row, Table},
sqlite::Connection,
}; };
pub fn manage_creation(args: &mut Args) -> Result<()> { pub fn manage_creation(args: &mut Args, extra_args: &ExtraArgs) -> Result<()> {
args.snapshot_name = format!( args.snapshot_id = format!("{:?}", md5::compute(&extra_args.snapshot_name));
"{}-{}", if operations::take_snapshot(args, &extra_args.snapshot_name) {
args.snapshot_prefix, database::commit_to_database(args, extra_args)?;
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)?
} }
Ok(()) Ok(())
} }
pub fn manage_deletion(args: &mut Args) -> Result<()> { pub fn manage_deletion(args: &Args, extra_args: &mut ExtraArgs) -> Result<()> {
let snapshot_data = database::return_snapshot_data(&args.database_connection, args)?; let snapshot_data = database::return_snapshot_data(&extra_args.database_connection, args)?;
if !snapshot_data.snap_id.is_empty() { if snapshot_data.snap_id.is_empty() {
args.snapshot_name = snapshot_data.destination + &snapshot_data.name;
} else {
eprintln!( eprintln!(
"Snapshot ID {} does not returned any data. Please double check the ID.", "Snapshot ID {} does not returned any data. Please double check the ID.",
args.snapshot_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) { if !extra_args.snapshot_name.is_empty() && operations::del_snapshot(&extra_args.snapshot_name) {
database::delete_from_database(&args.database_connection, args)? database::delete_from_database(&extra_args.database_connection, args)?;
} }
Ok(()) Ok(())
} }
pub fn manage_restoring(args: &mut Args) -> Result<()> { pub fn manage_restoring(args: &mut Args, extra_args: &mut ExtraArgs) -> Result<()> {
let snapshot_data = database::return_snapshot_data(&args.database_connection, args)?; let snapshot_data = database::return_snapshot_data(&extra_args.database_connection, args)?;
if !snapshot_data.snap_id.is_empty() { if snapshot_data.snap_id.is_empty() {
args.snapshot_name = snapshot_data.destination + &snapshot_data.name; 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() { if args.source_dir.is_empty() {
args.source_dir = snapshot_data.source; 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!( println!(
"The snapshot with ID {} was successfully restored to {}", "The snapshot with ID {} was successfully restored to {}",
args.snapshot_id, args.source_dir args.snapshot_id, args.source_dir
) );
} }
Ok(()) Ok(())
} }
pub fn manage_listing(args: &mut Args) -> Result<()> { pub fn manage_listing(database_connection: &Connection) -> Result<()> {
let snaps_data = database::return_all_data(&args.database_connection)?; let snaps_data = database::return_all_data(database_connection)?;
let mut table = Table::new(); let mut table = Table::new();
table.set_titles(row![ table.set_titles(row![
@ -89,12 +93,12 @@ pub fn manage_listing(args: &mut Args) -> Result<()> {
Ok(()) Ok(())
} }
pub fn keep_only_x(args: &mut Args) -> Result<()> { pub fn keep_only_x(args: &mut Args, extra_args: &mut ExtraArgs) -> Result<()> {
let snaps_data = database::return_only_x_items(&args.database_connection, args)?; let snaps_data = database::return_only_x_items(&extra_args.database_connection, args)?;
for data in &snaps_data { 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(); args.snapshot_id = data.snap_id.clone();
manage_deletion(args)? manage_deletion(args, extra_args)?;
} }
Ok(()) Ok(())

View file

@ -1,8 +1,9 @@
use { use {
crate::{ crate::{
errors::*, args::Args,
structs::{Args, Database}, structs::{Database, ExtraArgs},
}, },
anyhow::Result,
sqlite::{Connection, State, Statement}, sqlite::{Connection, State, Statement},
}; };
@ -13,12 +14,12 @@ pub fn setup_initial_database(connection: &Connection) -> Result<()> {
Ok(()) 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!( let statement = format!(
"INSERT INTO snapshots (name, snap_id, kind, source, destination, ro_rw) VALUES ('{}', '{}', '{}', '{}', '{}', '{}')", "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(()) Ok(())
} }
@ -40,7 +41,7 @@ pub fn return_snapshot_data(connection: &Connection, args: &Args) -> Result<Data
))?; ))?;
let mut snap_data = Database::default(); 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)?; 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 let mut statement = connection
.prepare("SELECT name,snap_id,kind,source,destination,ro_rw,datetime(date, 'localtime') FROM snapshots ORDER BY date DESC")?; .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(); let db_struct = Database::default();
snapshots_data.push(populate_db_struct(&statement, db_struct)?); 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>> { pub fn return_only_x_items(connection: &Connection, args: &Args) -> Result<Vec<Database>> {
let mut snapshots_data: Vec<Database> = Vec::new(); 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(); let db_struct = Database::default();
snapshots_data.push(populate_db_struct(&statement, db_struct)?); 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> { fn populate_db_struct(row: &Statement, mut db_struct: Database) -> Result<Database> {
db_struct.name = row.read::<String>(0)?; // I'll improve this later.
db_struct.snap_id = row.read::<String>(1)?;
db_struct.kind = row.read::<String>(2)?; db_struct.name = row.read(0)?;
db_struct.source = row.read::<String>(3)?; db_struct.snap_id = row.read(1)?;
db_struct.destination = row.read::<String>(4)?; db_struct.kind = row.read(2)?;
db_struct.ro_rw = row.read::<String>(5)?; db_struct.source = row.read(3)?;
db_struct.date = row.read::<String>(6)?; db_struct.destination = row.read(4)?;
db_struct.ro_rw = row.read(5)?;
db_struct.date = row.read(6)?;
Ok(db_struct) 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 args;
pub mod controller; pub mod controller;
pub mod database; pub mod database;
pub mod errors; pub mod operations;
mod operations; pub mod structs;
mod structs; pub mod utils;

View file

@ -1,41 +1,75 @@
use rusnapshot::{args, controller, database, errors::*}; use std::path::Path;
fn run() -> Result<()> { use anyhow::Result;
let mut arguments = args::get_args(); 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('/') { 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('/') { 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 { if arguments.create_snapshot {
controller::manage_creation(&mut arguments)? controller::manage_creation(&mut arguments, &extra_args)?;
} }
if arguments.delete_snapshot { if arguments.delete_snapshot {
controller::manage_deletion(&mut arguments)? controller::manage_deletion(&arguments, &mut extra_args)?;
} }
if arguments.list_snapshots { if arguments.list_snapshots {
controller::manage_listing(&mut arguments)? controller::manage_listing(&extra_args.database_connection)?;
} }
if arguments.clean_snapshots { if arguments.clean_snapshots {
controller::keep_only_x(&mut arguments)? controller::keep_only_x(&mut arguments, &mut extra_args)?;
} }
if arguments.restore_snapshot { if arguments.restore_snapshot {
controller::manage_restoring(&mut arguments)? controller::manage_restoring(&mut arguments, &mut extra_args)?;
} }
Ok(()) Ok(())
} }
fn main() { fn main() {
if let Err(err) = run() { if let Err(err) = try_run() {
eprintln!("\nError: {}", err); eprintln!("\nError: {err}");
for cause in err.iter_chain().skip(1) {
eprintln!("Error description: {}", cause);
}
std::process::exit(1); std::process::exit(1);
} }
} }

View file

@ -1,13 +1,17 @@
use crate::utils::is_same_character;
use std::path::PathBuf;
use { use {
crate::structs::Args, crate::args::Args,
anyhow::Result,
std::{path::Path, process::Command}, std::{path::Path, process::Command},
}; };
pub fn take_snapshot(args: &Args) -> bool { pub fn take_snapshot(args: &Args, snapshot_name: &str) -> bool {
let snapshot_name = format!("{}/{}", args.dest_dir, args.snapshot_name); let snapshot_name = format!("{}/{}", args.dest_dir, snapshot_name);
let mut btrfs_args = vec!["subvolume", "snapshot", &args.source_dir, &snapshot_name]; let mut btrfs_args = vec!["subvolume", "snapshot", &args.source_dir, &snapshot_name];
if !args.rw_snapshots { if !args.read_write {
btrfs_args.push("-r") btrfs_args.push("-r");
} }
Command::new("btrfs") Command::new("btrfs")
.args(&btrfs_args) .args(&btrfs_args)
@ -16,15 +20,23 @@ pub fn take_snapshot(args: &Args) -> bool {
.success() .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") Command::new("btrfs")
.args(["subvolume", "delete", &args.snapshot_name]) .args(["subvolume", "delete", snapshot_name])
.status() .status()
.expect("Error deleting the snapshot.") .expect("Error deleting the snapshot.")
.success() .success()
} }
pub fn restore_snapshot(args: &Args) -> bool { pub fn restore_snapshot(args: &Args, snapshot_name: &str) -> bool {
(!Path::new(&args.source_dir).exists() (!Path::new(&args.source_dir).exists()
|| Command::new("btrfs") || Command::new("btrfs")
.args(["subvolume", "delete", &args.source_dir]) .args(["subvolume", "delete", &args.source_dir])
@ -32,13 +44,19 @@ pub fn restore_snapshot(args: &Args) -> bool {
.expect("Error deleting the subvolume.") .expect("Error deleting the subvolume.")
.success()) .success())
&& Command::new("btrfs") && Command::new("btrfs")
.args([ .args(["subvolume", "snapshot", &snapshot_name, &args.source_dir])
"subvolume",
"snapshot",
&args.snapshot_name,
&args.source_dir,
])
.status() .status()
.expect("Error restoring the snapshot.") .expect("Error restoring the snapshot.")
.success() .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; use sqlite::Connection;
pub struct Args { #[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
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,
}
pub struct Database { pub struct Database {
pub name: String, pub name: String,
pub snap_id: String, pub snap_id: String,
@ -28,16 +11,7 @@ pub struct Database {
pub date: String, pub date: String,
} }
impl Database { pub struct ExtraArgs {
pub fn default() -> Database { pub snapshot_name: String,
Database { pub database_connection: Connection,
name: String::new(),
snap_id: String::new(),
kind: String::new(),
source: String::new(),
destination: String::new(),
ro_rw: String::new(),
date: String::new(),
}
}
} }

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)
}