mirror of
https://github.com/edu4rdshl/rusnapshot.git
synced 2026-07-17 23:24:55 +00:00
v0.2.0: big refactor and improvements.
This commit is contained in:
parent
b19e7579c6
commit
c32742b355
12 changed files with 507 additions and 1562 deletions
1448
Cargo.lock
generated
1448
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
|
@ -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]
|
||||||
|
|
|
||||||
218
src/args.rs
218
src/args.rs
|
|
@ -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.
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Args {
|
||||||
|
/// Get the database connection string.
|
||||||
|
#[must_use]
|
||||||
|
pub fn database_connection(&self) -> Connection {
|
||||||
|
match sqlite::open(&self.database_file) {
|
||||||
Ok(mut connection) => {
|
Ok(mut connection) => {
|
||||||
let timeout = value_t!(matches, "timeout", usize).unwrap_or_else(|_| 5000);
|
|
||||||
connection
|
connection
|
||||||
.set_busy_timeout(timeout)
|
.set_busy_timeout(self.timeout)
|
||||||
.expect("Failed to set database timeout");
|
.expect("Failed to set database timeout");
|
||||||
|
|
||||||
connection
|
connection
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
eprintln!("Error while trying to stablish the database connection. Error: {e}");
|
||||||
"Error while trying to stablish the database connection. Error: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
std::process::exit(1)
|
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()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn return_settings(
|
/// Initialize the database and directory structure.
|
||||||
matches: &clap::ArgMatches,
|
pub fn init(&self, extra_args: &ExtraArgs) -> Result<()> {
|
||||||
settings: &mut config::Config,
|
if !std::path::Path::new(&self.dest_dir).exists() {
|
||||||
) -> HashMap<String, String> {
|
println!("Setting up the directory structure.");
|
||||||
if matches.is_present("config-file") {
|
operations::setup_directory_structure(self)?;
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
},
|
database::setup_initial_database(&extra_args.database_connection)?;
|
||||||
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) => {
|
|
||||||
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);
|
|
||||||
std::process::exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn return_value_or_default(
|
Ok(())
|
||||||
settings: &HashMap<String, String>,
|
}
|
||||||
value: &str,
|
|
||||||
default_value: String,
|
|
||||||
) -> String {
|
|
||||||
settings.get(value).unwrap_or(&default_value).to_string()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
107
src/cli.yml
107
src/cli.yml
|
|
@ -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
|
|
||||||
|
|
@ -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(())
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
pub use failure::{Error, ResultExt};
|
|
||||||
pub type Result<T> = ::std::result::Result<T, Error>;
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
66
src/main.rs
66
src/main.rs
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
3
src/utils.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub fn is_same_character(s: &str, specific_char: char) -> bool {
|
||||||
|
s.chars().all(|c| c == specific_char)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue