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]
|
||||
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]
|
||||
|
|
|
|||
226
src/args.rs
226
src/args.rs
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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 {
|
||||
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(())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 controller;
|
||||
pub mod database;
|
||||
pub mod errors;
|
||||
mod operations;
|
||||
mod structs;
|
||||
pub mod operations;
|
||||
pub 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<()> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
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