Added documentation
This commit is contained in:
BIN
.usage.gif
Normal file
BIN
.usage.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 793 KiB |
122
README.md
122
README.md
@@ -1,3 +1,121 @@
|
|||||||
# backup.py
|
<div align="center">
|
||||||
|
<h1>backup.py</h1>
|
||||||
|
<h6><i>Modular and lightweight backup utility to save, encrypt and verify your personal data.</i></h6>
|
||||||
|
<img src=".usage.gif" />
|
||||||
|
</div>
|
||||||
|
|
||||||
Work in progress
|
`backup.py` is a CLI utility written in Python designed to backup small-scale UNIX environments
|
||||||
|
such as VPS, personal servers and workstations. It relies on `tar`, `gpg` and Python builtin
|
||||||
|
modules to copy, compress and encrypt your files.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
`backup.py` supports two major options: `--backup`, to create a new backup and `--extract`
|
||||||
|
to extract an existing backup archive.
|
||||||
|
|
||||||
|
In order to create a backup file, you first need to create a *"sources file"* to specify
|
||||||
|
the absolute paths to backup. For instance:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# directories end with a slash...
|
||||||
|
photos=/home/marco/Pictures/
|
||||||
|
documents=/home/marco/Documents/
|
||||||
|
# while individual files do not
|
||||||
|
wireguard=/etc/wireguard/wg0.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you can start the backup with the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ sudo ./backup.py -V --checksum --backup sources.ini /home/marco "very_bad_pw"
|
||||||
|
Copying photos (1/3)
|
||||||
|
Computing checksums...DONE
|
||||||
|
└──Computing checksums... [██████████████████████████████] 100.0% (2/2) - (processing 'wallpaper2.jpg')
|
||||||
|
Copying documents (2/3)
|
||||||
|
Computing checksums...DONE
|
||||||
|
└──Computing checksums... [██████████████████████████████] 100.0% (7844/7844) - (processing '__init__.cpython-313.pyc')
|
||||||
|
Copying wireguard (3/3)
|
||||||
|
Computing checksums...DONE
|
||||||
|
└──Computing checksums... [██████████████████████████████] 100.0% (1/1) - (processing 'wg0.conf')
|
||||||
|
Compressing backup...DONE
|
||||||
|
└──Compressing backup... [██████████████████████████████] 100.0% (9062/9061) - (processing 'wg0.conf')
|
||||||
|
Encrypting backup...DONE
|
||||||
|
File name: '/home/marco/backup-wood-20260122.tar.gz.enc'
|
||||||
|
Checksums file: '/home/marco/backup-wood-20260122.sha256'
|
||||||
|
File size: 5533767184 bytes (5.15 GiB)
|
||||||
|
Elapsed time: 2 minutes, 10 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
Both the `--checksum` and the `--verbose` (`-V`) flags are optional. The former generates a checksum
|
||||||
|
file containing the hashes of each single file, the latter enables the *verbose mode*, which renders
|
||||||
|
the user interface as shown before.
|
||||||
|
|
||||||
|
To extract an existing backup, you can instead proceed as follows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ ./backup.py -V -c --extract backup-wood-20260122.tar.gz.enc "very_bad_pw" backup-wood-20260122.sha256
|
||||||
|
Decrypting backup...DONE
|
||||||
|
Extracting backup...DONE
|
||||||
|
└──Extracting backup... [██████████████████████████████] 100.0% (9062/9062) - (processing 'wg0.conf')
|
||||||
|
Verifying backup...DONE
|
||||||
|
└──Verifying backup... [██████████████████████████████] 100.0% (7847/7847) - (processing '__init__.cpython-313.pyc')
|
||||||
|
Backup extracted to: '/home/marco/Projects/backup.py/backup.py.tmp'
|
||||||
|
Elapsed time: 1 minute, 3 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a new directory named `backup.py.tmp` on your local path. Just like before,
|
||||||
|
the `-V` and the `-c` options are optional.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
As stated before, `python.py` is built from scratch in modern Python (3.10+) without using
|
||||||
|
any external dependency except for `tar` and `gpg`.
|
||||||
|
|
||||||
|
The *sources file* follows an INI-like syntax structured using associative records between
|
||||||
|
labels and absolute paths. In order words:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
<label>=<path>
|
||||||
|
```
|
||||||
|
|
||||||
|
where:
|
||||||
|
- `<label>` is a descriptive name of a backup entry;
|
||||||
|
- `<path>` is the absolute path to a directory or a file.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# File 'server_backup.ini'
|
||||||
|
# List directories and files to backup
|
||||||
|
#
|
||||||
|
nginx=/etc/nginx/
|
||||||
|
ssh=/etc/ssh/
|
||||||
|
www=/var/www/html/
|
||||||
|
|
||||||
|
# no slash here ----v
|
||||||
|
host_file=/etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, individual files are specified by omitting the trailing slash at the end
|
||||||
|
of the absolute path. Comments, on the other hand, are inserted using the `#` token. Blank
|
||||||
|
lines are ignored.
|
||||||
|
|
||||||
|
Internally, `backup.py` orchestrates several UNIX utilities to create backups. In particular,
|
||||||
|
it follows the procedure listed below:
|
||||||
|
|
||||||
|
1. **Copy phase**: uses Python `shutil.copytree()` to copy files while preserving metadata and
|
||||||
|
symlinks (without following them) and by ignoring special files;
|
||||||
|
2. **Compression**: creates a gzip-compressed tar archive using GNU tar;
|
||||||
|
3. **Encryption**: encrypts the archive with GPG using AES-256 symmetric encryption;
|
||||||
|
4. **Checksum** (optional): computes SHA256 hashes for each file in the backup archive.
|
||||||
|
|
||||||
|
The backup process creates temporary files in `backup.py.tmp` and `backup.py.tar.gz`, which are
|
||||||
|
automatically cleaned up on completion or interruption (i.e., `C-c`).
|
||||||
|
|
||||||
|
## Old version
|
||||||
|
This implementation of `backup.py` is a porting of an old backup script originally written in Bash
|
||||||
|
that I developed back in 2018. While this new version should be compatible with old backup archives,
|
||||||
|
it may start to diverge at a certain point in the future. If you're experience incompatibilities and want
|
||||||
|
to revert to the original version, you can do so by visiting the
|
||||||
|
[latest stable commit](https://git.marcocetica.com/marco/backup.py/src/commit/786c30ef14abe2056dfa5cb250b766db73ca71aa).
|
||||||
|
|
||||||
|
## License
|
||||||
|
This software is released under GPLv3. You can obtain a copy of this license by visiting [this page](https://choosealicense.com/licenses/gpl-3.0/).
|
||||||
|
|||||||
@@ -502,7 +502,7 @@ class Backup:
|
|||||||
|
|
||||||
print(f"File name: '{backup_archive}'")
|
print(f"File name: '{backup_archive}'")
|
||||||
if config.checksum:
|
if config.checksum:
|
||||||
print(f"Checksum file: '{checksum_file}'")
|
print(f"Checksums file: '{checksum_file}'")
|
||||||
print(f"File size: {file_size} bytes ({file_size_hr})")
|
print(f"File size: {file_size} bytes ({file_size_hr})")
|
||||||
print(f"Elapsed time: {self.prettify_timestamp(elapsed_time)}")
|
print(f"Elapsed time: {self.prettify_timestamp(elapsed_time)}")
|
||||||
|
|
||||||
@@ -620,7 +620,7 @@ class Backup:
|
|||||||
with open(checksum_file, 'r') as cf:
|
with open(checksum_file, 'r') as cf:
|
||||||
expected_hashes = set(line.strip() for line in cf if line.strip())
|
expected_hashes = set(line.strip() for line in cf if line.strip())
|
||||||
except IOError as err:
|
except IOError as err:
|
||||||
return Err(f"Failed to load checksum file: {err}.")
|
return Err(f"Failed to load checksums file: {err}.")
|
||||||
|
|
||||||
files = Backup.collect_files(extracted_dir)
|
files = Backup.collect_files(extracted_dir)
|
||||||
progress = None
|
progress = None
|
||||||
@@ -792,7 +792,7 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not checksum_file.exists():
|
if not checksum_file.exists():
|
||||||
print(f"Checksum file '{checksum_file}' does not exist.", file=sys.stderr)
|
print(f"Checksums file '{checksum_file}' does not exist.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
extract_res = backup.extract_backup(archive_file, decryption_pass, checksum_file, args.verbose)
|
extract_res = backup.extract_backup(archive_file, decryption_pass, checksum_file, args.verbose)
|
||||||
|
|||||||
12
sources.ini
Normal file
12
sources.ini
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Backup.py sample sources file
|
||||||
|
# To backup new entries, add a mapping
|
||||||
|
# of the time '<LABEL>=<PATH>'
|
||||||
|
#
|
||||||
|
|
||||||
|
# directories end with a slash...
|
||||||
|
web_server=/var/www/
|
||||||
|
ssh=/etc/ssh/
|
||||||
|
|
||||||
|
# while individual files do not
|
||||||
|
wireguard=/etc/wireguard/wg0.conf
|
||||||
|
firewall=/etc/nftables.conf
|
||||||
Reference in New Issue
Block a user