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.

Add interactive attic init command for S3 setup

+181 -2
+6
cli/mod.ts
··· 107 107 } 108 108 }); 109 109 110 + main.command("init", "Set up attic configuration") 111 + .action(async () => { 112 + const { runInit } = await import("./src/commands/init.ts"); 113 + await runInit(); 114 + }); 115 + 110 116 main.command("verify", "Verify backup integrity against S3") 111 117 .option("--deep", "Download and re-checksum each object") 112 118 .option("--rebuild-manifest", "Reconstruct manifest from S3 metadata")
+129
cli/src/commands/init.ts
··· 1 + import { Confirm, Input, Secret } from "@cliffy/prompt"; 2 + import { 3 + type AtticConfig, 4 + configPath, 5 + loadConfig, 6 + writeConfig, 7 + } from "../config/config.ts"; 8 + 9 + const EU_PROVIDER_EXAMPLES = [ 10 + " Scaleway (EU): https://s3.fr-par.scw.cloud", 11 + " Hetzner (EU): https://fsn1.your-objectstorage.com", 12 + " OVH (EU): https://s3.gra.io.cloud.ovh.net", 13 + " AWS: https://s3.eu-west-1.amazonaws.com", 14 + ]; 15 + 16 + export async function runInit(): Promise<void> { 17 + console.log("\n attic — iCloud Photos backup to S3-compatible storage\n"); 18 + 19 + const existing = loadConfig(); 20 + if (existing) { 21 + const overwrite = await Confirm.prompt({ 22 + message: `Config already exists at ${configPath()}. Overwrite?`, 23 + default: false, 24 + }); 25 + if (!overwrite) { 26 + console.log(" Cancelled.\n"); 27 + return; 28 + } 29 + } 30 + 31 + console.log(" S3 Connection"); 32 + console.log(" " + "─".repeat(40) + "\n"); 33 + console.log(" Provider examples:"); 34 + for (const line of EU_PROVIDER_EXAMPLES) { 35 + console.log(line); 36 + } 37 + console.log(); 38 + 39 + const endpoint = await Input.prompt({ 40 + message: "Endpoint URL", 41 + validate: (v) => { 42 + if (!v.startsWith("https://")) return "Must start with https://"; 43 + return true; 44 + }, 45 + }); 46 + 47 + const region = await Input.prompt({ 48 + message: "Region", 49 + hint: "e.g. fr-par, eu-central-1, fsn1", 50 + }); 51 + 52 + const bucket = await Input.prompt({ 53 + message: "Bucket name", 54 + }); 55 + 56 + const pathStyle = await Confirm.prompt({ 57 + message: "Use path-style URLs? (most S3-compatible providers need this)", 58 + default: true, 59 + }); 60 + 61 + console.log("\n Credentials"); 62 + console.log(" " + "─".repeat(40) + "\n"); 63 + 64 + const accessKey = await Input.prompt({ 65 + message: "Access key", 66 + }); 67 + 68 + const secretKey = await Secret.prompt({ 69 + message: "Secret key", 70 + }); 71 + 72 + const config: AtticConfig = { 73 + endpoint, 74 + region, 75 + bucket, 76 + pathStyle, 77 + keychain: { 78 + accessKeyService: "attic-s3-access-key", 79 + secretKeyService: "attic-s3-secret-key", 80 + }, 81 + }; 82 + 83 + // Write config file 84 + console.log(`\n Writing config to ${configPath()}...`); 85 + writeConfig(config); 86 + console.log(" Done."); 87 + 88 + // Store credentials in Keychain (-U flag: update if exists, create if not) 89 + console.log(" Storing credentials in macOS Keychain..."); 90 + await storeKeychainCredential( 91 + config.keychain.accessKeyService, 92 + accessKey, 93 + ); 94 + await storeKeychainCredential( 95 + config.keychain.secretKeyService, 96 + secretKey, 97 + ); 98 + console.log(" Done."); 99 + 100 + console.log( 101 + '\n Setup complete. Run "attic scan" to see your Photos library.\n', 102 + ); 103 + } 104 + 105 + async function storeKeychainCredential( 106 + service: string, 107 + value: string, 108 + ): Promise<void> { 109 + const cmd = new Deno.Command("security", { 110 + args: [ 111 + "add-generic-password", 112 + "-U", 113 + "-s", 114 + service, 115 + "-a", 116 + "attic", 117 + "-w", 118 + value, 119 + ], 120 + stderr: "piped", 121 + }); 122 + const { code, stderr } = await cmd.output(); 123 + if (code !== 0) { 124 + const err = new TextDecoder().decode(stderr); 125 + throw new Error( 126 + `Failed to store credential in Keychain for service "${service}": ${err.trim()}`, 127 + ); 128 + } 129 + }
+1
deno.json
··· 6 6 "lint": "deno lint", 7 7 "fmt": "deno fmt", 8 8 "fmt:check": "deno fmt --check", 9 + "init": "deno run --allow-read --allow-write --allow-env --allow-run=security cli/mod.ts init", 9 10 "scan": "deno run --allow-read --allow-env --allow-ffi cli/mod.ts scan", 10 11 "status": "deno run --allow-read --allow-write --allow-env --allow-ffi cli/mod.ts status", 11 12 "backup": "deno run --allow-read --allow-write --allow-env --allow-sys --allow-ffi --allow-net --allow-run cli/mod.ts backup",
+45 -2
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@cliffy/ansi@1.0.0": "1.0.0", 4 5 "jsr:@cliffy/command@1": "1.0.0", 5 6 "jsr:@cliffy/flags@1.0.0": "1.0.0", 6 7 "jsr:@cliffy/internal@1.0.0": "1.0.0", 8 + "jsr:@cliffy/keycode@1.0.0": "1.0.0", 9 + "jsr:@cliffy/prompt@1": "1.0.0", 7 10 "jsr:@cliffy/table@1.0.0": "1.0.0", 8 11 "jsr:@db/sqlite@0.12": "0.12.0", 9 12 "jsr:@denosaurs/plug@1": "1.1.0", 10 13 "jsr:@std/assert@0.217": "0.217.0", 11 14 "jsr:@std/assert@1": "1.0.18", 15 + "jsr:@std/assert@^1.0.18": "1.0.18", 12 16 "jsr:@std/encoding@1": "1.0.10", 17 + "jsr:@std/encoding@^1.0.10": "1.0.10", 13 18 "jsr:@std/fmt@1": "1.0.9", 14 19 "jsr:@std/fmt@^1.0.9": "1.0.9", 15 20 "jsr:@std/fs@1": "1.0.22", 16 21 "jsr:@std/internal@^1.0.12": "1.0.12", 22 + "jsr:@std/io@~0.225.3": "0.225.3", 17 23 "jsr:@std/path@0.217": "0.217.0", 18 24 "jsr:@std/path@1": "1.1.4", 19 25 "jsr:@std/path@^1.1.4": "1.1.4", 20 26 "jsr:@std/text@^1.0.17": "1.0.17", 21 - "npm:@aws-sdk/client-s3@3": "3.1008.0" 27 + "npm:@aws-sdk/client-s3@3": "3.1008.0", 28 + "npm:@types/node@*": "24.2.0" 22 29 }, 23 30 "jsr": { 31 + "@cliffy/ansi@1.0.0": { 32 + "integrity": "987008f74e50aa72cc1517ffccc769711734a14927bc4599e052efe1b9a840e2", 33 + "dependencies": [ 34 + "jsr:@cliffy/internal", 35 + "jsr:@std/encoding@^1.0.10", 36 + "jsr:@std/io" 37 + ] 38 + }, 24 39 "@cliffy/command@1.0.0": { 25 40 "integrity": "c52a241ea68857fcdaff4f3173eb404f8017d7bc35553b6f533c592b89dde7d2", 26 41 "dependencies": [ ··· 41 56 "@cliffy/internal@1.0.0": { 42 57 "integrity": "1e17ccbcd5420093c0a93e5b3827bbdc9abac5195bacf187edc44665e54bdde6" 43 58 }, 59 + "@cliffy/keycode@1.0.0": { 60 + "integrity": "755dbf007be110dcb5625f87eb61b362b6a0ca6835453af03ebd3b34d399cf14" 61 + }, 62 + "@cliffy/prompt@1.0.0": { 63 + "integrity": "48b4cd35199fda7832f35e1fe0a3e8bc2b1ea49ba57b4ec0e29e22db44e8ca9f", 64 + "dependencies": [ 65 + "jsr:@cliffy/ansi", 66 + "jsr:@cliffy/internal", 67 + "jsr:@cliffy/keycode", 68 + "jsr:@std/assert@^1.0.18", 69 + "jsr:@std/fmt@^1.0.9", 70 + "jsr:@std/io", 71 + "jsr:@std/path@^1.1.4", 72 + "jsr:@std/text" 73 + ] 74 + }, 44 75 "@cliffy/table@1.0.0": { 45 76 "integrity": "3fdaa9e1ef1ea62022108adabd826932bdea8dd05497079896febcd41322907f", 46 77 "dependencies": [ ··· 57 88 "@denosaurs/plug@1.1.0": { 58 89 "integrity": "eb2f0b7546c7bca2000d8b0282c54d50d91cf6d75cb26a80df25a6de8c4bc044", 59 90 "dependencies": [ 60 - "jsr:@std/encoding", 91 + "jsr:@std/encoding@1", 61 92 "jsr:@std/fmt@1", 62 93 "jsr:@std/fs", 63 94 "jsr:@std/path@1" ··· 87 118 }, 88 119 "@std/internal@1.0.12": { 89 120 "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 121 + }, 122 + "@std/io@0.225.3": { 123 + "integrity": "27b07b591384d12d7b568f39e61dff966b8230559122df1e9fd11cc068f7ddd1" 90 124 }, 91 125 "@std/path@0.217.0": { 92 126 "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", ··· 1059 1093 "tslib" 1060 1094 ] 1061 1095 }, 1096 + "@types/node@24.2.0": { 1097 + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", 1098 + "dependencies": [ 1099 + "undici-types" 1100 + ] 1101 + }, 1062 1102 "bowser@2.14.1": { 1063 1103 "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" 1064 1104 }, ··· 1084 1124 }, 1085 1125 "tslib@2.8.1": { 1086 1126 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 1127 + }, 1128 + "undici-types@7.10.0": { 1129 + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" 1087 1130 } 1088 1131 }, 1089 1132 "workspace": {