Skip to content

Commit fc13960

Browse files
remove non-effect s3 abstraction (#1060)
* remove non-effect s3 abstraction * formatting * tsc * Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * rewrite s3 abstraction to not use layers * ci * fix download action --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 0720f67 commit fc13960

File tree

31 files changed

+757
-1099
lines changed

31 files changed

+757
-1099
lines changed

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

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ 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 { S3Buckets } from "@cap/web-backend";
89
import { and, eq, inArray } from "drizzle-orm";
10+
import { Effect, Option } from "effect";
911
import { revalidatePath } from "next/cache";
1012
import { v4 as uuidv4 } from "uuid";
11-
import { createBucketProvider } from "@/utils/s3";
13+
import { runPromise } from "@/lib/server";
1214

1315
interface CreateSpaceResponse {
1416
success: boolean;
@@ -89,25 +91,29 @@ export async function createSpace(
8991
user.activeOrganizationId
9092
}/spaces/${spaceId}/icon-${Date.now()}.${fileExtension}`;
9193

92-
const bucket = await createBucketProvider();
93-
94-
await bucket.putObject(fileKey, await iconFile.bytes(), {
95-
contentType: iconFile.type,
96-
});
97-
98-
// Construct the icon URL
99-
if (serverEnv().CAP_AWS_BUCKET_URL) {
100-
// If a custom bucket URL is defined, use it
101-
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
102-
} else if (serverEnv().CAP_AWS_ENDPOINT) {
103-
// For custom endpoints like MinIO
104-
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.name}/${fileKey}`;
105-
} else {
106-
// Default AWS S3 URL format
107-
iconUrl = `https://${bucket.name}.s3.${
108-
serverEnv().CAP_AWS_REGION || "us-east-1"
109-
}.amazonaws.com/${fileKey}`;
110-
}
94+
await Effect.gen(function* () {
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 },
101+
);
102+
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+
}
116+
}).pipe(runPromise);
111117
} catch (error) {
112118
console.error("Error uploading space icon:", error);
113119
return {

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

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import {
88
spaces,
99
spaceVideos,
1010
} from "@cap/database/schema";
11+
import { S3Buckets } from "@cap/web-backend";
1112
import { eq } from "drizzle-orm";
13+
import { Effect, Option } from "effect";
1214
import { revalidatePath } from "next/cache";
13-
import { createBucketProvider } from "@/utils/s3";
15+
import { runPromise } from "@/lib/server";
1416

1517
interface DeleteSpaceResponse {
1618
success: boolean;
@@ -67,25 +69,27 @@ export async function deleteSpace(
6769

6870
// 4. Delete space icons from S3
6971
try {
70-
const bucketProvider = await createBucketProvider();
72+
await Effect.gen(function* () {
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+
}
90+
}).pipe(runPromise);
7191

7292
// List all objects with the space prefix
73-
74-
const listedObjects = await bucketProvider.listObjects({
75-
prefix: `organizations/${user.activeOrganizationId}/spaces/${spaceId}/`,
76-
});
77-
78-
if (listedObjects.Contents?.length) {
79-
await bucketProvider.deleteObjects(
80-
listedObjects.Contents.map((content) => ({
81-
Key: content.Key,
82-
})),
83-
);
84-
85-
console.log(
86-
`Deleted ${listedObjects.Contents.length} objects for space ${spaceId}`,
87-
);
88-
}
8993
} catch (error) {
9094
console.error("Error deleting space icons from S3:", error);
9195
// Continue with space deletion even if S3 deletion fails

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ 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 { S3Buckets } from "@cap/web-backend";
78
import { and, eq } from "drizzle-orm";
9+
import { Effect, Option } from "effect";
810
import { revalidatePath } from "next/cache";
911
import { v4 as uuidv4 } from "uuid";
10-
import { createBucketProvider } from "@/utils/s3";
12+
import { runPromise } from "@/lib/server";
1113
import { uploadSpaceIcon } from "./upload-space-icon";
1214

1315
export async function updateSpace(formData: FormData) {
@@ -48,14 +50,18 @@ export async function updateSpace(formData: FormData) {
4850
// Remove icon from S3 and set iconUrl to null
4951
const spaceArr = await db().select().from(spaces).where(eq(spaces.id, id));
5052
const space = spaceArr[0];
51-
if (space && space.iconUrl) {
52-
try {
53-
const bucketProvider = await createBucketProvider();
54-
const prevKeyMatch = space.iconUrl.match(/organizations\/.+/);
55-
if (prevKeyMatch && prevKeyMatch[0])
56-
await bucketProvider.deleteObject(prevKeyMatch[0]);
57-
} catch (e) {
58-
console.warn("Failed to delete old space icon from S3", e);
53+
if (space?.iconUrl) {
54+
const key = space.iconUrl.match(/organizations\/.+/)?.[0];
55+
56+
if (key) {
57+
try {
58+
await Effect.gen(function* () {
59+
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
60+
yield* bucket.deleteObject(key);
61+
}).pipe(runPromise);
62+
} catch (e) {
63+
console.warn("Failed to delete old space icon from S3", e);
64+
}
5965
}
6066
}
6167
await db().update(spaces).set({ iconUrl: null }).where(eq(spaces.id, id));

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

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ 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 { S3Buckets } from "@cap/web-backend";
78
import DOMPurify from "dompurify";
89
import { eq } from "drizzle-orm";
10+
import { Effect, Option } from "effect";
911
import { JSDOM } from "jsdom";
1012
import { revalidatePath } from "next/cache";
1113
import { sanitizeFile } from "@/lib/sanitizeFile";
12-
import { createBucketProvider } from "@/utils/s3";
14+
import { runPromise } from "@/lib/server";
1315

1416
export async function uploadOrganizationIcon(
1517
formData: FormData,
@@ -56,27 +58,30 @@ export async function uploadOrganizationIcon(
5658

5759
try {
5860
const sanitizedFile = await sanitizeFile(file);
59-
60-
const bucket = await createBucketProvider();
61-
62-
await bucket.putObject(fileKey, await sanitizedFile.bytes(), {
63-
contentType: file.type,
64-
});
65-
66-
// Construct the icon URL
67-
let iconUrl;
68-
if (serverEnv().CAP_AWS_BUCKET_URL) {
69-
// If a custom bucket URL is defined, use it
70-
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
71-
} else if (serverEnv().CAP_AWS_ENDPOINT) {
72-
// For custom endpoints like MinIO
73-
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.name}/${fileKey}`;
74-
} else {
75-
// Default AWS S3 URL format
76-
iconUrl = `https://${bucket.name}.s3.${
77-
serverEnv().CAP_AWS_REGION || "us-east-1"
78-
}.amazonaws.com/${fileKey}`;
79-
}
61+
let iconUrl: string | undefined;
62+
63+
await Effect.gen(function* () {
64+
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
65+
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+
}
84+
}).pipe(runPromise);
8085

8186
// Update organization with new icon URL
8287
await db()

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ 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 { S3Buckets } from "@cap/web-backend";
78
import { eq } from "drizzle-orm";
9+
import { Effect, Option } from "effect";
810
import { revalidatePath } from "next/cache";
911
import { sanitizeFile } from "@/lib/sanitizeFile";
10-
import { createBucketProvider } from "@/utils/s3";
12+
import { runPromise } from "@/lib/server";
1113

1214
export async function uploadSpaceIcon(formData: FormData, spaceId: string) {
1315
const user = await getCurrentUser();
@@ -52,16 +54,18 @@ export async function uploadSpaceIcon(formData: FormData, spaceId: string) {
5254
space.organizationId
5355
}/spaces/${spaceId}/icon-${Date.now()}.${fileExtension}`;
5456

55-
const bucket = await createBucketProvider();
57+
const [bucket] = await S3Buckets.getBucketAccess(Option.none()).pipe(
58+
runPromise,
59+
);
5660

5761
try {
5862
// Remove previous icon if exists
5963
if (space.iconUrl) {
6064
// Try to extract the previous S3 key from the URL
61-
const prevKeyMatch = space.iconUrl.match(/organizations\/.+/);
62-
if (prevKeyMatch && prevKeyMatch[0]) {
65+
const key = space.iconUrl.match(/organizations\/.+/)?.[0];
66+
if (key) {
6367
try {
64-
await bucket.deleteObject(prevKeyMatch[0]);
68+
await bucket.deleteObject(key).pipe(runPromise);
6569
} catch (e) {
6670
// Log and continue
6771
console.warn("Failed to delete old space icon from S3", e);
@@ -71,18 +75,23 @@ export async function uploadSpaceIcon(formData: FormData, spaceId: string) {
7175

7276
const sanitizedFile = await sanitizeFile(file);
7377

74-
await bucket.putObject(fileKey, await sanitizedFile.bytes(), {
75-
contentType: file.type,
76-
});
78+
await bucket
79+
.putObject(
80+
fileKey,
81+
Effect.promise(() => sanitizedFile.bytes()),
82+
{ contentType: file.type },
83+
)
84+
.pipe(runPromise);
85+
86+
let iconUrl: string | undefined;
7787

7888
// Construct the icon URL
79-
let iconUrl;
8089
if (serverEnv().CAP_AWS_BUCKET_URL) {
8190
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
8291
} else if (serverEnv().CAP_AWS_ENDPOINT) {
83-
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.name}/${fileKey}`;
92+
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
8493
} else {
85-
iconUrl = `https://${bucket.name}.s3.${
94+
iconUrl = `https://${bucket.bucketName}.s3.${
8695
serverEnv().CAP_AWS_REGION || "us-east-1"
8796
}.amazonaws.com/${fileKey}`;
8897
}

apps/web/actions/video/upload.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ 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 { S3Buckets } from "@cap/web-backend";
1314
import { type Folder, Video } from "@cap/web-domain";
1415
import { eq } from "drizzle-orm";
16+
import { Effect, Option } from "effect";
1517
import { revalidatePath } from "next/cache";
18+
import { runPromise } from "@/lib/server";
1619
import { dub } from "@/utils/dub";
17-
import { createBucketProvider } from "@/utils/s3";
1820

1921
async function getVideoUploadPresignedUrl({
2022
fileKey,
@@ -86,8 +88,6 @@ async function getVideoUploadPresignedUrl({
8688
}
8789
}
8890

89-
const bucket = await createBucketProvider(customBucket);
90-
9191
const contentType = fileKey.endsWith(".aac")
9292
? "audio/aac"
9393
: fileKey.endsWith(".webm")
@@ -109,19 +109,27 @@ async function getVideoUploadPresignedUrl({
109109
"x-amz-meta-audiocodec": audioCodec ?? "",
110110
};
111111

112-
const presignedPostData = await bucket.getPresignedPostUrl(fileKey, {
113-
Fields,
114-
Expires: 1800,
115-
});
116-
117-
const customEndpoint = serverEnv().CAP_AWS_ENDPOINT;
118-
if (customEndpoint && !customEndpoint.includes("amazonaws.com")) {
119-
if (serverEnv().S3_PATH_STYLE) {
120-
presignedPostData.url = `${customEndpoint}/${bucket.name}`;
121-
} else {
122-
presignedPostData.url = customEndpoint;
112+
const presignedPostData = await Effect.gen(function* () {
113+
const [bucket] = yield* S3Buckets.getBucketAccess(
114+
Option.fromNullable(customBucket?.id),
115+
);
116+
117+
const presignedPostData = yield* bucket.getPresignedPostUrl(fileKey, {
118+
Fields,
119+
Expires: 1800,
120+
});
121+
122+
const customEndpoint = serverEnv().CAP_AWS_ENDPOINT;
123+
if (customEndpoint && !customEndpoint.includes("amazonaws.com")) {
124+
if (serverEnv().S3_PATH_STYLE) {
125+
presignedPostData.url = `${customEndpoint}/${bucket.bucketName}`;
126+
} else {
127+
presignedPostData.url = customEndpoint;
128+
}
123129
}
124-
}
130+
131+
return presignedPostData;
132+
}).pipe(runPromise);
125133

126134
const videoId = fileKey.split("/")[1];
127135
if (videoId) {
@@ -214,15 +222,12 @@ export async function createVideoAndGetUploadUrl({
214222

215223
const idToUse = Video.VideoId.make(videoId || nanoId());
216224

217-
const bucket = await createBucketProvider(customBucket);
218-
219225
const videoData = {
220226
id: idToUse,
221227
name: `Cap ${
222228
isScreenshot ? "Screenshot" : isUpload ? "Upload" : "Recording"
223229
} - ${formattedDate}`,
224230
ownerId: user.id,
225-
awsBucket: bucket.name,
226231
source: { type: "desktopMP4" as const },
227232
isScreenshot,
228233
bucket: customBucket?.id,

0 commit comments

Comments
 (0)