Compare commits

...

55 commits

Author SHA1 Message Date
1351a6bd9b (chore): update dependencies 2025-09-08 19:12:53 -05:00
a2de007c87 (chore): fix systemd units
- Best wording
- Don't allow to enable `.service` units, that wouldn't make sense because they're managed by timers
2025-09-08 11:54:31 -05:00
2f47feccd7 (chore): bump version and update deps 2025-08-10 01:21:03 -05:00
daca0998fa (chore): change default for database file
It's more common that people uses @snapshots (/.snapshopts/) subvolume to store snapshots, so let's default to that dir
2025-08-10 01:20:50 -05:00
2c8c16ad4e Bump dependencies 2025-08-02 14:21:21 -05:00
0285857b91 Exclude windows builds 2025-08-02 14:09:02 -05:00
c2d1d12abf Update builder.sh 2025-08-02 13:55:36 -05:00
d57c34e9d1 (chore): update builder.sh
- Disable docker checks, we now use podman
- Use cross for all builds, otherwise the build ends up in glibc versions mismatch
2025-08-02 13:00:58 -05:00
bfca23d1c7 (chore): bump version. 2025-05-04 02:01:54 -05:00
ae403db784 (chore): bump version. 2025-05-04 02:00:36 -05:00
f23e74c610 (feat): improve database handling. 2025-05-04 01:59:44 -05:00
9c7f10c186 Merge branch 'master' of https://github.com/Edu4rdSHL/rusnapshot 2025-05-04 01:41:50 -05:00
b47ebd7616 (feat): update examples. 2025-05-04 01:41:49 -05:00
97fa393825
Create LICENSE 2025-05-04 00:33:15 -05:00
8f67b4d044 (chore): update edition 2025-05-04 00:19:51 -05:00
7b363e0207 (chore): bump version 2025-05-04 00:12:50 -05:00
2a2058f781 (build): disable LTO by default 2025-05-03 23:37:48 -05:00
bd0887f416 (chore): update deps. 2025-05-03 23:34:47 -05:00
5e1a3e7115 (chore): clippy fix. 2025-05-03 23:34:36 -05:00
0b117ac07c (docs): point out the main feature and update description. 2025-05-03 23:34:16 -05:00
b8b2c6f0f8 (chore): more sensible defaults. 2025-05-03 23:33:54 -05:00
844b6543b4 Update dependencies 2025-04-20 14:50:57 -05:00
e421db4013 Clippy fixes 2025-04-20 14:50:52 -05:00
573c50d356
Create FUNDING.yml 2024-02-07 01:05:46 -05:00
41951edb8b Rename file. 2024-02-07 01:01:05 -05:00
235945c8e8 Update SETUP documentation 2024-02-07 00:58:39 -05:00
01e518d3d3 Update documentation. 2024-02-07 00:04:37 -05:00
e24d29c048 Update config templates. 2024-02-07 00:03:29 -05:00
230b2babbe Add support for using a hostname identifier 2024-02-07 00:01:10 -05:00
14c0f371d2 Bump version. 2024-02-06 22:54:57 -05:00
53ca8d4773 Merge branch 'master' of https://github.com/Edu4rdSHL/rusnapshot 2024-02-05 23:22:14 -05:00
4648c12379 Update prefered database file location. 2024-02-05 23:22:10 -05:00
a9aa9d9eae
Create rust.yml 2024-02-05 12:02:35 -05:00
07ae19da1b Bump version. 2024-02-05 11:49:10 -05:00
933c780ccc Apply clippy fixes. 2024-02-05 11:48:39 -05:00
df68951767 Move creation step preparation to a separate function 2024-02-05 11:48:32 -05:00
68fb2b0157 Add rustfmt 2024-02-05 11:47:43 -05:00
98b9313cce Bump version. 2024-02-05 03:31:00 -05:00
142370e703 Remove unused flag. 2024-02-05 03:30:37 -05:00
962471de48 Better messages. 2024-02-05 03:28:28 -05:00
0a1beeb3b7 Add better informational messages 2024-02-05 03:27:51 -05:00
ca4e96a662 Fixes. 2024-02-05 03:26:46 -05:00
a231f177b5 Use proper type for keep_only. 2024-02-05 03:26:22 -05:00
ddcc8d58d9 Update config templates 2024-02-05 02:54:34 -05:00
33a6153f23 Update README 2024-02-05 02:47:59 -05:00
502753acf9 Use args.dest_dir (--to) when restoring the snapshot 2024-02-05 02:47:48 -05:00
bac3a8c69e Add support for TOML config files 2024-02-05 02:36:33 -05:00
c32742b355 v0.2.0: big refactor and improvements. 2024-02-05 01:27:12 -05:00
b19e7579c6 Update deps. 2023-06-12 15:49:53 -05:00
f69fee8287 Fix clippy warnings 2023-06-12 15:49:46 -05:00
Edu4rdSHL
e68c28b223
Update builder.sh
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-03-11 15:32:33 -05:00
Edu4rdSHL
56017b0ad9
Update builder.sh to add x86 support.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-02-13 06:09:48 -05:00
Edu4rdSHL
6f87a2aa3f
Rusnapshot only works on Linux... because BTRFS does.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-02-13 05:41:39 -05:00
Edu4rdSHL
6ea79569d7
Merge branch 'master' of https://github.com/Edu4rdSHL/rusnapshot 2021-05-11 03:18:53 -05:00
Edu4rdSHL
665e6a7f85
Add builder.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-10 20:54:26 -05:00
29 changed files with 1120 additions and 1579 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
github: [edu4rdshl]

22
.github/workflows/rust.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

14
.rustfmt.toml Normal file
View file

@ -0,0 +1,14 @@
# Rustfmt configuration file.
# How imports should be grouped into use statements. Imports will be merged or split to the configured level of granularity.
imports_granularity = "One"
# Unix or Windows line endings
newline_style = "Unix"
# Convert /* */ comments to // comments where possible
normalize_comments = true
# Convert #![doc] and #[doc] attributes to //! and /// doc comments.
normalize_doc_attributes = true
# Remove nested parens.
remove_nested_parens = true
# Reorder impl items. type and const are put first, then macros and methods.
reorder_impl_items = true

1636
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,25 @@
[package] [package]
name = "rusnapshot" name = "rusnapshot"
version = "0.1.0" version = "0.5.3"
authors = ["edu4rdshl"] authors = ["edu4rdshl"]
edition = "2018" edition = "2024"
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.33.3", features = ["yaml"] } clap = { version = "4.5.47", features = ["derive", "env"] }
reqwest = { version = "0.11.3", features = ["default-tls"] } serde = { version = "1.0.219", features = ["derive"] }
openssl = { version = "0.10.30", features = ["vendored"] } sqlite = "0.37.0"
config = { version = "0.11.0", features = ["yaml", "json", "toml", "hjson", "ini"] } md5 = "0.8.0"
failure = "0.1.8" chrono = "0.4.42"
sqlite = "0.26.0" prettytable-rs = "0.10.0"
md5 = "0.7.0" anyhow = "1.0.99"
chrono = "0.4.19" toml = "0.9.5"
prettytable-rs = "0.8.0" hostname = "0.4.1"
[profile.release]
lto = true
codegen-units = 1
panic = 'abort'
strip = true

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Eduard Tolosa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,5 +1,5 @@
# Description # Description
Simple and handy btrfs snapshoting tool. Supports unattended snapshots, tracking, restoring, automatic cleanup and more. Backed with SQLite. Simple and handy definitions-based snapshoting tool for BTRFS. Supports unattended snapshots, tracking, restoring, automatic cleanup and more. Backed with SQLite.
# Documentation # Documentation
@ -7,16 +7,21 @@ See the [docs folder](https://github.com/Edu4rdSHL/rusnapshot/blob/master/docs/)
# Features # Features
**The main feature of Rustnapshot is the ability to create snapshots of BTRFS subvolumes using simple TOML definitions. It is designed to be simple and easy to use, while still providing powerful features for managing snapshots. See [the examples folder](https://github.com/Edu4rdSHL/rusnapshot/tree/master/examples) for more information.**
Among the features of Rustnapshot are:
- Allows you to specify the origin and destination of snapshots at will of the user. - Allows you to specify the origin and destination of snapshots at will of the user.
- Track snapshots using SQLite as backend database. - Track snapshots using SQLite as backend database.
- Easy setup using small templates instead of confusing long files. - Easy setup using small templates instead of confusing long files.
- You can create snapshots of the volumes you want simply by using different configuration templates. - Ability to create snapshots of the volumes you want simply by using different configuration templates.
- You can create read-only or read-write snapshots. - Ability to create read-only or read-write snapshots.
- You can use the same SQLite database for everything. - Ability to use the same SQLite database for everything.
- You can specify the prefix of the name for the snapshots. - Ability to specify the prefix of the name for the snapshots for better identification.
- You can specify a `kind` identifier to differentiate them in the database. Useful if you plan to have hourly, weekly, montly or more "kind" of snapshots of the same subvolume(s). - Ability to specify a `kind` identifier to differentiate them in the database. Useful if you plan to have hourly, weekly, monthly or more "kind" of snapshots of the same subvolume(s).
- Ability to specify the maximum number of snapshots to keep for automatic cleanup.
- Supports restoration of snapshots in the original directory or a specific one. - Supports restoration of snapshots in the original directory or a specific one.
- Automatic snapshots cleanup. - Supports machine name identification for better tracking when using the same database in multiple machines.
- Nice CLI output to see the status and details of snapshots. - Nice CLI output to see the status and details of snapshots.
# Known limitations # Known limitations

71
builder.sh Executable file
View file

@ -0,0 +1,71 @@
#!/usr/bin/env bash
NAME="rusnapshot"
LINUX_TARGET="x86_64-unknown-linux-musl"
LINUX_X86_TARGET="i686-unknown-linux-musl"
ARMV7_TARGET="armv7-unknown-linux-gnueabihf"
AARCH_TARGET="aarch64-unknown-linux-gnu"
MANPAGE_DIR="./$NAME.1"
BIN_OUTPUT_DIR="./ghbinaries"
rm -rf "$BIN_OUTPUT_DIR"
mkdir -p "$BIN_OUTPUT_DIR"
# Linux build
echo "Building Linux artifact."
if cross build -q --release --target="$LINUX_TARGET"; then
echo "Linux artifact build: SUCCESS"
cp "target/$LINUX_TARGET/release/$NAME" "target/$LINUX_TARGET/release/$NAME-linux"
strip "target/$LINUX_TARGET/release/$NAME-linux"
sha512sum "target/$LINUX_TARGET/release/$NAME-linux" >"$BIN_OUTPUT_DIR/$NAME-linux.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-linux-x64.zip" "target/$LINUX_TARGET/release/$NAME-linux"
else
echo "Linux artifact build: FAILED"
fi
# Linux x86 build
echo "Building Linux x86 artifact."
if cross build -q --release --target="$LINUX_X86_TARGET"; then
echo "Linux x86 artifact build: SUCCESS"
cp "target/$LINUX_X86_TARGET/release/$NAME" "target/$LINUX_X86_TARGET/release/$NAME-linux-i386"
strip "target/$LINUX_X86_TARGET/release/$NAME-linux-i386"
sha512sum "target/$LINUX_X86_TARGET/release/$NAME-linux-i386" >"$BIN_OUTPUT_DIR/$NAME-linux-i386.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-linux-i386.zip" "target/$LINUX_X86_TARGET/release/$NAME-linux-i386"
else
echo "Linux x86 artifact build: FAILED"
fi
# ARMV7 build
echo "Building ARMv7 artifact."
if cross build -q --release --target="$ARMV7_TARGET"; then
echo "ARMv7 artifact build: SUCCESS"
cp "target/$ARMV7_TARGET/release/$NAME" "target/$ARMV7_TARGET/release/$NAME-armv7"
strip "target/$ARMV7_TARGET/release/$NAME-armv7"
sha512sum "target/$ARMV7_TARGET/release/$NAME-armv7" >"$BIN_OUTPUT_DIR/$NAME-armv7.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-armv7.zip" "target/$ARMV7_TARGET/release/$NAME-armv7"
else
echo "ARMv7 artifact build: FAILED"
fi
# Aarch64 build
echo "Building Aarch64 artifact."
if cross build -q --release --target="$AARCH_TARGET"; then
echo "Aarch64 artifact build: SUCCESS"
cp "target/$AARCH_TARGET/release/$NAME" "target/$AARCH_TARGET/release/$NAME-aarch64"
strip "target/$AARCH_TARGET/release/$NAME-aarch64"
sha512sum "target/$AARCH_TARGET/release/$NAME-aarch64" >"$BIN_OUTPUT_DIR/$NAME-aarch64.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-aarch64.zip" "target/$AARCH_TARGET/release/$NAME-aarch64"
else
echo "Aarch64 artifact build: FAILED"
fi
echo "Creating manpage..."
if command -v help2man >/dev/null; then
if help2man -o "$MANPAGE_DIR" "target/$LINUX_TARGET/release/$NAME"; then
echo "Manpage created sucessfully and saved in $MANPAGE_DIR"
else
echo "Error creating manpage."
fi
else
echo "Please install the help2man package."
fi

View file

@ -14,7 +14,7 @@
- Create a snapshot using a [config file](https://github.com/Edu4rdSHL/rusnapshot/tree/master/examples/config-templates): - Create a snapshot using a [config file](https://github.com/Edu4rdSHL/rusnapshot/tree/master/examples/config-templates):
`sudo rusnapshot --config {{path/to/config.toml}} --cr` `sudo rusnapshot --config {{path/to/config.toml}} --create`
- List created snapshots: - List created snapshots:
@ -30,11 +30,11 @@
- Create a read-write snapshot: - Create a read-write snapshot:
`sudo rusnapshot -c {{path/to/config.toml}} --cr --rw` `sudo rusnapshot -c {{path/to/config.toml}} --create --rw`
- Restore a snapshot: - Restore a snapshot:
`sudo rusnapshot -c {{path/to/config.toml}} --id {{snapshot_id}} --restore` `sudo rusnapshot --id {{snapshot_id}} --restore`
# Notes # Notes

View file

@ -1,16 +1,17 @@
# Example configuration file for rustnapshot # Example configuration file for rusnapshot
# These are all the current available options # These are all the current available options
# Snapshots directory # Snapshots directory
dest_dir = "/mnt/defvol/_snapshots/" dest_dir = "/.snapshots"
# Directory to be snapshoted # Directory to be snapshot-ed
source_dir = "/mnt/defvol/_active/root/" source_dir = "/"
# SQLite database file PATH, created if no exists, recommended to be in the snapshots dir # SQLite database file PATH where snapshots metadata will be stored. Created if not exists
database_file = "/mnt/defvol/_snapshots/rustnapshot.sqlite" database_file = "/.snapshots/rusnapshot.db"
# Prefix for the snapshots, they will be saved in the format prefix-$current_date # Prefix for the snapshots, they will be saved in the format prefix-$current_date
snapshot_prefix = "root" snapshot_prefix = "root"
# Snapshots identifier # Snapshots identifier
snapshot_kind = "weekly" snapshot_kind = "weekly"
# Last snapshots to keep. If prefix is specified then only the snapshots with that prefix # Last snapshots to keep. If prefix is specified then only the snapshots with that prefix will be deleted
# are deleted keep_only = 3
keep_only = "2" # Machine name to be used in the snapshots metadata
machine = "Oribos"

View file

@ -1,5 +1,12 @@
dest_dir = "/mnt/defvol/_snapshots/" # Snapshots directory
source_dir = "/home/" dest_dir = "/.snapshots"
database_file = "/mnt/defvol/_snapshots/rustnapshot.sqlite" # Directory to be snapshot-ed
source_dir = "/home"
# SQLite database file PATH where snapshots metadata will be stored. Created if not exists
database_file = "/.snapshots/rusnapshot.db"
# Prefix for the snapshots, they will be saved in the format prefix-$current_date
snapshot_prefix = "home" snapshot_prefix = "home"
keep_only = "2" # Last snapshots to keep. If prefix is specified then only the snapshots with that prefix will be deleted
keep_only = 3
# Machine name to be used in the snapshots metadata
machine = "Oribos"

View file

@ -1,5 +1,12 @@
dest_dir = "/mnt/defvol/_snapshots/" # Snapshots directory
dest_dir = "/.snapshots"
# Directory to be snapshot-ed
source_dir = "/" source_dir = "/"
database_file = "/mnt/defvol/_snapshots/rustnapshot.sqlite" # SQLite database file PATH where snapshots metadata will be stored. Created if not exists
database_file = "/.snapshots/rusnapshot.db"
# Prefix for the snapshots, they will be saved in the format prefix-$current_date
snapshot_prefix = "root" snapshot_prefix = "root"
keep_only = "2" # Last snapshots to keep. If prefix is specified then only the snapshots with that prefix will be deleted
keep_only = 3
# Machine name to be used in the snapshots metadata
machine = "Oribos"

View file

@ -1,12 +1,9 @@
[Unit] [Unit]
Description=Snapshot root directory. Description=Daily snapshots for home and root subvolumes
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --cr --clean --kind three-hours ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --create --kind daily
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --cr --clean --kind three-hours --rw ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --clean --kind daily
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --cr --clean --kind three-hours ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --create --kind daily
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --cr --clean --kind three-hours --rw ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --clean --kind daily
[Install]
WantedBy=default.target

View file

@ -0,0 +1,9 @@
[Unit]
Description=Take daily snapshots with rusnapshot
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target

View file

@ -1,12 +1,9 @@
[Unit] [Unit]
Description=Take system snapshots monthly. Description=Monthly snapshots for home and root subvolumes
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --cr --clean --kind monthly ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --create --kind monthly
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --cr --clean --kind monthly --rw ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --clean --kind monthly
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --cr --clean --kind monthly ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --create --kind monthly
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --cr --clean --kind monthly --rw ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --clean --kind monthly
[Install]
WantedBy=default.target

View file

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Take system snapshots monthly. Description=Take monthly snapshots with rusnapshot
[Timer] [Timer]
OnCalendar=monthly OnCalendar=monthly

View file

@ -1,9 +0,0 @@
[Unit]
Description=Take snapshots every 3 hours.
[Timer]
OnBootSec=1h
OnUnitActiveSec=3h
[Install]
WantedBy=timers.target

View file

@ -1,12 +1,9 @@
[Unit] [Unit]
Description=Take system snapshots weekly. Description=Weekly snapshots for home and root subvolumes
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --cr --clean --kind weekly ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --create --kind weekly
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --cr --clean --kind weekly --rw ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-root.toml --clean --kind weekly
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --cr --clean --kind weekly ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --create --kind weekly
ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --cr --clean --kind weekly --rw ExecStart=/usr/bin/rusnapshot -c /etc/rusnapshot/config-home.toml --clean --kind weekly
[Install]
WantedBy=default.target

View file

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Take system snapshots weekly. Description=Take weekly snapshots with rusnapshot
[Timer] [Timer]
OnCalendar=weekly OnCalendar=weekly

View file

@ -1,155 +1,163 @@
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,
std::collections::BTreeMap,
}; };
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 = "")]
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 = "/.snapshots/rustnapshot.sqlite"
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 = "3")]
"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 = "10000")]
} 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.
/// This option requires: -k/--keep, -p/--prefix and --kind via command line or configuration file.
#[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,
/// Machine name to be used in the metadata.
#[clap(short, long, default_value = "")]
pub machine: String,
}
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(), /// Initialize the database and directory structure.
snapshot_id: value_t!(matches, "snapshot-id", String).unwrap_or_else(|_| String::new()), pub fn init(&self, extra_args: &ExtraArgs) -> Result<()> {
snapshot_prefix: if matches.is_present("snapshot-prefix") { if !std::path::Path::new(&self.dest_dir).exists() {
value_t!(matches, "snapshot-prefix", String) println!("Setting up the directory structure.");
.unwrap_or_else(|_| String::from("snapshot")) operations::setup_directory_structure(self)?;
} else { }
return_value_or_default(&settings, "snapshot_prefix", String::from("snapshot")) database::setup_initial_database(&extra_args.database_connection)?;
},
snapshot_kind: if matches.is_present("snapshot-kind") { Ok(())
value_t!(matches, "snapshot-kind", String) }
.unwrap_or_else(|_| String::from("rusnapshot"))
} else { pub fn check_for_source_and_dest_dir(&mut self) {
return_value_or_default(&settings, "snapshot_kind", String::from("rusnapshot")) if self.source_dir.is_empty() || self.dest_dir.is_empty() {
}, eprintln!("Specify both source and destination directories before taking a snapshot.");
snapshot_ro_rw: if matches.is_present("read-write") { std::process::exit(1);
"read-write".to_string() }
} else { }
"read-only".to_string()
}, /// Deserialize the configuration file.
keep_only: if matches.is_present("keep-only") { pub fn from_config_file(&mut self) -> Result<()> {
value_t!(matches, "keep-only", usize).unwrap_or_else(|_| 0) let config_file = std::fs::read_to_string(self.config_file.as_deref().unwrap())?;
} else { let config: BTreeMap<String, toml::Value> = toml::from_str(&config_file)?;
return_value_or_default(&settings, "keep_only", 0.to_string())
if let Some(dest_dir) = config.get("dest_dir") {
self.dest_dir = dest_dir
.as_str()
.expect("Failed to parse dest_dir")
.to_string();
}
if let Some(source_dir) = config.get("source_dir") {
self.source_dir = source_dir
.as_str()
.expect("Failed to parse source_dir")
.to_string();
}
if let Some(snapshot_prefix) = config.get("snapshot_prefix") {
self.snapshot_prefix = snapshot_prefix
.as_str()
.expect("Failed to parse snapshot_prefix")
.to_string();
}
if let Some(snapshot_kind) = config.get("snapshot_kind") {
self.snapshot_kind = snapshot_kind
.as_str()
.expect("Failed to parse snapshot_kind")
.to_string();
}
if let Some(database_file) = config.get("database_file") {
self.database_file = database_file
.as_str()
.expect("Failed to parse database_file")
.to_string();
}
if let Some(keep_only) = config.get("keep_only") {
self.keep_only = keep_only
.to_string()
.parse() .parse()
.unwrap_or_default() .expect("Failed to parse keep_only, make sure it's a number");
}, }
if let Some(timeout) = config.get("timeout") {
self.timeout = timeout
.to_string()
.parse()
.expect("Failed to parse timeout, make sure it's a number");
}
if let Some(machine) = config.get("machine") {
self.machine = machine
.to_string()
.parse()
.expect("Failed to parse machine");
} }
}
fn return_settings( Ok(())
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)
}
}
} 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(
settings: &HashMap<String, String>,
value: &str,
default_value: String,
) -> String {
settings.get(value).unwrap_or(&default_value).to_string()
}

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,76 @@
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::{Table, row},
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") } else {
eprintln!("Error while taking the snapshot");
}
Ok(())
}
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
); );
args.snapshot_id = format!("{:?}", md5::compute(&args.snapshot_name)); std::process::exit(1);
if operations::take_snapshot(args) { } else {
database::commit_to_database(&args.database_connection, args)? extra_args.snapshot_name = snapshot_data.destination + &snapshot_data.name;
}
if !extra_args.snapshot_name.is_empty() && operations::del_snapshot(&extra_args.snapshot_name) {
database::delete_from_database(&extra_args.database_connection, args)?;
} else {
eprintln!("Error while deleting the snapshot");
} }
Ok(()) Ok(())
} }
pub fn manage_deletion(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;
} 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);
if !args.snapshot_name.is_empty() && operations::del_snapshot(args) {
database::delete_from_database(&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;
if args.source_dir.is_empty() {
args.source_dir = snapshot_data.source;
}
} else { } else {
eprintln!( extra_args.snapshot_name = snapshot_data.destination + &snapshot_data.name;
"Snapshot ID {} does not returned any data. Please double check the ID.", if args.dest_dir.is_empty() {
args.snapshot_id // The current destination directory corresponds to the source directory
) // at the moment of the snapshot creation.
args.dest_dir = snapshot_data.source;
}
} }
if !args.snapshot_name.is_empty() && operations::restore_snapshot(args) {
println!( println!(
"The snapshot with ID {} was successfully restored to {}", "Restoring snapshot {} to {}",
args.snapshot_id, args.source_dir extra_args.snapshot_name, args.dest_dir
) );
if !extra_args.snapshot_name.is_empty()
&& operations::restore_snapshot(args, &extra_args.snapshot_name)
{
println!("The snapshot was successfully restored");
} else {
eprintln!("Error while restoring the snapshot");
} }
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![
@ -69,7 +79,8 @@ pub fn manage_listing(args: &mut Args) -> Result<()> {
"KIND", "KIND",
"SOURCE DIR", "SOURCE DIR",
"DESTINATION DIR", "DESTINATION DIR",
"RO/RW", "MACHINE",
"RW",
"DATE" "DATE"
]); ]);
@ -80,6 +91,7 @@ pub fn manage_listing(args: &mut Args) -> Result<()> {
data.kind, data.kind,
data.source, data.source,
data.destination, data.destination,
data.machine,
data.ro_rw, data.ro_rw,
data.date, data.date,
]); ]);
@ -89,12 +101,14 @@ 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.clone_from(&data.snap_id);
manage_deletion(args)?
manage_deletion(args, extra_args)?;
} }
Ok(()) Ok(())

View file

@ -1,24 +1,31 @@
use { use {
crate::{ crate::{
errors::*, args::Args,
structs::{Args, Database}, structs::{Database, ExtraArgs},
}, },
anyhow::Result,
sqlite::{Connection, State, Statement}, sqlite::{Connection, State, Statement},
}; };
pub fn setup_initial_database(connection: &Connection) -> Result<()> { pub fn setup_initial_database(connection: &Connection) -> Result<()> {
connection.execute("CREATE TABLE IF NOT EXISTS snapshots (name TEXT NOT NULL, snap_id TEXT NOT NULL, kind TEXT NOT NULL, source TEXT NOT NULL, destination TEXT NOT NULL, ro_rw TEXT NOT NULL, date TEXT DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(name, snap_id))")?; connection.execute("CREATE TABLE IF NOT EXISTS snapshots (name TEXT NOT NULL, snap_id TEXT NOT NULL, kind TEXT NOT NULL, source TEXT NOT NULL, destination TEXT NOT NULL, machine TEXT NOT NULL, ro_rw TEXT NOT NULL, date TEXT DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(name, snap_id))")?;
connection.execute("PRAGMA journal_mode=WAL")?; connection.execute("PRAGMA journal_mode=WAL")?;
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, machine, 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.machine,
args.read_write
); );
connection.execute(&statement)?; extra_args.database_connection.execute(statement)?;
Ok(()) Ok(())
} }
@ -28,7 +35,7 @@ pub fn delete_from_database(connection: &Connection, args: &Args) -> Result<()>
"DELETE FROM snapshots WHERE name = '{}' OR snap_id = '{}'", "DELETE FROM snapshots WHERE name = '{}' OR snap_id = '{}'",
args.snapshot_id, args.snapshot_id args.snapshot_id, args.snapshot_id
); );
connection.execute(&statement)?; connection.execute(statement)?;
Ok(()) Ok(())
} }
@ -40,8 +47,8 @@ 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)?;
} }
Ok(snap_data) Ok(snap_data)
@ -51,11 +58,10 @@ pub fn return_all_data(connection: &Connection) -> Result<Vec<Database>> {
let mut snapshots_data: Vec<Database> = Vec::new(); let mut snapshots_data: Vec<Database> = Vec::new();
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,machine,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)?);
snapshots_data.push(populate_db_struct(&statement, db_struct)?);
} }
Ok(snapshots_data) Ok(snapshots_data)
@ -64,23 +70,24 @@ 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,machine,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)?);
snapshots_data.push(populate_db_struct(&statement, db_struct)?);
} }
Ok(snapshots_data) Ok(snapshots_data)
} }
fn populate_db_struct(row: &Statement, mut db_struct: Database) -> Result<Database> { fn populate_db_struct(stmt: &Statement) -> Result<Database> {
db_struct.name = row.read::<String>(0)?; Ok(Database {
db_struct.snap_id = row.read::<String>(1)?; name: stmt.read(0)?,
db_struct.kind = row.read::<String>(2)?; snap_id: stmt.read(1)?,
db_struct.source = row.read::<String>(3)?; kind: stmt.read(2)?,
db_struct.destination = row.read::<String>(4)?; source: stmt.read(3)?,
db_struct.ro_rw = row.read::<String>(5)?; destination: stmt.read(4)?,
db_struct.date = row.read::<String>(6)?; machine: stmt.read(5)?,
Ok(db_struct) ro_rw: stmt.read(6)?,
date: stmt.read(7)?,
})
} }

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,51 @@
use rusnapshot::{args, controller, database, errors::*}; use {
anyhow::Result,
chrono::Utc,
clap::Parser,
rusnapshot::{args, controller, structs::ExtraArgs, utils},
};
fn run() -> Result<()> { fn try_run() -> Result<()> {
let mut arguments = args::get_args(); let mut arguments = args::Args::parse();
if !arguments.source_dir.is_empty() && !arguments.source_dir.ends_with('/') { if arguments.config_file.is_some() {
arguments.source_dir += "/" arguments.from_config_file()?;
}
if !arguments.dest_dir.is_empty() && !arguments.dest_dir.ends_with('/') {
arguments.dest_dir += "/"
} }
database::setup_initial_database(&arguments.database_connection)?; 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(),
};
if arguments.create_snapshot { if arguments.create_snapshot {
controller::manage_creation(&mut arguments)? utils::check_creation_requirements(&mut arguments, &extra_args)?;
}
if arguments.create_snapshot {
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,18 @@
use { use {
crate::structs::Args, crate::{args::Args, utils::is_same_character},
std::{path::Path, process::Command}, anyhow::Result,
std::{
path::{Path, PathBuf},
process::Command,
},
}; };
pub fn take_snapshot(args: &Args) -> bool { #[must_use]
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]; 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,29 +21,39 @@ pub fn take_snapshot(args: &Args) -> bool {
.success() .success()
} }
pub fn del_snapshot(args: &Args) -> bool { #[must_use]
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: {snapshot_name}. Refusing to delete the root subvolume."
);
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 { #[must_use]
(!Path::new(&args.source_dir).exists() pub fn restore_snapshot(args: &Args, snapshot_name: &str) -> bool {
|| Command::new("btrfs") !Path::new(&args.dest_dir).exists()
.args(&["subvolume", "delete", &args.source_dir])
.status()
.expect("Error deleting the subvolume.")
.success())
&& Command::new("btrfs") && Command::new("btrfs")
.args(&[ .args(["subvolume", "snapshot", snapshot_name, &args.dest_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,
@ -25,19 +8,11 @@ pub struct Database {
pub source: String, pub source: String,
pub destination: String, pub destination: String,
pub ro_rw: String, pub ro_rw: String,
pub machine: String,
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(),
}
}
} }

50
src/utils.rs Normal file
View file

@ -0,0 +1,50 @@
use {
crate::{args::Args, structs::ExtraArgs},
anyhow::Result,
std::path::Path,
};
#[must_use]
pub fn is_same_character(s: &str, specific_char: char) -> bool {
s.chars().all(|c| c == specific_char)
}
pub fn check_creation_requirements(arguments: &mut Args, extra_args: &ExtraArgs) -> Result<()> {
arguments.check_for_source_and_dest_dir();
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 += "/";
}
if !arguments.dest_dir.is_empty() && !arguments.dest_dir.ends_with('/') {
arguments.dest_dir += "/";
}
// 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 !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.machine.is_empty() {
arguments.machine = hostname::get()?.to_string_lossy().to_string();
}
Ok(())
}