Add documentation for backup structure, commands, configuration, and usage

Oliver Surke
2026-05-13 20:11:22 +02:00
parent 76b6b9cc44
commit ea2a002038
9 changed files with 360 additions and 1 deletions
+91
@@ -0,0 +1,91 @@
# Backup structure and sync behavior
## Cloud directory layout
After `ssr sync`, the default layout under your cloud root is:
```text
<cloud root>/
├── BACKUP/ # default SSR_BACKUP_SUBDIR
│ ├── mackup/ # Mackup copy destination (~/.mackup.cfg directory=mackup)
│ └── <hostname> - <hardware-uuid>/
│ ├── Brewfile
│ ├── Brewfile.bak
│ ├── unmanaged-apps.md # apps not from brew / MAS
│ └── macos-prefs/
│ ├── com.apple.dock.plist
│ ├── com.apple.finder.plist
│ └── …
```
### Mackup path change (upgrades)
Older releases used a separate `Mackup/` folder under the cloud root. Current SSR keeps Mackup under the same `SSR_BACKUP_SUBDIR` tree (`…/mackup/`). After upgrading, run `ssr sync` to repopulate the new location, or move your existing `mackup/` tree manually if you need history without a full re-upload.
## Unmanaged applications
During each sync, SSR compares every `.app` in `/Applications` and `~/Applications` (configurable via `SSR_APP_DIRS`) against:
1. **Mac App Store** — bundle contains `Contents/_MASReceipt/receipt`
2. **Homebrew Cask** — bundle filename matches a casks `.app` artifact
Unmatched apps are written to `unmanaged-apps.md` (Markdown table: version, bundle id, best-effort installer link) and logged as warnings. Disable with `SSR_SCAN_UNMANAGED_APPS=false`.
**Link selection order** for each app:
1. **Sparkle feed**`SUFeedURL` in `Info.plist` → vendor homepage
2. **Known vendor map** — bundle-id prefix → official download (Adobe, Microsoft, Docker, Cursor, Anthropic, JetBrains, GitKraken, Signal, Zoom, …)
3. **Web search** — DuckDuckGo: `<App name> macOS download`
## macOS preferences snapshot
`ssr sync` snapshots a curated set of preference domains to `<backup_dir>/macos-prefs/` (one plist per domain). On `ssr restore`, these are replayed **after** `config/macos-defaults.sh`, so the snapshot is the last writer.
### Default domains
| Domain | What it covers |
|--------|------------------|
| `com.apple.dock` | Dock position, apps, hot corners |
| `com.apple.finder` | View options, sidebar, path bar |
| `com.apple.symbolichotkeys` | Global shortcuts (Spotlight, Mission Control, screenshots, …) |
| `com.apple.HIToolbox` | Input sources / keyboard layouts |
| `com.apple.spaces` | Spaces / Mission Control |
| `com.apple.menuextra.clock` | Menu bar clock |
| `NSGlobalDomain` | Global UI, key repeat, sound, language |
### Extend domains
In `~/.config/ssr/ssr.conf`:
```bash
SSR_MACOS_PREF_DOMAINS_EXTRA="com.apple.universalaccess com.apple.Spotlight"
```
### Conflict resolution on restore
When a snapshot domain overlaps domains that `config/macos-defaults.sh` also sets, `ssr restore` warns and prompts:
```text
[WARN] Pref-snapshot overlaps with declarative defaults for 2 domain(s):
com.apple.dock, com.apple.finder
Domain `com.apple.dock`:
Snapshot from last sync (2026-05-11T17:30:00+0200) will OVERRIDE values
set by macos-defaults.sh.
[A]ccept snapshot / [r]eject (keep declarative)? [A/r]:
```
- Default: **Accept** (Enter).
- `r`: keep declarative values for that domain.
- Non-interactive (launchd, CI): `SSR_PREFS_CONFLICT_DEFAULT` (`accept` or `reject`).
- `SSR_ASSUME_YES=true`: skip all such prompts.
**Mackup overlap:** if Mackup also syncs `com.apple.dock` (or similar), two mechanisms compete — pick one.
### Restore from another machines Brewfile
```bash
ssr restore "$HOME/Library/Mobile Documents/com~apple~CloudDocs/BACKUP/oldhost - ABCD-1234/Brewfile"
```
(Adjust path for your cloud provider — see [Configuration-reference](https://git.surke.de/osurke/system-sync-restore/wiki/Configuration-reference).)
+17
@@ -0,0 +1,17 @@
# Commands
```text
ssr install First-time install: link binary, seed config, schedule sync
ssr sync One-shot backup → cloud
ssr restore [Brewfile] Restore machine; Brewfile defaults to cloud copy
ssr update [--dry-run] Pull latest ssr code, refresh launchd if needed
ssr schedule on|off Enable/disable daily launchd job
ssr config show|edit Show or edit the active config
ssr completions install Install zsh/bash/fish tab-completion
ssr status Config, cloud target, last sync, schedule state
ssr version Print version
```
### `ssr mackup` (legacy)
If an older install used `mackup link`, run `ssr mackup unlink` once before relying on copy-based sync.
+46
@@ -0,0 +1,46 @@
# Configuration reference
Config file: `~/.config/ssr/ssr.conf` (override with `--config` or `$SSR_CONFIG`).
The file is **sourced by bash** but only `KEY=VALUE` lines (and `#` comments) are accepted; other content causes `ssr` to abort.
If `SSR_CLOUD_PROVIDER=` appears more than once with **different** values, interactive commands prompt once and rewrite the file to a single line (duplicate same values are merged silently).
## Keys
| Key | Default | Purpose |
|-----|---------|---------|
| `SSR_CLOUD_PROVIDER` | `icloud` | `icloud` \| `dropbox` \| `googledrive` \| `onedrive` \| `custom` |
| `SSR_CLOUD_PATH` | — | Absolute path; only when provider is `custom` |
| `SSR_BACKUP_SUBDIR` | `BACKUP` | Cloud subfolder for per-host backups and Mackup `path=` (`directory=` in `~/.mackup.cfg` names the `mackup/` subfolder) |
| `SSR_ALLOW_CONF_OVERWRITE` | _(unset)_ | `yes` to apply a changed `~/.mackup.cfg` without a TTY (after reviewing logged diff); needed for launchd when cloud path changes |
| `SSR_BREW_UPGRADE` | `true` | Run `brew upgrade` during sync |
| `SSR_BREW_CLEANUP` | `true` | Run `brew cleanup` after sync |
| `SSR_SCAN_UNMANAGED_APPS` | `true` | Scan for `.app` bundles not from brew/MAS |
| `SSR_APP_DIRS` | `/Applications:~/Applications` | Colon-separated dirs for unmanaged scan |
| `SSR_SNAPSHOT_MACOS_PREFS` | `true` | Snapshot macOS pref domains during sync |
| `SSR_MACOS_PREF_DOMAINS` | _(built-in set)_ | Full override of snapshotted domains (space-separated) |
| `SSR_MACOS_PREF_DOMAINS_EXTRA` | — | Extra domains appended to defaults |
| `SSR_PREFS_CONFLICT_DEFAULT` | `accept` | Non-TTY pref conflict: `accept` or `reject` |
| `SSR_ASSUME_YES` | `false` | Auto-accept pref-snapshot conflicts on restore |
| `SSR_DISABLE_GATEKEEPER` | `false` | Run `spctl --global-disable` during restore (security trade-off) |
| `SSR_OPEN_CASKS` | `false` | Open each installed cask after restore |
| `SSR_MACOS_DEFAULTS` | repo `config/macos-defaults.sh` | Path to your own defaults script |
| `SSR_LAUNCHD_LABEL` | `de.surke.ssr.sync` | launchd job label |
| `SSR_SCHEDULE_HOUR` | `9` | Daily hour (023, local) |
| `SSR_SCHEDULE_MINUTE` | `0` | Daily minute |
| `SSR_PREFIX` | `$HOME/.local/bin` | Symlink install prefix for `ssr` |
| `SSR_STATE_DIR` | `$HOME/.local/state/ssr` | Internal state |
| `SSR_LOG_DIR` | `$HOME/.local/state/ssr/logs` | launchd logs |
## Cloud provider roots
| Provider | Resolved root |
|----------|----------------|
| `icloud` | `~/Library/Mobile Documents/com~apple~CloudDocs` |
| `dropbox` | `~/Library/CloudStorage/Dropbox*` (fallback `~/Dropbox`) |
| `googledrive` | `~/Library/CloudStorage/GoogleDrive-*` |
| `onedrive` | `~/Library/CloudStorage/OneDrive-*` |
| `custom` | `SSR_CLOUD_PATH` (must be set) |
See `config/ssr.conf.example` in the repo for the full annotated template.
+27 -1
@@ -1 +1,27 @@
Willkommen im Wiki. # System Sync & Restore (ssr) — Wiki
Reproducible macOS setup: snapshot Homebrew + app configs to your cloud drive, restore on a new machine with one workflow.
**Repository:** [system-sync-restore](https://git.surke.de/osurke/system-sync-restore) · **Issues & PRs:** use the main repo, not this wiki clone.
## Documentation map
| Topic | Page |
|--------|------|
| Requirements, install, first steps | [Quick-start](https://git.surke.de/osurke/system-sync-restore/wiki/Quick-start) |
| CLI commands | [Commands](https://git.surke.de/osurke/system-sync-restore/wiki/Commands) |
| Cloud layout, Mackup, unmanaged apps, macOS pref snapshots | [Backup-structure-and-sync](https://git.surke.de/osurke/system-sync-restore/wiki/Backup-structure-and-sync) |
| `ssr.conf` keys and cloud providers | [Configuration-reference](https://git.surke.de/osurke/system-sync-restore/wiki/Configuration-reference) |
| launchd schedule, logs, `ssr update` | [Scheduling-logging-and-updates](https://git.surke.de/osurke/system-sync-restore/wiki/Scheduling-logging-and-updates) |
| zsh / bash / fish completions | [Shell-completions](https://git.surke.de/osurke/system-sync-restore/wiki/Shell-completions) |
| Security model, uninstall, legacy script migration | [Security-Uninstall-and-Migration](https://git.surke.de/osurke/system-sync-restore/wiki/Security-Uninstall-and-Migration) |
| Repo directories and scripts | [Repository-layout](https://git.surke.de/osurke/system-sync-restore/wiki/Repository-layout) |
## At a glance
- **Backup:** `ssr sync` — Brewfile + Mackup copy to cloud (no symlinks by default).
- **Restore:** `ssr restore` — Homebrew, Brewfile, Mackup, macOS defaults + pref snapshots.
- **Schedule:** `ssr schedule on` — daily `launchd` agent (configurable hour).
- **No secrets in git:** state lives on your cloud drive; repo is code + templates only.
Successor to `system_sync.sh` / `system_restore.sh`: same intent, modular and idempotent tooling.
+43
@@ -0,0 +1,43 @@
# Quick start
## Requirements
- macOS (Apple Silicon or Intel)
- Bash 3.2+ (preinstalled)
- Network access for the initial Homebrew install
- A cloud folder you control (iCloud Drive is the default)
## Install
```bash
git clone https://git.surke.de/osurke/system-sync-restore.git ~/.ssr
cd ~/.ssr
./install.sh
```
`install.sh` will:
1. Symlink `bin/ssr` into `~/.local/bin` (override with `SSR_PREFIX`).
2. Seed `~/.config/ssr/ssr.conf` from `config/ssr.conf.example` (`chmod 600`).
3. Optionally enable the scheduled daily sync.
If `~/.local/bin` is not on your `PATH`:
```bash
export PATH="$HOME/.local/bin:$PATH"
```
## First commands
```bash
ssr sync # one-shot backup to cloud
ssr status # config, cloud target, last sync, schedule
```
Restore on a **new** machine after clone + `./install.sh`:
```bash
ssr restore
```
Default Brewfile location: `<cloud>/BACKUP/<hostname> - <hardware-uuid>/Brewfile`. To use another hosts snapshot, pass the Brewfile path — see [Backup-structure-and-sync](https://git.surke.de/osurke/system-sync-restore/wiki/Backup-structure-and-sync).
+35
@@ -0,0 +1,35 @@
# Repository layout
```text
.
├── install.sh # Wrapper → bin/ssr-install
├── bin/
│ ├── ssr # CLI dispatcher
│ ├── ssr-install
│ ├── ssr-update
│ ├── ssr-sync
│ ├── ssr-restore
│ ├── ssr-schedule
│ ├── ssr-config
│ ├── ssr-completions
│ └── ssr-status
├── lib/
│ ├── common.sh # Logging, validation, config load
│ ├── cloud.sh
│ ├── brew.sh
│ ├── mackup.sh
│ ├── apps.sh # Unmanaged .app detection
│ ├── macos.sh
│ └── macos-prefs.sh
├── config/
│ ├── ssr.conf.example
│ └── macos-defaults.sh
├── completions/ # _ssr, ssr.bash, ssr.fish
├── launchd/
│ └── de.surke.ssr.sync.plist
├── VERSION
├── LICENSE
└── README.md
```
Runtime needs: macOS and `curl` (Homebrew bootstrap). Logic lives in the repo; state on disk and cloud per your config.
+48
@@ -0,0 +1,48 @@
# Scheduling, logging, and updates
## launchd schedule
```bash
ssr schedule on # write & load ~/Library/LaunchAgents/<label>.plist
ssr schedule off # unload & remove
ssr schedule status # check state
```
Default label: `de.surke.ssr.sync` (`SSR_LAUNCHD_LABEL`). Hour/minute: `SSR_SCHEDULE_HOUR`, `SSR_SCHEDULE_MINUTE`.
## Logs
Logs: `$SSR_LOG_DIR/sync.out.log` and `sync.err.log`.
```bash
tail -f ~/.local/state/ssr/logs/sync.err.log
```
launchd-level inspection:
```bash
launchctl list | grep ssr
log show --predicate 'process == "ssr"' --last 1h
```
## Updating (three layers)
### 1. Tool code (this repository)
```bash
ssr update # git pull --ff-only, then refresh side-effects
ssr update --dry-run # show what would change
```
- Refuses dirty working tree or detached HEAD.
- Fast-forward only.
- Re-renders and reloads launchd plist if `launchd/` or `bin/ssr*` changed **and** schedule is on.
- Prints new config keys when `config/ssr.conf.example` gains options.
### 2. Backup data (cloud)
Refreshed by scheduled `ssr sync`. Previous Brewfile kept as `Brewfile.bak`; deeper history = your cloud providers versioning.
### 3. Homebrew / Mackup
`ssr sync` runs `brew upgrade` / `brew cleanup` when `SSR_BREW_UPGRADE` / `SSR_BREW_CLEANUP` are `true` (defaults). Set to `false` in `ssr.conf` to snapshot without auto-upgrading packages.
+29
@@ -0,0 +1,29 @@
# Security, uninstall, and legacy migration
## Security
- No secrets in the git repository; cloud data stays on your drive / account.
- Config is **validated** on load: only `#` comments and `KEY=VALUE`; foreign lines abort execution.
- `chmod 600` on `ssr.conf` and the rendered launchd plist.
- `SSR_DISABLE_GATEKEEPER=true` is opt-in; default is off.
- No `curl … | bash` except Homebrews own bootstrap during install.
## Uninstall
```bash
ssr schedule off
rm -f ~/.local/bin/ssr
rm -rf ~/.config/ssr ~/.local/state/ssr
```
Cloud backup data is **not** removed.
## Migration from `system_sync.sh` / `system_restore.sh`
| Legacy | New |
|--------|-----|
| `system_sync.sh` | `ssr sync` |
| `system_restore.sh <Brewfile>` | `ssr restore <Brewfile>` |
| Manual cron / Lingon | `ssr schedule on` |
| Hard-coded iCloud path | `SSR_CLOUD_PROVIDER` in `ssr.conf` |
| Inline `.macos` block | `config/macos-defaults.sh` (override with `SSR_MACOS_DEFAULTS`) |
+24
@@ -0,0 +1,24 @@
# Shell completions
`ssr install` can install completions interactively. Manual install:
```bash
ssr completions install # uses $SHELL
ssr completions install --shell all # zsh + bash + fish
ssr completions status
ssr completions uninstall --shell all
```
## Install locations (user-scoped, no sudo)
| Shell | Path |
|-------|------|
| zsh | `~/.zsh/completions/_ssr` |
| bash | `~/.local/share/bash-completion/completions/ssr` |
| fish | `~/.config/fish/completions/ssr.fish` |
- **zsh:** ensure `~/.zsh/completions` is on `$fpath` (installer prints `~/.zshrc` snippet if missing).
- **bash:** requires [bash-completion@2](https://github.com/scop/bash-completion) sourced from your rc; installer prints Homebrew path hints.
- **fish:** no extra setup.
Open a new shell or `exec $SHELL`, then `ssr <TAB>` lists subcommands.