commit f17345dae802c18ab5a6727ca2ac56df3cd12dc0 Author: 洛天依 Date: Mon Jan 20 02:50:30 2025 +0000 initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2087dd7 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..39629d5 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7846b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +includes +excludes +passwd +rclone.conf diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aeb7971 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Tianyi CodeLab + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbaa58b --- /dev/null +++ b/README.md @@ -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. diff --git a/backupd b/backupd new file mode 100755 index 0000000..d87f6da --- /dev/null +++ b/backupd @@ -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 "$@" diff --git a/excludes.example b/excludes.example new file mode 100644 index 0000000..4ca5399 --- /dev/null +++ b/excludes.example @@ -0,0 +1 @@ +/opt/containerd diff --git a/includes.example b/includes.example new file mode 100644 index 0000000..485c107 --- /dev/null +++ b/includes.example @@ -0,0 +1,9 @@ +/usr/local/bin +/usr/local/etc +/etc/passwd +/etc/group +/etc/nginx +/etc/systemd/system +/var/log/nginx +/srv +/opt diff --git a/passwd.example b/passwd.example new file mode 100644 index 0000000..156122c --- /dev/null +++ b/passwd.example @@ -0,0 +1,2 @@ +AES_PASSWD=some-password +ITER_COUNT=100000 diff --git a/rclone.conf.example b/rclone.conf.example new file mode 100644 index 0000000..78d699e --- /dev/null +++ b/rclone.conf.example @@ -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