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.

Fix process hanging after backup by destroying S3 client

The AWS SDK S3Client keeps HTTP connections alive in its pool. Deno
waits for them to drain before exiting, causing a long hang after
backup completes. Added destroy() to S3Provider interface and call
it in finally blocks for all commands.

+50 -36
+42 -36
cli/mod.ts
··· 51 51 52 52 const config = requireConfig(); 53 53 const reader = openPhotosDb(db); 54 + const credentials = await loadKeychainCredentials( 55 + config.keychain.accessKeyService, 56 + config.keychain.secretKeyService, 57 + ); 58 + const s3 = createS3Provider( 59 + credentials, 60 + bucket ?? config.bucket, 61 + s3ConnectionFromConfig(config), 62 + ); 54 63 try { 55 64 const assets = reader.readAssets(); 56 - const credentials = await loadKeychainCredentials( 57 - config.keychain.accessKeyService, 58 - config.keychain.secretKeyService, 59 - ); 60 - const s3 = createS3Provider( 61 - credentials, 62 - bucket ?? config.bucket, 63 - s3ConnectionFromConfig(config), 64 - ); 65 65 const manifestStore = createS3ManifestStore(s3); 66 66 const manifest = await manifestStore.load(); 67 67 printStatusReport(assets, manifest); 68 68 } finally { 69 + s3.destroy(); 69 70 reader.close(); 70 71 } 71 72 }); ··· 116 117 const logger = options.log 117 118 ? createFileLogger(options.log) 118 119 : createNullLogger(); 120 + const credentials = await loadKeychainCredentials( 121 + config.keychain.accessKeyService, 122 + config.keychain.secretKeyService, 123 + ); 124 + const s3 = createS3Provider( 125 + credentials, 126 + options.bucket ?? config.bucket, 127 + s3ConnectionFromConfig(config), 128 + ); 129 + 119 130 try { 120 131 const assets = reader.readAssets(); 121 - 122 - const credentials = await loadKeychainCredentials( 123 - config.keychain.accessKeyService, 124 - config.keychain.secretKeyService, 125 - ); 126 - const s3 = createS3Provider( 127 - credentials, 128 - options.bucket ?? config.bucket, 129 - s3ConnectionFromConfig(config), 130 - ); 131 132 132 133 const manifestStore = createS3ManifestStore(s3); 133 134 const manifest = await loadManifestWithMigration(manifestStore); ··· 148 149 notifyOnComplete: options.notify ?? false, 149 150 }); 150 151 } finally { 152 + s3.destroy(); 151 153 logger.close(); 152 154 reader.close(); 153 155 } ··· 179 181 180 182 const config = requireConfig(); 181 183 const reader = openPhotosDb(options.db); 184 + const credentials = await loadKeychainCredentials( 185 + config.keychain.accessKeyService, 186 + config.keychain.secretKeyService, 187 + ); 188 + const s3 = createS3Provider( 189 + credentials, 190 + options.bucket ?? config.bucket, 191 + s3ConnectionFromConfig(config), 192 + ); 182 193 try { 183 194 const assets = reader.readAssets(); 184 - 185 - const credentials = await loadKeychainCredentials( 186 - config.keychain.accessKeyService, 187 - config.keychain.secretKeyService, 188 - ); 189 - const s3 = createS3Provider( 190 - credentials, 191 - options.bucket ?? config.bucket, 192 - s3ConnectionFromConfig(config), 193 - ); 194 195 195 196 const manifestStore = createS3ManifestStore(s3); 196 197 const manifest = await manifestStore.load(); ··· 201 202 }); 202 203 if (report.failed > 0) Deno.exit(2); 203 204 } finally { 205 + s3.destroy(); 204 206 reader.close(); 205 207 } 206 208 }); ··· 243 245 ); 244 246 const manifestStore = createS3ManifestStore(s3); 245 247 246 - if (options.rebuildManifest) { 247 - await rebuildManifest(s3, manifestStore); 248 - } else { 249 - const manifest = await manifestStore.load(); 250 - await runVerify(manifest, s3, { 251 - deep: options.deep ?? false, 252 - }); 248 + try { 249 + if (options.rebuildManifest) { 250 + await rebuildManifest(s3, manifestStore); 251 + } else { 252 + const manifest = await manifestStore.load(); 253 + await runVerify(manifest, s3, { 254 + deep: options.deep ?? false, 255 + }); 256 + } 257 + } finally { 258 + s3.destroy(); 253 259 } 254 260 }); 255 261
+2
cli/src/storage/s3-client.mock.ts
··· 41 41 }); 42 42 }, 43 43 44 + destroy() {}, 45 + 44 46 async *listObjects(prefix: string): AsyncIterable<S3Object> { 45 47 for (const [key, obj] of objects) { 46 48 if (key.startsWith(prefix)) {
+6
cli/src/storage/s3-client.ts
··· 27 27 getObject(key: string): Promise<Uint8Array>; 28 28 headObject(key: string): Promise<S3ObjectMeta | null>; 29 29 listObjects(prefix: string): AsyncIterable<S3Object>; 30 + /** Close the underlying HTTP connection pool so the process can exit. */ 31 + destroy(): void; 30 32 } 31 33 32 34 export interface S3ConnectionConfig { ··· 104 106 } 105 107 throw error; 106 108 } 109 + }, 110 + 111 + destroy() { 112 + client.destroy(); 107 113 }, 108 114 109 115 async *listObjects(prefix: string): AsyncIterable<S3Object> {