Skip to content

Commit 29ebf78

Browse files
committed
rewrite s3 abstraction to not use layers
1 parent 0c83de3 commit 29ebf78

File tree

23 files changed

+667
-801
lines changed

23 files changed

+667
-801
lines changed

apps/web/actions/organization/create-space.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getCurrentUser } from "@cap/database/auth/session";
55
import { nanoId, nanoIdLength } from "@cap/database/helpers";
66
import { spaceMembers, spaces, users } from "@cap/database/schema";
77
import { serverEnv } from "@cap/env";
8-
import { S3BucketAccess, S3Buckets } from "@cap/web-backend";
8+
import { S3Buckets } from "@cap/web-backend";
99
import { and, eq, inArray } from "drizzle-orm";
1010
import { Effect, Option } from "effect";
1111
import { revalidatePath } from "next/cache";
@@ -92,33 +92,27 @@ export async function createSpace(
9292
}/spaces/${spaceId}/icon-${Date.now()}.${fileExtension}`;
9393

9494
await Effect.gen(function* () {
95-
const s3Buckets = yield* S3Buckets;
96-
const [S3ProviderLayer] = yield* s3Buckets.getProviderForBucket(
97-
Option.none(),
95+
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
96+
97+
yield* bucket.putObject(
98+
fileKey,
99+
yield* Effect.promise(() => iconFile.bytes()),
100+
{ contentType: iconFile.type },
98101
);
99102

100-
yield* Effect.gen(function* () {
101-
const bucket = yield* S3BucketAccess;
102-
yield* bucket.putObject(
103-
fileKey,
104-
yield* Effect.promise(() => iconFile.bytes()),
105-
{ contentType: iconFile.type },
106-
);
107-
108-
// Construct the icon URL
109-
if (serverEnv().CAP_AWS_BUCKET_URL) {
110-
// If a custom bucket URL is defined, use it
111-
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
112-
} else if (serverEnv().CAP_AWS_ENDPOINT) {
113-
// For custom endpoints like MinIO
114-
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
115-
} else {
116-
// Default AWS S3 URL format
117-
iconUrl = `https://${bucket.bucketName}.s3.${
118-
serverEnv().CAP_AWS_REGION || "us-east-1"
119-
}.amazonaws.com/${fileKey}`;
120-
}
121-
}).pipe(Effect.provide(S3ProviderLayer));
103+
// Construct the icon URL
104+
if (serverEnv().CAP_AWS_BUCKET_URL) {
105+
// If a custom bucket URL is defined, use it
106+
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
107+
} else if (serverEnv().CAP_AWS_ENDPOINT) {
108+
// For custom endpoints like MinIO
109+
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
110+
} else {
111+
// Default AWS S3 URL format
112+
iconUrl = `https://${bucket.bucketName}.s3.${
113+
serverEnv().CAP_AWS_REGION || "us-east-1"
114+
}.amazonaws.com/${fileKey}`;
115+
}
122116
}).pipe(runPromise);
123117
} catch (error) {
124118
console.error("Error uploading space icon:", error);

apps/web/actions/organization/delete-space.ts

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
spaces,
99
spaceVideos,
1010
} from "@cap/database/schema";
11-
import { S3BucketAccess, S3Buckets } from "@cap/web-backend";
11+
import { S3Buckets } from "@cap/web-backend";
1212
import { eq } from "drizzle-orm";
1313
import { Effect, Option } from "effect";
1414
import { revalidatePath } from "next/cache";
@@ -70,30 +70,23 @@ export async function deleteSpace(
7070
// 4. Delete space icons from S3
7171
try {
7272
await Effect.gen(function* () {
73-
const s3Buckets = yield* S3Buckets;
74-
const [S3ProviderLayer] = yield* s3Buckets.getProviderForBucket(
75-
Option.none(),
76-
);
77-
78-
yield* Effect.gen(function* () {
79-
const bucket = yield* S3BucketAccess;
80-
81-
const listedObjects = yield* bucket.listObjects({
82-
prefix: `organizations/${user.activeOrganizationId}/spaces/${spaceId}/`,
83-
});
84-
85-
if (listedObjects.Contents?.length) {
86-
yield* bucket.deleteObjects(
87-
listedObjects.Contents.map((content) => ({
88-
Key: content.Key,
89-
})),
90-
);
91-
92-
console.log(
93-
`Deleted ${listedObjects.Contents.length} objects for space ${spaceId}`,
94-
);
95-
}
96-
}).pipe(Effect.provide(S3ProviderLayer));
73+
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
74+
75+
const listedObjects = yield* bucket.listObjects({
76+
prefix: `organizations/${user.activeOrganizationId}/spaces/${spaceId}/`,
77+
});
78+
79+
if (listedObjects.Contents?.length) {
80+
yield* bucket.deleteObjects(
81+
listedObjects.Contents.map((content) => ({
82+
Key: content.Key,
83+
})),
84+
);
85+
86+
console.log(
87+
`Deleted ${listedObjects.Contents.length} objects for space ${spaceId}`,
88+
);
89+
}
9790
}).pipe(runPromise);
9891

9992
// List all objects with the space prefix

apps/web/actions/organization/update-space.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { db } from "@cap/database";
44
import { getCurrentUser } from "@cap/database/auth/session";
55
import { nanoIdLength } from "@cap/database/helpers";
66
import { spaceMembers, spaces } from "@cap/database/schema";
7-
import { S3BucketAccess, S3Buckets } from "@cap/web-backend";
7+
import { S3Buckets } from "@cap/web-backend";
88
import { and, eq } from "drizzle-orm";
99
import { Effect, Option } from "effect";
1010
import { revalidatePath } from "next/cache";
@@ -56,15 +56,8 @@ export async function updateSpace(formData: FormData) {
5656
if (key) {
5757
try {
5858
await Effect.gen(function* () {
59-
const s3Buckets = yield* S3Buckets;
60-
const [S3ProviderLayer] = yield* s3Buckets.getProviderForBucket(
61-
Option.none(),
62-
);
63-
64-
yield* Effect.gen(function* () {
65-
const bucket = yield* S3BucketAccess;
66-
yield* bucket.deleteObject(key);
67-
}).pipe(Effect.provide(S3ProviderLayer));
59+
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
60+
yield* bucket.deleteObject(key);
6861
}).pipe(runPromise);
6962
} catch (e) {
7063
console.warn("Failed to delete old space icon from S3", e);

apps/web/actions/organization/upload-organization-icon.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { db } from "@cap/database";
44
import { getCurrentUser } from "@cap/database/auth/session";
55
import { organizations } from "@cap/database/schema";
66
import { serverEnv } from "@cap/env";
7-
import { S3BucketAccess, S3Buckets } from "@cap/web-backend";
7+
import { S3Buckets } from "@cap/web-backend";
88
import DOMPurify from "dompurify";
99
import { eq } from "drizzle-orm";
1010
import { Effect, Option } from "effect";
@@ -61,32 +61,26 @@ export async function uploadOrganizationIcon(
6161
let iconUrl: string | undefined;
6262

6363
await Effect.gen(function* () {
64-
const s3Buckets = yield* S3Buckets;
65-
const [S3ProviderLayer] = yield* s3Buckets.getProviderForBucket(
66-
Option.none(),
67-
);
64+
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
6865

69-
yield* Effect.gen(function* () {
70-
const bucket = yield* S3BucketAccess;
71-
yield* bucket.putObject(
72-
fileKey,
73-
yield* Effect.promise(() => sanitizedFile.bytes()),
74-
{ contentType: file.type },
75-
);
76-
// Construct the icon URL
77-
if (serverEnv().CAP_AWS_BUCKET_URL) {
78-
// If a custom bucket URL is defined, use it
79-
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
80-
} else if (serverEnv().CAP_AWS_ENDPOINT) {
81-
// For custom endpoints like MinIO
82-
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
83-
} else {
84-
// Default AWS S3 URL format
85-
iconUrl = `https://${bucket.bucketName}.s3.${
86-
serverEnv().CAP_AWS_REGION || "us-east-1"
87-
}.amazonaws.com/${fileKey}`;
88-
}
89-
}).pipe(Effect.provide(S3ProviderLayer));
66+
yield* bucket.putObject(
67+
fileKey,
68+
yield* Effect.promise(() => sanitizedFile.bytes()),
69+
{ contentType: file.type },
70+
);
71+
// Construct the icon URL
72+
if (serverEnv().CAP_AWS_BUCKET_URL) {
73+
// If a custom bucket URL is defined, use it
74+
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
75+
} else if (serverEnv().CAP_AWS_ENDPOINT) {
76+
// For custom endpoints like MinIO
77+
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
78+
} else {
79+
// Default AWS S3 URL format
80+
iconUrl = `https://${bucket.bucketName}.s3.${
81+
serverEnv().CAP_AWS_REGION || "us-east-1"
82+
}.amazonaws.com/${fileKey}`;
83+
}
9084
}).pipe(runPromise);
9185

9286
// Update organization with new icon URL

apps/web/actions/organization/upload-space-icon.ts

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { db } from "@cap/database";
44
import { getCurrentUser } from "@cap/database/auth/session";
55
import { spaces } from "@cap/database/schema";
66
import { serverEnv } from "@cap/env";
7-
import { S3BucketAccess, S3Buckets } from "@cap/web-backend";
7+
import { S3Buckets } from "@cap/web-backend";
88
import { eq } from "drizzle-orm";
99
import { Effect, Option } from "effect";
1010
import { revalidatePath } from "next/cache";
@@ -54,10 +54,9 @@ export async function uploadSpaceIcon(formData: FormData, spaceId: string) {
5454
space.organizationId
5555
}/spaces/${spaceId}/icon-${Date.now()}.${fileExtension}`;
5656

57-
const [S3ProviderLayer] = await Effect.gen(function* () {
58-
const s3Buckets = yield* S3Buckets;
59-
return yield* s3Buckets.getProviderForBucket(Option.none());
60-
}).pipe(runPromise);
57+
const [bucket] = await S3Buckets.getBucketAccess(Option.none()).pipe(
58+
runPromise,
59+
);
6160

6261
try {
6362
// Remove previous icon if exists
@@ -66,10 +65,7 @@ export async function uploadSpaceIcon(formData: FormData, spaceId: string) {
6665
const key = space.iconUrl.match(/organizations\/.+/)?.[0];
6766
if (key) {
6867
try {
69-
await Effect.gen(function* () {
70-
const bucket = yield* S3BucketAccess;
71-
return yield* bucket.deleteObject(key);
72-
}).pipe(Effect.provide(S3ProviderLayer), runPromise);
68+
await bucket.deleteObject(key).pipe(runPromise);
7369
} catch (e) {
7470
// Log and continue
7571
console.warn("Failed to delete old space icon from S3", e);
@@ -79,26 +75,26 @@ export async function uploadSpaceIcon(formData: FormData, spaceId: string) {
7975

8076
const sanitizedFile = await sanitizeFile(file);
8177

82-
let iconUrl;
83-
await Effect.gen(function* () {
84-
const bucket = yield* S3BucketAccess;
85-
yield* bucket.putObject(
78+
await bucket
79+
.putObject(
8680
fileKey,
8781
Effect.promise(() => sanitizedFile.bytes()),
8882
{ contentType: file.type },
89-
);
90-
91-
// Construct the icon URL
92-
if (serverEnv().CAP_AWS_BUCKET_URL) {
93-
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
94-
} else if (serverEnv().CAP_AWS_ENDPOINT) {
95-
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
96-
} else {
97-
iconUrl = `https://${bucket.bucketName}.s3.${
98-
serverEnv().CAP_AWS_REGION || "us-east-1"
99-
}.amazonaws.com/${fileKey}`;
100-
}
101-
}).pipe(Effect.provide(S3ProviderLayer), runPromise);
83+
)
84+
.pipe(runPromise);
85+
86+
let iconUrl: string | undefined;
87+
88+
// Construct the icon URL
89+
if (serverEnv().CAP_AWS_BUCKET_URL) {
90+
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
91+
} else if (serverEnv().CAP_AWS_ENDPOINT) {
92+
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
93+
} else {
94+
iconUrl = `https://${bucket.bucketName}.s3.${
95+
serverEnv().CAP_AWS_REGION || "us-east-1"
96+
}.amazonaws.com/${fileKey}`;
97+
}
10298

10399
// Update space with new icon URL
104100
await db().update(spaces).set({ iconUrl }).where(eq(spaces.id, spaceId));

apps/web/actions/video/upload.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { nanoId } from "@cap/database/helpers";
1010
import { s3Buckets, videos, videoUploads } from "@cap/database/schema";
1111
import { buildEnv, NODE_ENV, serverEnv } from "@cap/env";
1212
import { userIsPro } from "@cap/utils";
13-
import { S3BucketAccess, S3Buckets } from "@cap/web-backend";
13+
import { S3Buckets } from "@cap/web-backend";
1414
import { type Folder, Video } from "@cap/web-domain";
1515
import { eq } from "drizzle-orm";
1616
import { Effect, Option } from "effect";
@@ -109,15 +109,11 @@ async function getVideoUploadPresignedUrl({
109109
"x-amz-meta-audiocodec": audioCodec ?? "",
110110
};
111111

112-
const [S3ProviderLayer] = await Effect.gen(function* () {
113-
const s3Buckets = yield* S3Buckets;
114-
return yield* s3Buckets.getProviderForBucket(
112+
const presignedPostData = await Effect.gen(function* () {
113+
const [bucket] = yield* S3Buckets.getBucketAccess(
115114
Option.fromNullable(customBucket?.id),
116115
);
117-
}).pipe(runPromise);
118116

119-
const presignedPostData = await Effect.gen(function* () {
120-
const bucket = yield* S3BucketAccess;
121117
const presignedPostData = yield* bucket.getPresignedPostUrl(fileKey, {
122118
Fields,
123119
Expires: 1800,
@@ -133,7 +129,7 @@ async function getVideoUploadPresignedUrl({
133129
}
134130

135131
return presignedPostData;
136-
}).pipe(Effect.provide(S3ProviderLayer), runPromise);
132+
}).pipe(runPromise);
137133

138134
const videoId = fileKey.split("/")[1];
139135
if (videoId) {

apps/web/actions/videos/download.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { db } from "@cap/database";
44
import { getCurrentUser } from "@cap/database/auth/session";
55
import { videos } from "@cap/database/schema";
6-
import { S3BucketAccess, S3Buckets } from "@cap/web-backend";
6+
import { S3Buckets } from "@cap/web-backend";
77
import type { Video } from "@cap/web-domain";
88
import { eq } from "drizzle-orm";
99
import { Effect, Option } from "effect";
@@ -36,16 +36,10 @@ export async function downloadVideo(videoId: Video.VideoId) {
3636
const videoKey = `${video.ownerId}/${videoId}/result.mp4`;
3737

3838
const downloadUrl = await Effect.gen(function* () {
39-
const s3Buckets = yield* S3Buckets;
40-
const [S3ProviderLayer] = yield* s3Buckets.getProviderForBucket(
39+
const [bucket] = yield* S3Buckets.getBucketAccess(
4140
Option.fromNullable(video.bucket),
4241
);
43-
44-
yield* Effect.gen(function* () {
45-
const bucket = yield* S3BucketAccess;
46-
47-
yield* bucket.getSignedObjectUrl(videoKey);
48-
}).pipe(Effect.provide(S3ProviderLayer));
42+
yield* bucket.getSignedObjectUrl(videoKey);
4943
}).pipe(runPromise);
5044

5145
return {

0 commit comments

Comments
 (0)