backup /data/ directory #172
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: backup /data/ directory | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: '0 0 * * *' | |
| jobs: | |
| backup-data: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| REMOTE: deploy@45.76.134.105 | |
| TARGET: /var/www/fridge.dev | |
| REMOTE_BACKUP_DIR: /home/deploy | |
| GDRIVE_REMOTE: gdrive | |
| GDRIVE_FOLDER_ID: ${{ secrets.GDRIVE_BACKUP_FOLDER_ID }} | |
| steps: | |
| - name: install ssh, rclone, and jq | |
| run: sudo apt-get update && sudo apt-get install -y openssh-client rclone jq | |
| - name: set up ssh | |
| env: | |
| DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} | |
| RCLONE_CONFIG_CONTENT: ${{ secrets.RCLONE_CONFIG }} | |
| run: | | |
| mkdir -p ~/.ssh ~/.config/rclone | |
| printf '%s\n' "$DEPLOY_KEY" | tr -d '\r' > ~/.ssh/id_ed25519 | |
| chmod 600 ~/.ssh/id_ed25519 | |
| ssh-keyscan -H 45.76.134.105 >> ~/.ssh/known_hosts | |
| printf '%s' "$RCLONE_CONFIG_CONTENT" > ~/.config/rclone/rclone.conf | |
| chmod 600 ~/.config/rclone/rclone.conf | |
| - name: create remote backup archive | |
| id: remote-backup | |
| run: | | |
| set -euo pipefail | |
| remote_archive_path="$( | |
| ssh "$REMOTE" TARGET="$TARGET" REMOTE_BACKUP_DIR="$REMOTE_BACKUP_DIR" bash -s <<'EOF' | |
| set -euo pipefail | |
| [ "$TARGET" = "/var/www/fridge.dev" ] || { echo "refusing to back up unexpected target: $TARGET" >&2; exit 1; } | |
| [ -d "$TARGET" ] || { echo "site directory not found: $TARGET" >&2; exit 1; } | |
| cd "$TARGET" | |
| command -v zip >/dev/null 2>&1 || { echo "zip is not installed on the server" >&2; exit 1; } | |
| [ -d data ] || { echo "data directory not found at $TARGET/data" >&2; exit 1; } | |
| [ -w "$REMOTE_BACKUP_DIR" ] || { echo "remote backup directory is not writable: $REMOTE_BACKUP_DIR" >&2; exit 1; } | |
| # PHP-FPM owns runtime data as http:http. Repair group-read/traverse | |
| # bits before checking so files left at 0600 by older writers do | |
| # not block deploy, which is a member of the http group. | |
| sudo -n -u http find data \ | |
| -path 'data/etc/banlists/index' -prune -o \ | |
| -exec chmod g+rX {} + 2>/dev/null || true | |
| unreadable_paths="$( | |
| find data \ | |
| -path 'data/etc/banlists/index' -prune -o \ | |
| \( -type f ! -readable -o -type d \( ! -readable -o ! -executable \) \) -print 2>&1 \ | |
| | head -50 || true | |
| )" | |
| if [ -n "$unreadable_paths" ]; then | |
| echo "data contains paths the deploy user cannot archive:" >&2 | |
| echo "$unreadable_paths" >&2 | |
| echo "fix ownership/permissions on $TARGET/data, then rerun this workflow" >&2 | |
| exit 1 | |
| fi | |
| echo "Removing stale remote backup archives from $REMOTE_BACKUP_DIR" >&2 | |
| find "$REMOTE_BACKUP_DIR" -maxdepth 1 -type f \ | |
| -regextype posix-extended \ | |
| -regex ".*/[0-9]{2}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}\\.zip" \ | |
| -delete | |
| archive_name="$(date +"%d-%m-%y_%H-%M-%S").zip" | |
| archive_path="${REMOTE_BACKUP_DIR%/}/$archive_name" | |
| zip_log="$(mktemp)" | |
| trap 'rm -f "$zip_log"' EXIT | |
| echo "Creating backup archive at $archive_path" >&2 | |
| df -h "$REMOTE_BACKUP_DIR" >&2 | |
| rm -f "$archive_path" | |
| if zip -r "$archive_path" data -x 'data/etc/banlists/index/*' >"$zip_log" 2>&1; then | |
| : | |
| else | |
| status=$? | |
| echo "zip failed with exit code $status" >&2 | |
| cat "$zip_log" >&2 | |
| rm -f "$archive_path" | |
| exit "$status" | |
| fi | |
| ls -lh "$archive_path" >&2 | |
| printf "%s" "$archive_path" | |
| EOF | |
| )" | |
| archive_name="$(basename "$remote_archive_path")" | |
| echo "remote_archive_path=$remote_archive_path" >> "$GITHUB_OUTPUT" | |
| echo "archive_name=$archive_name" >> "$GITHUB_OUTPUT" | |
| - name: download backup archive | |
| run: | | |
| set -euo pipefail | |
| scp "$REMOTE:${{ steps.remote-backup.outputs.remote_archive_path }}" "${{ steps.remote-backup.outputs.archive_name }}" | |
| - name: upload backup to Google Drive | |
| run: | | |
| set -euo pipefail | |
| rclone copy \ | |
| "${{ steps.remote-backup.outputs.archive_name }}" \ | |
| "${GDRIVE_REMOTE}:" \ | |
| --drive-root-folder-id "${GDRIVE_FOLDER_ID}" | |
| - name: remove remote backup archive | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| remote_archive_path="${{ steps.remote-backup.outputs.remote_archive_path }}" | |
| [ -n "$remote_archive_path" ] || exit 0 | |
| ssh "$REMOTE" "rm -f '$remote_archive_path'" | |
| - name: keep only the 10 newest backups | |
| run: | | |
| set -euo pipefail | |
| old_files="$( | |
| rclone lsjson "${GDRIVE_REMOTE}:" \ | |
| --drive-root-folder-id "${GDRIVE_FOLDER_ID}" \ | |
| --files-only \ | |
| | jq -r 'sort_by(.ModTime) | reverse | .[10:][]?.Path' | |
| )" | |
| if [ -z "$old_files" ]; then | |
| echo "No old backups to delete." | |
| exit 0 | |
| fi | |
| while IFS= read -r file_path; do | |
| [ -n "$file_path" ] || continue | |
| rclone deletefile "${GDRIVE_REMOTE}:${file_path}" --drive-root-folder-id "${GDRIVE_FOLDER_ID}" | |
| done <<< "$old_files" | |
| - name: remove local backup archive | |
| if: always() | |
| run: | | |
| archive_name="${{ steps.remote-backup.outputs.archive_name }}" | |
| [ -n "$archive_name" ] || exit 0 | |
| rm -f "$archive_name" |