#!/bin/bash # # A script to backup a single-user MacBook using restic to either a local # filesystem or Backblaze B2. set -o errexit set -o nounset set -o pipefail # --- Configuration --- # Directory to be backed up. # For this script, we assume the user's home directory. SOURCE_DIR="${HOME}" # Exclude file location. EXCLUDE_FILE="${HOME}/.restic_exclude.darwin" # --- Functions --- usage() { cat << EOF Usage: $(basename "$0") [-l /path/to/repo | -b [bucket]] [-u UPLOAD_LIMIT] A script to backup a single-user MacBook using restic. Options: -l Backup to a local filesystem repository at the given path. -b [bucket] Backup to a Backblaze B2 bucket. The bucket name is optional. If not provided, it will be read from the B2_BUCKET_NAME environment variable. -u Limit the upload speed to the given value in KB/s. -h Show this help message. EOF } # --- Main Script --- # Check if restic is installed if ! command -v restic >/dev/null 2>&1; then echo "Error: restic command not found." >&2 echo "Please install restic first: https://restic.net/" >&2 exit 1 fi if [[ $# -eq 0 ]]; then usage exit 1 fi BACKUP_MODE="" REPO="" UPLOAD_LIMIT="" while getopts ":l:bu:h" opt; do case ${opt} in l) BACKUP_MODE="local" REPO="${OPTARG}" ;; b) BACKUP_MODE="b2" if [[ ${OPTIND} -le $# && "${!OPTIND:-}" != -* ]]; then REPO="b2:${!OPTIND}:" OPTIND=$((OPTIND + 1)) fi ;; u) UPLOAD_LIMIT="${OPTARG}" ;; h) usage exit 0 ;; \?) echo "Invalid option: -${OPTARG}" >&2 usage exit 1 ;; :) echo "Option -${OPTARG} requires an argument." >&2 usage exit 1 ;; esac done # --- Pre-run checks --- if [[ -z "${BACKUP_MODE}" ]]; then echo "Error: You must specify a backup mode (-l or -b)." >&2 usage exit 1 fi if [[ "${BACKUP_MODE}" == "b2" ]]; then if [[ -f "${HOME}/.resticb2" ]] ; then # shellcheck disable=SC1090 . "${HOME}/.resticb2" fi export B2_ACCOUNT_ID="${B2_ACCOUNT_ID:-}" export B2_ACCOUNT_KEY="${B2_ACCOUNT_KEY:-}" export B2_BUCKET_NAME="${B2_BUCKET_NAME:-}" if [[ -z "${B2_ACCOUNT_ID}" || -z "${B2_ACCOUNT_KEY}" ]]; then echo "Error: For Backblaze B2 backups, you must set the B2_ACCOUNT_ID and B2_ACCOUNT_KEY environment variables." >&2 exit 1 fi if [[ -z "${REPO}" ]]; then if [[ -n "${B2_BUCKET_NAME}" ]]; then REPO="b2:${B2_BUCKET_NAME}:" else echo "Error: Backup mode is B2 but no bucket name was provided and the B2_BUCKET_NAME environment variable is not set." >&2 usage exit 1 fi fi fi KEYCHAIN_ENTRY_NAME="restic_repo_password" local_user="$(id -un 2>/dev/null || whoami)" if security find-generic-password -a "${local_user}" -s "${KEYCHAIN_ENTRY_NAME}" >/dev/null 2>&1 ; then printf -v RESTIC_PASSWORD_COMMAND 'security find-generic-password -a %q -s %q -w' "${local_user}" "${KEYCHAIN_ENTRY_NAME}" export RESTIC_PASSWORD_COMMAND # Source file? elif [[ -f "${HOME}/.resticpass" ]] ; then export RESTIC_PASSWORD_FILE="${HOME}/.resticpass" fi # Ensure exclude file exists so restic does not fail when checking exclusions. if [[ ! -f "${EXCLUDE_FILE}" ]]; then echo "Exclude file '${EXCLUDE_FILE}' not found. Creating a default macOS exclude file..." cat << 'EOF' > "${EXCLUDE_FILE}" # Default restic exclusions for macOS .Trash Library/Caches Library/Logs .CFUserTextEncoding EOF fi # If the repository does not exist, initialize it. # The user will be prompted for a password, which will be required for all # future interactions with the repository. if ! restic -r "${REPO}" snapshots >/dev/null 2>&1; then echo "Restic repository not found or not accessible. Initializing..." restic init -r "${REPO}" fi # --- Run Backup --- echo "Starting restic backup..." echo "Source: ${SOURCE_DIR}" echo "Repository: ${REPO}" BACKUP_ARGS=( "restic" "backup" "--verbose" "--repo" "${REPO}" "--exclude-file" "${EXCLUDE_FILE}" "--one-file-system" "--tag" "macbook-backup" ) if [[ -n "${UPLOAD_LIMIT}" ]]; then BACKUP_ARGS+=("--limit-upload" "${UPLOAD_LIMIT}") fi BACKUP_ARGS+=("${SOURCE_DIR}") "${BACKUP_ARGS[@]}" echo "Backup complete." # --- Prune old snapshots (optional, but recommended) --- # Keeps the last 7 daily, 4 weekly, and 6 monthly snapshots. echo "Pruning old snapshots..." restic forget \ --repo "${REPO}" \ --keep-daily 7 \ --keep-weekly 4 \ --keep-monthly 6 \ --prune echo "Pruning complete." echo "Restic backup script finished."