mirror of
https://github.com/edu4rdshl/rusnapshot.git
synced 2026-07-17 23:24:55 +00:00
Compare commits
55 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1351a6bd9b | |||
| a2de007c87 | |||
| 2f47feccd7 | |||
| daca0998fa | |||
| 2c8c16ad4e | |||
| 0285857b91 | |||
| c2d1d12abf | |||
| d57c34e9d1 | |||
| bfca23d1c7 | |||
| ae403db784 | |||
| f23e74c610 | |||
| 9c7f10c186 | |||
| b47ebd7616 | |||
| 97fa393825 | |||
| 8f67b4d044 | |||
| 7b363e0207 | |||
| 2a2058f781 | |||
| bd0887f416 | |||
| 5e1a3e7115 | |||
| 0b117ac07c | |||
| b8b2c6f0f8 | |||
| 844b6543b4 | |||
| e421db4013 | |||
| 573c50d356 | |||
| 41951edb8b | |||
| 235945c8e8 | |||
| 01e518d3d3 | |||
| e24d29c048 | |||
| 230b2babbe | |||
| 14c0f371d2 | |||
| 53ca8d4773 | |||
| 4648c12379 | |||
| a9aa9d9eae | |||
| 07ae19da1b | |||
| 933c780ccc | |||
| df68951767 | |||
| 68fb2b0157 | |||
| 98b9313cce | |||
| 142370e703 | |||
| 962471de48 | |||
| 0a1beeb3b7 | |||
| ca4e96a662 | |||
| a231f177b5 | |||
| ddcc8d58d9 | |||
| 33a6153f23 | |||
| 502753acf9 | |||
| bac3a8c69e | |||
| c32742b355 | |||
| b19e7579c6 | |||
| f69fee8287 | |||
|
|
e68c28b223 | ||
|
|
56017b0ad9 | ||
|
|
6f87a2aa3f | ||
|
|
6ea79569d7 | ||
|
|
665e6a7f85 |
29 changed files with 1120 additions and 1579 deletions
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
github: [edu4rdshl]
|
||||||
22
.github/workflows/rust.yml
vendored
Normal file
22
.github/workflows/rust.yml
vendored
Normal 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
14
.rustfmt.toml
Normal 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
1636
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
29
Cargo.toml
29
Cargo.toml
|
|
@ -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
21
LICENSE
Normal 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.
|
||||||
19
README.md
19
README.md
|
|
@ -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
71
builder.sh
Executable 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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
9
examples/services/rusnapshot-daily.timer
Normal file
9
examples/services/rusnapshot-daily.timer
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Take daily snapshots with rusnapshot
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Take system snapshots monthly.
|
Description=Take monthly snapshots with rusnapshot
|
||||||
|
|
||||||
[Timer]
|
[Timer]
|
||||||
OnCalendar=monthly
|
OnCalendar=monthly
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Take snapshots every 3 hours.
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=1h
|
|
||||||
OnUnitActiveSec=3h
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Take system snapshots weekly.
|
Description=Take weekly snapshots with rusnapshot
|
||||||
|
|
||||||
[Timer]
|
[Timer]
|
||||||
OnCalendar=weekly
|
OnCalendar=weekly
|
||||||
|
|
|
||||||
280
src/args.rs
280
src/args.rs
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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,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(())
|
||||||
|
|
|
||||||
|
|
@ -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)?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
48
src/main.rs
48
src/main.rs
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
50
src/utils.rs
Normal 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(())
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue