1import * as s3 from "@distilled.cloud/aws/s3";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4import * as Schedule from "effect/Schedule";
5import {
6 createAssetsBucket,
7 ensureAssetsBucketTags,
8 lookupAssetsBucket,
9 lookupAssetsBuckets,
10} from "./Assets.ts";
11
12/**
13 * Bootstrap the AWS environment by creating the assets bucket.
14 *
15 * This is idempotent - running it multiple times is safe.
16 * The bucket is tagged and later discovered by tag lookup instead of by name.
17 */
18export const bootstrap = Effect.fn(function* () {
19 const existingBucket = yield* lookupAssetsBucket;
20
21 if (Option.isSome(existingBucket)) {
22 yield* ensureAssetsBucketTags(existingBucket.value);
23 yield* Effect.logInfo(
24 `Assets bucket already exists: ${existingBucket.value}`,
25 );
26 return { bucketName: existingBucket.value, created: false };
27 }
28
29 const bucketName = yield* createAssetsBucket;
30 return { bucketName, created: true };
31});
32
33export const destroyBootstrap = Effect.fn(function* () {
34 const bucketNames = yield* lookupAssetsBuckets;
35
36 for (const bucketName of bucketNames) {
37 yield* Effect.logInfo(`Destroying assets bucket: ${bucketName}`);
38 yield* deleteAllObjects(bucketName);
39 yield* s3.deleteBucket({ Bucket: bucketName }).pipe(
40 Effect.retry({
41 while: (e) =>
42 e._tag === "OperationAborted" || e._tag === "ServiceUnavailable",
43 schedule: Schedule.max([
44 Schedule.exponential(100),
45 Schedule.recurs(10),
46 ]),
47 }),
48 );
49 }
50
51 return {
52 bucketNames,
53 destroyed: bucketNames.length,
54 };
55});
56
57const deleteAllObjects = Effect.fn(function* (bucketName: string) {
58 let continuationToken: string | undefined;
59 do {
60 const listResponse = yield* s3.listObjectsV2({
61 Bucket: bucketName,
62 ContinuationToken: continuationToken,
63 });
64
65 if (listResponse.Contents && listResponse.Contents.length > 0) {
66 yield* s3.deleteObjects({
67 Bucket: bucketName,
68 Delete: {
69 Objects: listResponse.Contents.map((obj) => ({
70 Key: obj.Key!,
71 })),
72 Quiet: true,
73 },
74 });
75 }
76
77 continuationToken = listResponse.NextContinuationToken;
78 } while (continuationToken);
79
80 let keyMarker: string | undefined;
81 let versionIdMarker: string | undefined;
82 do {
83 const listVersionsResponse = yield* s3.listObjectVersions({
84 Bucket: bucketName,
85 KeyMarker: keyMarker,
86 VersionIdMarker: versionIdMarker,
87 });
88
89 const objectsToDelete = [
90 ...(listVersionsResponse.Versions ?? []).map((v) => ({
91 Key: v.Key!,
92 VersionId: v.VersionId,
93 })),
94 ...(listVersionsResponse.DeleteMarkers ?? []).map((dm) => ({
95 Key: dm.Key!,
96 VersionId: dm.VersionId,
97 })),
98 ];
99
100 if (objectsToDelete.length > 0) {
101 yield* s3.deleteObjects({
102 Bucket: bucketName,
103 Delete: {
104 Objects: objectsToDelete,
105 Quiet: true,
106 },
107 });
108 }
109
110 keyMarker = listVersionsResponse.NextKeyMarker;
111 versionIdMarker = listVersionsResponse.NextVersionIdMarker;
112 } while (keyMarker);
113});