1import * as Lambda from "@/AWS/Lambda";
2import * as S3 from "@/AWS/S3";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Schema from "effect/Schema";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9
10export class S3VersionedObjectLockFunction extends Lambda.Function<S3VersionedObjectLockFunction>()(
11 "S3VersionedObjectLockFunction",
12) {}
13
14const versionRequest = Schema.Struct({
15 Key: Schema.String,
16 VersionId: Schema.optional(Schema.String),
17});
18
19export default S3VersionedObjectLockFunction.make(
20 {
21 main: import.meta.url,
22 functionUrl: true,
23 timeout: Duration.seconds(30),
24 memorySize: 512,
25 },
26 Effect.gen(function* () {
27 const props = {
28 versioning: "Enabled",
29 objectLockEnabled: true,
30 forceDestroy: true,
31 } as const;
32 const buckets = {
33 GetObjectRetention: yield* S3.Bucket(
34 "VersionedObjectLockGetRetentionBucket",
35 props,
36 ),
37 PutObjectRetention: yield* S3.Bucket(
38 "VersionedObjectLockPutRetentionBucket",
39 props,
40 ),
41 GetObjectLegalHold: yield* S3.Bucket(
42 "VersionedObjectLockGetHoldBucket",
43 props,
44 ),
45 PutObjectLegalHold: yield* S3.Bucket(
46 "VersionedObjectLockPutHoldBucket",
47 props,
48 ),
49 RestoreObject: yield* S3.Bucket("VersionedObjectLockRestoreBucket", {
50 versioning: "Enabled",
51 forceDestroy: true,
52 }),
53 };
54 const getRetention = yield* S3.GetObjectRetention(
55 buckets.GetObjectRetention,
56 );
57 const putRetention = yield* S3.PutObjectRetention(
58 buckets.PutObjectRetention,
59 );
60 const getHold = yield* S3.GetObjectLegalHold(buckets.GetObjectLegalHold);
61 const putHold = yield* S3.PutObjectLegalHold(buckets.PutObjectLegalHold);
62 const restore = yield* S3.RestoreObject(buckets.RestoreObject);
63 const info = yield* Effect.forEach(
64 Object.entries(buckets),
65 ([binding, bucket]) =>
66 Effect.gen(function* () {
67 return {
68 binding,
69 bucketName: yield* bucket.bucketName,
70 bucketArn: yield* bucket.bucketArn,
71 };
72 }),
73 );
74
75 return {
76 fetch: Effect.gen(function* () {
77 const request = yield* HttpServerRequest;
78 const url = yield* Effect.sync(() => new URL(request.originalUrl));
79 if (url.pathname === "/info") {
80 const resolved = yield* Effect.forEach(info, (bucket) =>
81 Effect.gen(function* () {
82 return {
83 binding: bucket.binding,
84 bucketName: yield* bucket.bucketName,
85 bucketArn: yield* bucket.bucketArn,
86 };
87 }),
88 );
89 if (
90 resolved.some((bucket) => !bucket.bucketName || !bucket.bucketArn)
91 ) {
92 return HttpServerResponse.text("Outputs not hydrated yet", {
93 status: 503,
94 });
95 }
96 return yield* HttpServerResponse.json(resolved);
97 }
98 if (request.method !== "POST") {
99 return HttpServerResponse.text("Use POST", { status: 405 });
100 }
101 const body = yield* request.json;
102 switch (url.pathname) {
103 case "/get-retention": {
104 const input =
105 yield* Schema.decodeUnknownEffect(versionRequest)(body);
106 return yield* HttpServerResponse.json(yield* getRetention(input));
107 }
108 case "/put-retention": {
109 const input = yield* Schema.decodeUnknownEffect(
110 Schema.Struct({
111 Key: Schema.String,
112 VersionId: Schema.String,
113 retainUntil: Schema.String,
114 }),
115 )(body);
116 const RetainUntilDate = yield* Effect.sync(
117 () => new Date(input.retainUntil),
118 );
119 yield* putRetention({
120 Key: input.Key,
121 VersionId: input.VersionId,
122 Retention: { Mode: "GOVERNANCE", RetainUntilDate },
123 });
124 return yield* HttpServerResponse.json({ retained: true });
125 }
126 case "/get-hold": {
127 const input =
128 yield* Schema.decodeUnknownEffect(versionRequest)(body);
129 return yield* HttpServerResponse.json(yield* getHold(input));
130 }
131 case "/put-hold": {
132 const input = yield* Schema.decodeUnknownEffect(
133 Schema.Struct({
134 Key: Schema.String,
135 VersionId: Schema.String,
136 Status: Schema.Literals(["ON", "OFF"]),
137 }),
138 )(body);
139 yield* putHold({
140 Key: input.Key,
141 VersionId: input.VersionId,
142 LegalHold: { Status: input.Status },
143 });
144 return yield* HttpServerResponse.json({ updated: true });
145 }
146 case "/restore": {
147 const input =
148 yield* Schema.decodeUnknownEffect(versionRequest)(body);
149 const result = yield* restore({
150 ...input,
151 RestoreRequest: {
152 Days: 1,
153 GlacierJobParameters: { Tier: "Standard" },
154 },
155 }).pipe(
156 Effect.as({ tag: "accepted", versionId: input.VersionId }),
157 Effect.catchTag("InvalidObjectState", () =>
158 Effect.succeed({ tag: "InvalidObjectState" }),
159 ),
160 );
161 return yield* HttpServerResponse.json(result);
162 }
163 default:
164 return HttpServerResponse.text("Not found", { status: 404 });
165 }
166 }).pipe(
167 Effect.catchTag(
168 [
169 "NoSuchVersion",
170 "NoSuchKey",
171 "MethodNotAllowed",
172 "InvalidRequest",
173 "AccessDeniedException",
174 ],
175 (error) =>
176 HttpServerResponse.json(
177 { tag: error._tag },
178 {
179 status:
180 error._tag === "MethodNotAllowed"
181 ? 405
182 : error._tag === "InvalidRequest"
183 ? 400
184 : error._tag === "AccessDeniedException"
185 ? 403
186 : 404,
187 },
188 ),
189 ),
190 Effect.orDie,
191 ),
192 };
193 }).pipe(
194 Effect.provide(
195 Layer.mergeAll(
196 S3.GetObjectRetentionHttp,
197 S3.PutObjectRetentionHttp,
198 S3.GetObjectLegalHoldHttp,
199 S3.PutObjectLegalHoldHttp,
200 S3.RestoreObjectHttp,
201 ),
202 ),
203 ),
204);