- Python 100%
This PR makes a pre-flight check to confirm bucket access permissions before starting any actual processing. Depending on the AWS authorization used, for example, sessions can time out while the archive is being created. This check prevents these expensive IO operations from being performed if there is an issue accessing the bucket _at the start of a run._ We can not determine if a timeout occurred while still archiving since we have not implemented multi-threading (yet). Reviewed-on: https://codeberg.org/vlw/3rd/pulls/16 |
||
|---|---|---|
| src | ||
| .example.config.json | ||
| .gitignore | ||
| backup.py | ||
| LICENSE | ||
| README.md | ||
| restore.py | ||
3rd
3rd is a small Linux backup tool for the off-site copy in a 3-2-1 backup setup. It creates encrypted 7z archives from configured files or directories, uploads them to AWS S3, and can restore them back into their original directory structure.
The main advantage is that related paths can be backed up as separate archives while still restoring cleanly as one tree. This is useful when a large directory contains a subdirectory of binaries, media, or other files that should use different compression, encryption, storage, or upload settings.
The backup script also includes a "last change detection", so unchanged targets are skipped if the newest version already exists in S3. This check is performed locally by keeping track of when each target was modified and uploaded.
Features
- Per-file or per-directory backup configuration.
- Independent compression level, password, temp directory, and S3 destination per target.
- Uploads use the S3 Glacier Deep Archive storage class.
- Split archives for configured child targets.
.placeholderfiles in parent archives that point to the S3 archive containing the excluded child target.- Restore-in-place behavior: all archives preserve enough path structure to extract into the same target tree.
- Last change detection, so unchanged targets are skipped if the newest version already exists in S3.
3rd is a wrapper around the AWS CLI aws and the 7zip CLI 7z. It is intended for Linux; other operating systems are untested.
Requirements
- Python 3 (v3.11.2)
- 7zip CLI (v26.02)
- AWS CLI (v2.32.23)
- Write access to an AWS S3 bucket
- Read access to the same bucket when restoring (optional)
Installation
Clone the repository:
git clone https://codeberg.org/vlw/3rd
cd 3rd
Create your config file:
cp -p .example.config.json .config.json
Edit .config.json and set the absolute paths and S3 destinations for your backups.
Configuration
The config file is a JSON array. Each object describes one archive target:
[
{
"password": "mypassword",
"compression": 10,
"abspath_temp": "/tmp",
"last_uploaded": null,
"abspath_target": "/home/me/archive",
"abspath_destination": "s3://my-bucket/archive.7z"
}
]
Fields:
| Key | Description |
|---|---|
password |
7z AES-256 password. Set to false to disable encryption. |
compression |
Compression level from 0 to 10. Use 0, false, or null to store without compression. |
abspath_temp |
Directory for temporary archives before upload or after download. Use null for the system temp directory. |
last_uploaded |
Modified datetime recorded after successful upload. Use null for a first run. |
abspath_target |
Absolute path to the file or directory to archive or restore. |
abspath_destination |
S3 URL for the archive. Uploaded archives use the S3 Glacier Deep Archive storage class. |
Note
last_uploadedis managed bybackup.py. When the current target modified datetime matcheslast_uploaded, the target is skipped without creating an archive.
Split Archives
You can configure both a parent directory and one or more paths inside it:
[
{
"password": "mypassword",
"compression": 10,
"abspath_temp": null,
"abspath_target": "/home/me/archive",
"abspath_destination": "s3://my-bucket/archive.7z",
"last_uploaded": null
},
{
"password": false,
"compression": 0,
"abspath_temp": null,
"abspath_target": "/home/me/archive/videos",
"abspath_destination": "s3://my-bucket/archive-videos.7z",
"last_uploaded": null
}
]
In this example, /home/me/archive/videos is not included in archive.7z. Instead, the parent archive contains:
archive/videos.placeholder
The placeholder file contains the S3 destination for the split archive:
s3://my-bucket/archive-videos.7z
This allows parent archives to be heavily compressed while skipping files like compressed video from recompressing. "Ultra" compression will waste a lot of CPU cycles on already-compressed files.
Backup
Run a backup:
python3 backup.py -i .config.json
Dry run:
python3 backup.py -i .config.json --dryrun
When a target has not changed since last_uploaded, the command prints a skipped message and does not run 7z or AWS for that target.
Restore
Restore all configured archives:
python3 restore.py -i .config.json
Archives are downloaded into abspath_temp, extracted into the correct path structure (abspath_target), and then removed from the temp directory.
After restore, generated .placeholder files are removed automatically. To only remove placeholders without downloading from AWS:
python3 restore.py -i .config.json --placeholders-only
CLI
backup.py:
| Argument | Description |
|---|---|
-s, --sleep |
Global sleep duration between logged commands. |
-i, --input |
Config file path. Defaults to .config.json. |
-l, --log-level |
Console log level. See src/Enums.py. |
-d, --dryrun |
Create archives but do not upload; preserved archives remain in temp storage. |
restore.py:
| Argument | Description |
|---|---|
-s, --sleep |
Global sleep duration between logged commands. |
-i, --input |
Config file path. Defaults to .config.json. |
-l, --log-level |
Console log level. See src/Enums.py. |
--placeholders-only |
Remove generated .placeholder files without downloading or extracting archives. |
Cron
Example weekly backup:
30 2 * * 3 cd /opt/3rd && /usr/bin/python3 backup.py -i .config.json
This runs every Wednesday at 02:30.