Created
September 1, 2026 15:58
-
-
Save Daij-Djan/9aeba540071de0b8da6fc4a29116070e to your computer and use it in GitHub Desktop.
dropbox-backup.sh - script to backup dropbox via rclone
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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ── dropbox-backup ─────────────────────────────────────────────────────────── | |
| # Ad-hoc, on-demand backup of the ENTIRE Dropbox account to the LaCie drive. | |
| # | |
| # Pulls straight from the Dropbox cloud over the API via rclone. It does NOT | |
| # read, touch, or depend on the local Dropbox sync folder | |
| # (~/Library/CloudStorage/Dropbox), so it can't disturb normal Dropbox sync, | |
| # and it also captures the ~4k files that are currently "online only". | |
| # | |
| # Layout created on the drive: | |
| # /Volumes/LaCie/Dropbox-Backup/current/ exact mirror of Dropbox now | |
| # /Volumes/LaCie/Dropbox-Backup/_archive/<stamp>/ files that were deleted or | |
| # replaced, kept per run | |
| # /Volumes/LaCie/Dropbox-Backup/logs/<stamp>.log full rclone log per run | |
| # | |
| # Nothing is ever permanently deleted from the drive: a file removed or changed | |
| # in Dropbox is moved into that run's _archive/ folder before the mirror is | |
| # updated. | |
| # | |
| # Usage: | |
| # dropbox-backup real run | |
| # dropbox-backup --dry-run show what would change, transfer nothing | |
| # dropbox-backup --help | |
| # Extra arguments are passed straight through to `rclone sync` | |
| # (e.g. --dry-run, --bwlimit 20M, --verbose). | |
| # ──────────────────────────────────────────────────────────────────────────── | |
| REMOTE="dropbox:" | |
| MOUNT="/Volumes/LaCie" | |
| DEST="$MOUNT/Dropbox-Backup" | |
| MIRROR="$DEST/current" | |
| STAMP="$(date +%Y-%m-%d_%H%M%S)" | |
| ARCHIVE="$DEST/_archive/$STAMP" | |
| LOGDIR="$DEST/logs" | |
| LOG="$LOGDIR/$STAMP.log" | |
| c_red=$'\033[31m'; c_grn=$'\033[32m'; c_cyn=$'\033[36m'; c_off=$'\033[0m' | |
| info() { printf '%s▸ %s%s\n' "$c_cyn" "$*" "$c_off"; } | |
| die() { printf '\n%s✗ %s%s\n' "$c_red" "$*" "$c_off" >&2; exit 1; } | |
| case "${1:-}" in | |
| -h|--help) sed -n '4,27p' "$0" | sed 's/^#\{1,\} \{0,1\}//'; exit 0 ;; | |
| esac | |
| # Create and announce the log file first, before any checks, so it exists | |
| # on disk immediately and you can start tailing it right away. | |
| mkdir -p "$LOGDIR" | |
| : > "$LOG" | |
| info "Log: $LOG" | |
| info "Tail it with: tail -f \"$LOG\"" | |
| echo | |
| # ── preflight ─────────────────────────────────────────────────────────────── | |
| command -v rclone >/dev/null || die "rclone not installed — run: brew install rclone" | |
| rclone listremotes | grep -qx "$REMOTE" \ | |
| || die "rclone remote '$REMOTE' is not set up. Run once: rclone config create dropbox dropbox" | |
| [[ -d "$MOUNT" ]] && mount | grep -q " on $MOUNT " \ | |
| || die "LaCie drive is not mounted at $MOUNT — plug it in / unlock it and retry" | |
| [[ -w "$MOUNT" ]] || die "$MOUNT is not writable" | |
| info "Checking the Dropbox remote is reachable…" | |
| rclone lsd "$REMOTE" --max-depth 1 >/dev/null \ | |
| || die "Can't reach Dropbox. Re-authorise with: rclone config reconnect $REMOTE" | |
| mkdir -p "$MIRROR" "$LOGDIR" | |
| avail_gb="$(df -g "$MOUNT" | awk 'NR==2 {print $4}')" | |
| [[ -n "${avail_gb:-}" && "$avail_gb" -lt 60 ]] \ | |
| && info "WARNING: only ${avail_gb} GB free on the LaCie" | |
| # ── run ───────────────────────────────────────────────────────────────────── | |
| progress=(--stats=30s --stats-one-line) | |
| [[ -t 1 ]] && progress=(--progress) | |
| info "Source : $REMOTE" | |
| info "Mirror : $MIRROR" | |
| info "Archive: $ARCHIVE (only created if something is removed/changed)" | |
| echo | |
| set +e | |
| rclone sync "$REMOTE" "$MIRROR" \ | |
| --backup-dir "$ARCHIVE" \ | |
| --create-empty-src-dirs \ | |
| --fast-list \ | |
| --modify-window 2s \ | |
| --transfers 8 \ | |
| --checkers 16 \ | |
| --retries 5 \ | |
| --low-level-retries 10 \ | |
| --exclude ".DS_Store" \ | |
| --exclude ".dropbox" \ | |
| --exclude ".dropbox.attr" \ | |
| --log-file "$LOG" \ | |
| --log-level INFO \ | |
| "${progress[@]}" \ | |
| "$@" | |
| rc=$? | |
| set -e | |
| # Remove the archive dir again if this run didn't move anything into it. | |
| [[ -d "$ARCHIVE" ]] && rmdir "$ARCHIVE" 2>/dev/null && \ | |
| info "Nothing was deleted or replaced this run." | |
| echo | |
| if [[ $rc -eq 0 ]]; then | |
| size="$(du -sh "$MIRROR" 2>/dev/null | cut -f1)" | |
| printf '%s✓ Backup complete.%s Mirror is %s on the LaCie.\n' "$c_grn" "$c_off" "${size:-?}" | |
| [[ -d "$ARCHIVE" ]] && printf ' Archived this run: %s (%s)\n' \ | |
| "$(du -sh "$ARCHIVE" 2>/dev/null | cut -f1)" "$ARCHIVE" | |
| else | |
| die "rclone exited with code $rc — see $LOG" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment