initial commit
All checks were successful
Lint / Lint (push) Successful in 15s

This commit is contained in:
洛天依 2025-01-20 02:50:30 +00:00
commit f17345dae8
Signed by: luo
SSH Key Fingerprint: SHA256:V1KdsvGUpiKVfrJo1oHrAPnc/Z6k/6xgaZN7iTbYBl4
10 changed files with 298 additions and 0 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true

14
.github/workflows/lint.yaml vendored Normal file
View File

@ -0,0 +1,14 @@
on: [push, pull_request]
name: Lint
jobs:
build:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: setup-shellcheck
run: sudo apt-get update && sudo apt-get install shellcheck
- run: shellcheck -x backupd

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
includes
excludes
passwd
rclone.conf

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Tianyi CodeLab <https://lty.name/>
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.

126
README.md Normal file
View File

@ -0,0 +1,126 @@
# BackupD
A simple backup script.
## Installation
Prerequisites:
- **OS MUST be Debian 12**
- CLI Tools:
- `rclone`
- `openssl`
- `tar`
- `zstd`
- `sha256sum`
- If you need to run the script as scheduled tasks, you need to use `cron` or `systemd-timer`.
Clone the repository:
```bash
git clone https://devops.lty.name/luo/backupd.git /opt/backupd
```
## Configuration
**Ownership**
Don't forget to change the ownership of the directory:
```bash
chown -R root:root /opt/backupd
```
**Rclone**
Run the following command to initialize Rclone:
```bash
export RCLONE_CONFIG=/opt/backupd/rclone.conf
rclone config
```
See `rclone.conf.example` for an example configuration.
Ensure `dest` section exist in `rclone.conf`. Otherwise, the script will **fail and work unexpectedly**.
**Includes and Excludes**
You also need to configure the `includes` and `excludes` files.
- `includes`: Files and directories to be backed up.
- `excludes`: Files and directories to be excluded from the backup.
See `includes.example` and `excludes.example` for example configurations.
**Encryption**
You **MUST** set the encryption password in the `passwd` file.
```bash
AES_PASSWD="your_password"
ITER_COUNT=100000
```
See `passwd.example` for an example configuration.
If you did not set the password, the script will encrypt your backup with your hostname, which **IS NOT SECURE**.
**Scheduled Tasks**
If you wish to run the script as scheduled tasks, copy the fillowing files to `/etc/systemd/system/`:
- `backupd.service`
- `backupd.timer`
```bash
cp /opt/backupd/backupd.service /etc/systemd/system/
cp /opt/backupd/backupd.timer /etc/systemd/system/
```
Then, enable and start the timer:
```bash
systemctl enable backupd.service
systemctl start backupd.service
systemctl enable --now backupd.timer
```
## Restore the Backup
First, ensure the required environment variables are set:
```bash
export RCLONE_CONFIG=/opt/backupd/rclone.conf
ITER_COUNT=100000
AES_PASSWD=
```
Then, view the list of backups:
```
rclone tree dest:
```
Fetch the backup you want to restore:
```bash
server=
rclone copy -P dest:server-$server/ ./restore-$server
cd ./restore-$server
```
Check the integrity of the backup:
```bash
for file in *.enc; do
rclone lsjson -M "dest:server-$server/$file" > "$file.metadata"
output=$(echo "$file" | cut -d"_" -f3-4 | cut -d"." -f1 | tr ':" ' '-').tar.zst
openssl enc -d -aes-256-cbc -pbkdf2 -iter $ITER_COUNT -k "$AES_PASSWD" -in "$file" -out "$output"
enc_hash=$(cat "$file.metadata" | jq -r '.[].Metadata."sha256-enc"')
zst_hash=$(cat "$file.metadata" | jq -r '.[].Metadata."sha256-zst"')
echo "$enc_hash $file" | sha256sum -c
echo "$zst_hash $output" | sha256sum -c
done
```
Decompress the backup:
```bash
for file in *.tar.zst; do
out=$server-${file%.tar.zst}
mkdir -p "$out" && tar -xvf "$file" -C "./$server-${file%.tar.zst}"
done
```
## Contributing Notice
If you wish to contribute to this project, please make sure you use `shellcheck` to lint the script.
```bash
shellcheck -x backupd
```
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

98
backupd Executable file
View File

@ -0,0 +1,98 @@
#!/usr/bin/bash
# backupd - v1.0.0
# a simple backup script
#
# Copyright (c) 2025 Tianyi CodeLab, under the MIT License.
DATA_DIR="$(cd "$(dirname "$0")" && pwd)"
RCLONE_DEST_NAME="dest"
ZSTD_LEVEL=13
main() {
local backup_name backup_remote
backup_name="backups_$(hostname)_$(date +%Y-%m-%d)_$(date +%H:%M:%S)"
backup_remote="$RCLONE_DEST_NAME:server-$(hostname)"
check_files
# shellcheck disable=SC1091
. "$DATA_DIR/passwd"
if [ -z "$AES_PASSWD" ]; then
echo -ne "\033[1;31mAES_PASSWD is not set in file 'passwd'. Using default hostname as password.\033[0m\n"
echo -ne "\033[1;31mWarning: THIS IS NOT SECURE!\033[0m\n"
AES_PASSWD="$(hostname)"
fi
if [ -z "$ITER_COUNT" ]; then
echo -ne "\033[1;31mITER_COUNT is not set in file 'passwd'. Using default value 100000.\033[0m\n"
ITER_COUNT=100000
fi
local ctx
local tar_file zst_file enc_file ext_list inc_list
local tar_hash zst_hash enc_hash
ctx="$(mktemp -d)"
tar_file="$ctx/$backup_name.tar"
zst_file="$ctx/$backup_name.tar.zst"
enc_file="$ctx/$backup_name.tar.zst.enc"
ext_list="$DATA_DIR/excludes"
inc_list="$DATA_DIR/includes"
echo -ne "\033[1;33mCreating backup $backup_name\033[0m\n"
exec_hooks pre
echo -ne "\033[1;33mCreating archive $tar_file\033[0m\n"
tar -cvf "$tar_file" -X "$ext_list" -T "$inc_list"
echo -ne "\033[1;33mCompressing archive $zst_file\033[0m\n"
zstd -T -${ZSTD_LEVEL} "$tar_file" -o "$zst_file"
echo -ne "\033[1;33mEncrypting archive $enc_file\033[0m\n"
openssl enc -aes-256-cbc -salt -pbkdf2 -iter "$ITER_COUNT" -in "$zst_file" -out "$enc_file" -k "$AES_PASSWD"
tar_hash="$(sha256sum "$tar_file" | cut -d ' ' -f 1)"
zst_hash="$(sha256sum "$zst_file" | cut -d ' ' -f 1)"
enc_hash="$(sha256sum "$enc_file" | cut -d ' ' -f 1)"
echo -ne "\033[1;34mArchive hash: $tar_hash\033[0m\n"
echo -ne "\033[1;34mCompressed hash: $zst_hash\033[0m\n"
echo -ne "\033[1;34mEncrypted hash: $enc_hash\033[0m\n"
echo -ne "\033[1;33mUploading archive to remote...\033[0m\n"
echo -ne "\033[1;34mRemote filename: $backup_remote\033[0m\n"
RCLONE_CONFIG="$DATA_DIR/rclone.conf" rclone copy \
-vv --checksum --no-traverse --s3-no-check-bucket --metadata \
--metadata-set "sha256-enc=$enc_hash" \
--metadata-set "sha256-zst=$zst_hash" \
--metadata-set "sha256-tar=$tar_hash" \
-P "$enc_file" "$backup_remote"
exec_hooks post
rm -rvf "$ctx"
echo -ne "\033[1;34m------------ backup done ------------\033[0m\n"
}
check_files() {
if [ ! -f "$DATA_DIR/includes" ]; then
echo -ne "\033[1;31mCreating configuration file 'includes'...\033[0m\n"
touch "$DATA_DIR/includes"
fi
if [ ! -f "$DATA_DIR/excludes" ]; then
echo -ne "\033[1;31mCreating configuration file 'excludes'...\033[0m\n"
touch "$DATA_DIR/excludes"
fi
if [ ! -d "$DATA_DIR/hooks-pre.d" ]; then
echo -ne "\033[1;31mCreating directory 'hooks-pre.d'...\033[0m\n"
mkdir -p "$DATA_DIR/hooks-pre.d"
fi
if [ ! -d "$DATA_DIR/hooks-post.d" ]; then
echo -ne "\033[1;31mCreating directory 'hooks-post.d'...\033[0m\n"
mkdir -p "$DATA_DIR/hooks-post.d"
fi
}
exec_hooks() {
local hook_type="$1"
echo -ne "\033[1;32mRunning $hook_type hooks...\033[0m\n"
for hook in "$DATA_DIR/hooks-$hook_type.d"/*; do
if [ -x "$hook" ]; then
echo -ne "------------ Running hook: $hook\n"
"$hook"
fi
done
}
main "$@"

1
excludes.example Normal file
View File

@ -0,0 +1 @@
/opt/containerd

9
includes.example Normal file
View File

@ -0,0 +1,9 @@
/usr/local/bin
/usr/local/etc
/etc/passwd
/etc/group
/etc/nginx
/etc/systemd/system
/var/log/nginx
/srv
/opt

2
passwd.example Normal file
View File

@ -0,0 +1,2 @@
AES_PASSWD=some-password
ITER_COUNT=100000

11
rclone.conf.example Normal file
View File

@ -0,0 +1,11 @@
[s3]
type = s3
provider = Other
access_key_id = YOUR_ACCESS_KEY_ID
secret_access_key = YOUR_SECRET_ACCESS_KEY
endpoint = https://minio.your-domain.example.com
acl = private
[dest]
type = alias
remote = s3:your-bucket-name