Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions apps/web/actions/organization/create-space.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { getCurrentUser } from "@cap/database/auth/session";
import { nanoId, nanoIdLength } from "@cap/database/helpers";
import { spaceMembers, spaces, users } from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import { S3Buckets } from "@cap/web-backend";
import { and, eq, inArray } from "drizzle-orm";
import { Effect, Option } from "effect";
import { revalidatePath } from "next/cache";
import { v4 as uuidv4 } from "uuid";
import { createBucketProvider } from "@/utils/s3";
import { runPromise } from "@/lib/server";

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

const bucket = await createBucketProvider();

await bucket.putObject(fileKey, await iconFile.bytes(), {
contentType: iconFile.type,
});

// Construct the icon URL
if (serverEnv().CAP_AWS_BUCKET_URL) {
// If a custom bucket URL is defined, use it
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
} else if (serverEnv().CAP_AWS_ENDPOINT) {
// For custom endpoints like MinIO
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.name}/${fileKey}`;
} else {
// Default AWS S3 URL format
iconUrl = `https://${bucket.name}.s3.${
serverEnv().CAP_AWS_REGION || "us-east-1"
}.amazonaws.com/${fileKey}`;
}
await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());

yield* bucket.putObject(
fileKey,
yield* Effect.promise(() => iconFile.bytes()),
{ contentType: iconFile.type },
);

// Construct the icon URL
if (serverEnv().CAP_AWS_BUCKET_URL) {
// If a custom bucket URL is defined, use it
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
} else if (serverEnv().CAP_AWS_ENDPOINT) {
// For custom endpoints like MinIO
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
} else {
// Default AWS S3 URL format
iconUrl = `https://${bucket.bucketName}.s3.${
serverEnv().CAP_AWS_REGION || "us-east-1"
}.amazonaws.com/${fileKey}`;
}
}).pipe(runPromise);
} catch (error) {
console.error("Error uploading space icon:", error);
return {
Expand Down
40 changes: 22 additions & 18 deletions apps/web/actions/organization/delete-space.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
spaces,
spaceVideos,
} from "@cap/database/schema";
import { S3Buckets } from "@cap/web-backend";
import { eq } from "drizzle-orm";
import { Effect, Option } from "effect";
import { revalidatePath } from "next/cache";
import { createBucketProvider } from "@/utils/s3";
import { runPromise } from "@/lib/server";

interface DeleteSpaceResponse {
success: boolean;
Expand Down Expand Up @@ -67,25 +69,27 @@ export async function deleteSpace(

// 4. Delete space icons from S3
try {
const bucketProvider = await createBucketProvider();
await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());

const listedObjects = yield* bucket.listObjects({
prefix: `organizations/${user.activeOrganizationId}/spaces/${spaceId}/`,
});

if (listedObjects.Contents?.length) {
yield* bucket.deleteObjects(
listedObjects.Contents.map((content) => ({
Key: content.Key,
})),
);

console.log(
`Deleted ${listedObjects.Contents.length} objects for space ${spaceId}`,
);
}
}).pipe(runPromise);

// List all objects with the space prefix

const listedObjects = await bucketProvider.listObjects({
prefix: `organizations/${user.activeOrganizationId}/spaces/${spaceId}/`,
});

if (listedObjects.Contents?.length) {
await bucketProvider.deleteObjects(
listedObjects.Contents.map((content) => ({
Key: content.Key,
})),
);

console.log(
`Deleted ${listedObjects.Contents.length} objects for space ${spaceId}`,
);
}
} catch (error) {
console.error("Error deleting space icons from S3:", error);
// Continue with space deletion even if S3 deletion fails
Expand Down
24 changes: 15 additions & 9 deletions apps/web/actions/organization/update-space.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { nanoIdLength } from "@cap/database/helpers";
import { spaceMembers, spaces } from "@cap/database/schema";
import { S3Buckets } from "@cap/web-backend";
import { and, eq } from "drizzle-orm";
import { Effect, Option } from "effect";
import { revalidatePath } from "next/cache";
import { v4 as uuidv4 } from "uuid";
import { createBucketProvider } from "@/utils/s3";
import { runPromise } from "@/lib/server";
import { uploadSpaceIcon } from "./upload-space-icon";

export async function updateSpace(formData: FormData) {
Expand Down Expand Up @@ -48,14 +50,18 @@ export async function updateSpace(formData: FormData) {
// Remove icon from S3 and set iconUrl to null
const spaceArr = await db().select().from(spaces).where(eq(spaces.id, id));
const space = spaceArr[0];
if (space && space.iconUrl) {
try {
const bucketProvider = await createBucketProvider();
const prevKeyMatch = space.iconUrl.match(/organizations\/.+/);
if (prevKeyMatch && prevKeyMatch[0])
await bucketProvider.deleteObject(prevKeyMatch[0]);
} catch (e) {
console.warn("Failed to delete old space icon from S3", e);
if (space?.iconUrl) {
const key = space.iconUrl.match(/organizations\/.+/)?.[0];

if (key) {
try {
await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
yield* bucket.deleteObject(key);
}).pipe(runPromise);
} catch (e) {
console.warn("Failed to delete old space icon from S3", e);
}
}
}
await db().update(spaces).set({ iconUrl: null }).where(eq(spaces.id, id));
Expand Down
49 changes: 27 additions & 22 deletions apps/web/actions/organization/upload-organization-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { organizations } from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import { S3Buckets } from "@cap/web-backend";
import DOMPurify from "dompurify";
import { eq } from "drizzle-orm";
import { Effect, Option } from "effect";
import { JSDOM } from "jsdom";
import { revalidatePath } from "next/cache";
import { sanitizeFile } from "@/lib/sanitizeFile";
import { createBucketProvider } from "@/utils/s3";
import { runPromise } from "@/lib/server";

export async function uploadOrganizationIcon(
formData: FormData,
Expand Down Expand Up @@ -56,27 +58,30 @@ export async function uploadOrganizationIcon(

try {
const sanitizedFile = await sanitizeFile(file);

const bucket = await createBucketProvider();

await bucket.putObject(fileKey, await sanitizedFile.bytes(), {
contentType: file.type,
});

// Construct the icon URL
let iconUrl;
if (serverEnv().CAP_AWS_BUCKET_URL) {
// If a custom bucket URL is defined, use it
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
} else if (serverEnv().CAP_AWS_ENDPOINT) {
// For custom endpoints like MinIO
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.name}/${fileKey}`;
} else {
// Default AWS S3 URL format
iconUrl = `https://${bucket.name}.s3.${
serverEnv().CAP_AWS_REGION || "us-east-1"
}.amazonaws.com/${fileKey}`;
}
let iconUrl: string | undefined;

await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());

yield* bucket.putObject(
fileKey,
yield* Effect.promise(() => sanitizedFile.bytes()),
{ contentType: file.type },
);
// Construct the icon URL
if (serverEnv().CAP_AWS_BUCKET_URL) {
// If a custom bucket URL is defined, use it
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
} else if (serverEnv().CAP_AWS_ENDPOINT) {
// For custom endpoints like MinIO
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
} else {
// Default AWS S3 URL format
iconUrl = `https://${bucket.bucketName}.s3.${
serverEnv().CAP_AWS_REGION || "us-east-1"
}.amazonaws.com/${fileKey}`;
}
}).pipe(runPromise);

// Update organization with new icon URL
await db()
Expand Down
31 changes: 20 additions & 11 deletions apps/web/actions/organization/upload-space-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { spaces } from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import { S3Buckets } from "@cap/web-backend";
import { eq } from "drizzle-orm";
import { Effect, Option } from "effect";
import { revalidatePath } from "next/cache";
import { sanitizeFile } from "@/lib/sanitizeFile";
import { createBucketProvider } from "@/utils/s3";
import { runPromise } from "@/lib/server";

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

const bucket = await createBucketProvider();
const [bucket] = await S3Buckets.getBucketAccess(Option.none()).pipe(
runPromise,
);

try {
// Remove previous icon if exists
if (space.iconUrl) {
// Try to extract the previous S3 key from the URL
const prevKeyMatch = space.iconUrl.match(/organizations\/.+/);
if (prevKeyMatch && prevKeyMatch[0]) {
const key = space.iconUrl.match(/organizations\/.+/)?.[0];
if (key) {
try {
await bucket.deleteObject(prevKeyMatch[0]);
await bucket.deleteObject(key).pipe(runPromise);
} catch (e) {
// Log and continue
console.warn("Failed to delete old space icon from S3", e);
Expand All @@ -71,18 +75,23 @@ export async function uploadSpaceIcon(formData: FormData, spaceId: string) {

const sanitizedFile = await sanitizeFile(file);

await bucket.putObject(fileKey, await sanitizedFile.bytes(), {
contentType: file.type,
});
await bucket
.putObject(
fileKey,
Effect.promise(() => sanitizedFile.bytes()),
{ contentType: file.type },
)
.pipe(runPromise);

let iconUrl: string | undefined;

// Construct the icon URL
let iconUrl;
if (serverEnv().CAP_AWS_BUCKET_URL) {
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
} else if (serverEnv().CAP_AWS_ENDPOINT) {
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.name}/${fileKey}`;
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
} else {
iconUrl = `https://${bucket.name}.s3.${
iconUrl = `https://${bucket.bucketName}.s3.${
serverEnv().CAP_AWS_REGION || "us-east-1"
}.amazonaws.com/${fileKey}`;
}
Expand Down
41 changes: 23 additions & 18 deletions apps/web/actions/video/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { nanoId } from "@cap/database/helpers";
import { s3Buckets, videos, videoUploads } from "@cap/database/schema";
import { buildEnv, NODE_ENV, serverEnv } from "@cap/env";
import { userIsPro } from "@cap/utils";
import { S3Buckets } from "@cap/web-backend";
import { type Folder, Video } from "@cap/web-domain";
import { eq } from "drizzle-orm";
import { Effect, Option } from "effect";
import { revalidatePath } from "next/cache";
import { runPromise } from "@/lib/server";
import { dub } from "@/utils/dub";
import { createBucketProvider } from "@/utils/s3";

async function getVideoUploadPresignedUrl({
fileKey,
Expand Down Expand Up @@ -86,8 +88,6 @@ async function getVideoUploadPresignedUrl({
}
}

const bucket = await createBucketProvider(customBucket);

const contentType = fileKey.endsWith(".aac")
? "audio/aac"
: fileKey.endsWith(".webm")
Expand All @@ -109,19 +109,27 @@ async function getVideoUploadPresignedUrl({
"x-amz-meta-audiocodec": audioCodec ?? "",
};

const presignedPostData = await bucket.getPresignedPostUrl(fileKey, {
Fields,
Expires: 1800,
});

const customEndpoint = serverEnv().CAP_AWS_ENDPOINT;
if (customEndpoint && !customEndpoint.includes("amazonaws.com")) {
if (serverEnv().S3_PATH_STYLE) {
presignedPostData.url = `${customEndpoint}/${bucket.name}`;
} else {
presignedPostData.url = customEndpoint;
const presignedPostData = await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(
Option.fromNullable(customBucket?.id),
);

const presignedPostData = yield* bucket.getPresignedPostUrl(fileKey, {
Fields,
Expires: 1800,
});

const customEndpoint = serverEnv().CAP_AWS_ENDPOINT;
if (customEndpoint && !customEndpoint.includes("amazonaws.com")) {
if (serverEnv().S3_PATH_STYLE) {
presignedPostData.url = `${customEndpoint}/${bucket.bucketName}`;
} else {
presignedPostData.url = customEndpoint;
}
}
}

return presignedPostData;
}).pipe(runPromise);

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

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

const bucket = await createBucketProvider(customBucket);

const videoData = {
id: idToUse,
name: `Cap ${
isScreenshot ? "Screenshot" : isUpload ? "Upload" : "Recording"
} - ${formattedDate}`,
ownerId: user.id,
awsBucket: bucket.name,
source: { type: "desktopMP4" as const },
isScreenshot,
bucket: customBucket?.id,
Expand Down
Loading
Loading