Keep using Photos.app like you always do. Attic quietly backs up your originals and edits to an S3 bucket you control. One-way, append-only.
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve backup progress output and CLI help text

+36 -17
+8 -4
README.md
··· 54 54 is saved to `~/.attic/config.json` and credentials are stored in the macOS 55 55 Keychain. 56 56 57 - Build the ladder binary (see [ladder](https://github.com/tijs/ladder) for 58 - details): 57 + Build the ladder binary and add it to your PATH (see 58 + [ladder](https://github.com/tijs/ladder) for details): 59 59 60 60 ```bash 61 61 git clone https://github.com/tijs/ladder.git 62 62 cd ladder 63 63 swift build -c release 64 + sudo cp .build/release/ladder /usr/local/bin/ 64 65 ``` 66 + 67 + Alternatively, pass `--ladder <path>` to the backup command or set the 68 + `LADDER_PATH` environment variable. 65 69 66 70 ## Commands 67 71 ··· 102 106 | Flag | Description | 103 107 | --------------------- | -------------------------------------------------------- | 104 108 | `--dry-run` | Show what would be uploaded without uploading | 105 - | `--limit N` | Back up at most N assets | 106 - | `--batch-size N` | Assets per ladder export batch (default: 50) | 109 + | `--limit N` | Stop after N assets (useful for test runs) | 110 + | `--batch-size N` | Assets per export batch (default: 50) | 107 111 | `--type photo\|video` | Only back up photos or videos | 108 112 | `--bucket NAME` | Override bucket from config | 109 113 | `--ladder PATH` | Path to the ladder binary (or set `LADDER_PATH` env var) |
attic-logo-transparent.png

This is a binary file and will not be displayed.

+2 -2
cli/mod.ts
··· 54 54 55 55 main.command("backup", "Back up pending assets to S3") 56 56 .option("--dry-run", "Show what would be uploaded") 57 - .option("--limit <n:integer>", "Back up at most N assets") 58 - .option("--batch-size <n:integer>", "Assets per ladder batch", { 57 + .option("--limit <n:integer>", "Stop after N assets (useful for test runs)") 58 + .option("--batch-size <n:integer>", "Assets per export batch", { 59 59 default: 50, 60 60 }) 61 61 .type("asset-type", assetType)
+26 -11
cli/src/commands/backup.ts
··· 79 79 0, 80 80 ); 81 81 82 + const photoCount = pending.filter((a) => a.kind === AssetKind.PHOTO).length; 83 + const videoCount = pending.filter((a) => a.kind === AssetKind.VIDEO).length; 84 + const typeSummary = [ 85 + photoCount > 0 ? `${photoCount} photos` : "", 86 + videoCount > 0 ? `${videoCount} videos` : "", 87 + ].filter(Boolean).join(", "); 88 + 82 89 console.log(`\n Attic — Backup`); 83 90 console.log(` ══════════════\n`); 84 - console.log(` Pending: ${pending.length.toLocaleString()} assets`); 91 + console.log(` Pending: ${pending.length.toLocaleString()} assets (${typeSummary})`); 85 92 console.log(` Est. size: ${formatBytes(pendingSize)}`); 86 93 if (options.dryRun) console.log(` Mode: DRY RUN`); 87 94 console.log(); ··· 124 131 const batchNum = Math.floor(i / options.batchSize) + 1; 125 132 const totalBatches = Math.ceil(pending.length / options.batchSize); 126 133 127 - console.log( 128 - ` Batch ${batchNum}/${totalBatches} (${batch.length} assets)`, 129 - ); 134 + if (totalBatches > 1) { 135 + console.log( 136 + ` Batch ${batchNum}/${totalBatches} (${batch.length} assets)`, 137 + ); 138 + } 130 139 131 140 // 1. Export via ladder 132 141 let batchResult; ··· 189 198 report.uploaded++; 190 199 report.totalBytes += exported.size; 191 200 201 + // Per-asset progress 202 + const done = report.uploaded + report.failed; 203 + const pct = ((done / pending.length) * 100).toFixed(0); 204 + const name = asset.originalFilename ?? asset.filename ?? "unknown"; 205 + const typeLabel = asset.kind === AssetKind.PHOTO ? "photo" : "video"; 206 + console.log( 207 + ` [${done}/${pending.length}] ${pct}% ${name} (${typeLabel}, ${formatBytes(exported.size)})`, 208 + ); 209 + 192 210 // Save manifest periodically (checked per-asset, not per-batch) 193 211 if (sinceLastSave >= options.saveInterval) { 194 212 await manifestStore.save(manifest); ··· 209 227 } 210 228 } 211 229 212 - // Progress 213 - const done = report.uploaded + report.failed; 214 - const pct = ((done / pending.length) * 100).toFixed(1); 215 - console.log( 216 - ` Progress: ${done}/${pending.length} (${pct}%) ` + 217 - `Uploaded: ${formatBytes(report.totalBytes)}`, 218 - ); 230 + // Batch separator when multiple batches 231 + if (totalBatches > 1 && batchNum < totalBatches) { 232 + console.log(); 233 + } 219 234 } 220 235 221 236 // Final save