#!/usr/bin/env bash # lib/cloud.sh — cloud storage provider abstraction. # Resolves SSR_CLOUD_DIR based on SSR_CLOUD_PROVIDER from config. # Echo one directory under ~/Library/CloudStorage matching find -name "$1". # $2 = label for errors (e.g. OneDrive). $3 = optional fallback dir if zero matches. # If several folders match (e.g. personal + work OneDrive), SSR_CLOUD_PATH must # be set to the absolute path of the chosen root (same as one of the matches); # otherwise we abort with a list of candidates. Tilde in SSR_CLOUD_PATH is expanded. ssr::cloud::resolve_storage_pattern() { local pattern="$1" label="$2" zero_fallback="${3:-}" local cs="$HOME/Library/CloudStorage" local custom="${SSR_CLOUD_PATH:-}" case "$custom" in '~' | '~'/*) custom="${custom/#\~/$HOME}" ;; esac local matches=() local d if [[ -d "$cs" ]]; then while IFS= read -r d; do [[ -n "$d" && -d "$d" ]] && matches+=("$d") done < <(find "$cs" -maxdepth 1 -type d -name "$pattern" 2>/dev/null | LC_ALL=C sort -u) fi local n="${#matches[@]}" if [[ "$n" -eq 0 ]]; then if [[ -n "$zero_fallback" && -d "$zero_fallback" ]]; then printf '%s\n' "$zero_fallback" return 0 fi ssr::die "$label folder not found under ~/Library/CloudStorage" fi if [[ "$n" -eq 1 ]]; then printf '%s\n' "${matches[0]}" return 0 fi if [[ -z "$custom" ]]; then ssr::err "Multiple $label folders found; set SSR_CLOUD_PATH in ssr.conf to the root you want (absolute path), or use SSR_CLOUD_PROVIDER=custom." for d in "${matches[@]}"; do ssr::err " $d"; done exit 1 fi ssr::validate_path "$custom" [[ -d "$custom" ]] || ssr::die "SSR_CLOUD_PATH is not a directory: $custom" local abs_c abs_m picked="" abs_c="$(cd "$custom" && pwd -P)" || ssr::die "SSR_CLOUD_PATH not accessible: $custom" for d in "${matches[@]}"; do abs_m="$(cd "$d" && pwd -P)" || continue if [[ "$abs_m" == "$abs_c" ]]; then picked="$d" break fi done if [[ -z "$picked" ]]; then ssr::err "SSR_CLOUD_PATH=$custom is not one of the detected $label roots:" for d in "${matches[@]}"; do ssr::err " $d"; done exit 1 fi printf '%s\n' "$picked" } ssr::cloud::resolve_dir() { local provider="${SSR_CLOUD_PROVIDER:-icloud}" custom="${SSR_CLOUD_PATH:-}" case "$provider" in icloud) printf '%s\n' "$HOME/Library/Mobile Documents/com~apple~CloudDocs" ;; dropbox) if [[ ! -d "$HOME/Library/CloudStorage" ]]; then [[ -d "$HOME/Dropbox" ]] || ssr::die "Dropbox not found (no ~/Library/CloudStorage and no ~/Dropbox)." printf '%s\n' "$HOME/Dropbox" return 0 fi ssr::cloud::resolve_storage_pattern 'Dropbox*' 'Dropbox' "$HOME/Dropbox" ;; googledrive) ssr::cloud::resolve_storage_pattern 'GoogleDrive-*' 'Google Drive' '' ;; onedrive) ssr::cloud::resolve_storage_pattern 'OneDrive-*' 'OneDrive' '' ;; custom) [[ -n "$custom" ]] || ssr::die "SSR_CLOUD_PROVIDER=custom requires SSR_CLOUD_PATH" case "$custom" in '~' | '~'/*) custom="${custom/#\~/$HOME}" ;; esac ssr::validate_path "$custom" printf '%s\n' "$custom" ;; *) ssr::die "Unknown SSR_CLOUD_PROVIDER: $provider (use: icloud|dropbox|googledrive|onedrive|custom)" ;; esac } # Returns the per-host backup directory under the cloud root. ssr::cloud::backup_dir() { local cloud host uuid cloud="$(ssr::cloud::resolve_dir)" host="$(hostname -s)" uuid="$(/usr/sbin/system_profiler SPHardwareDataType 2>/dev/null \ | awk '/Hardware UUID/ { print $3 }')" [[ -n "$uuid" ]] || uuid="no-uuid" local subdir="${SSR_BACKUP_SUBDIR:-BACKUP}" printf '%s/%s/%s - %s\n' "$cloud" "$subdir" "$host" "$uuid" } # Parent directory for Mackup file_system storage: same cloud subfolder as # per-host backups (SSR_BACKUP_SUBDIR). ~/.mackup.cfg sets path= and # directory=mackup — Mackup places files under /mackup/ (not SSR-controlled). ssr::cloud::mackup_dir() { local cloud subdir cloud="$(ssr::cloud::resolve_dir)" subdir="${SSR_BACKUP_SUBDIR:-BACKUP}" printf '%s/%s\n' "$cloud" "$subdir" } ssr::cloud::ensure_available() { local cloud cloud="$(ssr::cloud::resolve_dir)" [[ -d "$cloud" ]] || ssr::die "Cloud directory not available: $cloud" ssr::ok "Cloud provider: ${SSR_CLOUD_PROVIDER:-icloud} → $cloud" }