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.

Genericize S3 client: remove Scaleway constants, read from config

+47 -26
+26 -12
cli/mod.ts
··· 1 - const DEFAULT_BUCKET = "photo-cloud-storage"; 1 + import { requireConfig } from "./src/config/config.ts"; 2 2 3 3 const command = Deno.args[0]; 4 4 ··· 43 43 const { createLadderExporter } = await import("./src/export/exporter.ts"); 44 44 45 45 const flags = parseBackupFlags(Deno.args.slice(1)); 46 + const config = requireConfig(); 46 47 const reader = openPhotosDb(flags.dbPath); 47 48 try { 48 49 const assets = reader.readAssets(); 49 50 const manifestStore = createManifestStore(); 50 51 const manifest = await manifestStore.load(); 51 52 52 - const credentials = await loadKeychainCredentials(); 53 + const credentials = await loadKeychainCredentials( 54 + config.keychain.accessKeyService, 55 + config.keychain.secretKeyService, 56 + ); 53 57 const s3 = createS3Provider( 54 58 credentials, 55 - flags.bucket ?? DEFAULT_BUCKET, 59 + flags.bucket ?? config.bucket, 60 + { 61 + endpoint: config.endpoint, 62 + region: config.region, 63 + pathStyle: config.pathStyle, 64 + }, 56 65 ); 57 66 58 67 const ladderPath = flags.ladderPath ?? ··· 80 89 ); 81 90 82 91 const verifyFlags = parseVerifyFlags(Deno.args.slice(1)); 92 + const config = requireConfig(); 83 93 84 - const credentials = await loadKeychainCredentials(); 94 + const credentials = await loadKeychainCredentials( 95 + config.keychain.accessKeyService, 96 + config.keychain.secretKeyService, 97 + ); 85 98 const s3 = createS3Provider( 86 99 credentials, 87 - verifyFlags.bucket ?? DEFAULT_BUCKET, 100 + verifyFlags.bucket ?? config.bucket, 101 + { 102 + endpoint: config.endpoint, 103 + region: config.region, 104 + pathStyle: config.pathStyle, 105 + }, 88 106 ); 89 107 const manifestStore = createManifestStore(); 90 108 ··· 99 117 break; 100 118 } 101 119 default: 102 - console.log(`attic — iCloud Photos backup to Scaleway S3\n`); 120 + console.log(`attic — iCloud Photos backup to S3-compatible storage\n`); 103 121 console.log(`Commands:`); 104 122 console.log(` scan Scan Photos library and show statistics`); 105 123 console.log(` status Compare Photos DB vs backup manifest`); ··· 110 128 console.log(` --limit N Back up at most N assets`); 111 129 console.log(` --batch-size N Assets per ladder batch (default: 50)`); 112 130 console.log(` --type photo|video Only back up photos or videos`); 113 - console.log( 114 - ` --bucket NAME S3 bucket (default: ${DEFAULT_BUCKET})`, 115 - ); 131 + console.log(` --bucket NAME S3 bucket (overrides config)`); 116 132 console.log(` --ladder PATH Path to ladder binary`); 117 133 console.log(` --db PATH Path to Photos.sqlite`); 118 134 console.log(`\nVerify flags:`); 119 135 console.log(` --deep Download and re-checksum each object`); 120 136 console.log(` --rebuild-manifest Reconstruct manifest from S3 metadata`); 121 - console.log( 122 - ` --bucket NAME S3 bucket (default: ${DEFAULT_BUCKET})`, 123 - ); 137 + console.log(` --bucket NAME S3 bucket (overrides config)`); 124 138 console.log(`\nUsage: deno task <command>`); 125 139 if (command) { 126 140 console.error(`\nUnknown command: ${command}`);
+19 -12
cli/src/storage/s3-client.ts
··· 29 29 listObjects(prefix: string): AsyncIterable<S3Object>; 30 30 } 31 31 32 - export interface ScalewayCredentials { 32 + export interface S3Credentials { 33 33 accessKeyId: string; 34 34 secretAccessKey: string; 35 35 } 36 36 37 - /** Read Scaleway S3 credentials from macOS Keychain. */ 38 - export async function loadKeychainCredentials(): Promise<ScalewayCredentials> { 39 - const accessKeyId = await keychainGet("attic-s3-access-key"); 40 - const secretAccessKey = await keychainGet("attic-s3-secret-key"); 37 + export interface S3ConnectionConfig { 38 + endpoint: string; 39 + region: string; 40 + pathStyle: boolean; 41 + } 42 + 43 + /** Read S3 credentials from macOS Keychain. */ 44 + export async function loadKeychainCredentials( 45 + accessKeyService = "attic-s3-access-key", 46 + secretKeyService = "attic-s3-secret-key", 47 + ): Promise<S3Credentials> { 48 + const accessKeyId = await keychainGet(accessKeyService); 49 + const secretAccessKey = await keychainGet(secretKeyService); 41 50 return { accessKeyId, secretAccessKey }; 42 51 } 43 52 ··· 63 72 return new TextDecoder().decode(stdout).trim(); 64 73 } 65 74 66 - const SCALEWAY_ENDPOINT = "https://s3.fr-par.scw.cloud"; 67 - const SCALEWAY_REGION = "fr-par"; 68 - 69 75 export function createS3Provider( 70 - credentials: ScalewayCredentials, 76 + credentials: S3Credentials, 71 77 bucket: string, 78 + connection: S3ConnectionConfig, 72 79 ): S3Provider { 73 80 const client = new S3Client({ 74 - endpoint: SCALEWAY_ENDPOINT, 75 - region: SCALEWAY_REGION, 81 + endpoint: connection.endpoint, 82 + region: connection.region, 76 83 credentials: { 77 84 accessKeyId: credentials.accessKeyId, 78 85 secretAccessKey: credentials.secretAccessKey, 79 86 }, 80 - forcePathStyle: true, 87 + forcePathStyle: connection.pathStyle, 81 88 }); 82 89 83 90 return {
+2 -2
deno.json
··· 8 8 "fmt:check": "deno fmt --check", 9 9 "scan": "deno run --allow-read --allow-env --allow-ffi cli/mod.ts scan", 10 10 "status": "deno run --allow-read --allow-write --allow-env --allow-ffi cli/mod.ts status", 11 - "backup": "deno run --allow-read --allow-write --allow-env --allow-sys --allow-ffi --allow-net=s3.fr-par.scw.cloud --allow-run cli/mod.ts backup", 12 - "verify": "deno run --allow-read --allow-write --allow-env --allow-sys --allow-net=s3.fr-par.scw.cloud --allow-run=security cli/mod.ts verify" 11 + "backup": "deno run --allow-read --allow-write --allow-env --allow-sys --allow-ffi --allow-net --allow-run cli/mod.ts backup", 12 + "verify": "deno run --allow-read --allow-write --allow-env --allow-sys --allow-net --allow-run=security cli/mod.ts verify" 13 13 } 14 14 }