Skip to content

Docker Build #397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
3 changes: 2 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
auto-install-peers = true
auto-install-peers = true
inject-workspace-packages=true
81 changes: 81 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# syntax=docker.io/docker/dockerfile:1

FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable


# 1. Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat


WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./

COPY /patches ./patches


# Install dependencies based on lockfile
RUN if [ -f yarn.lock ]; then \
yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then \
npm ci; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm; \
else \
echo "Lockfile not found." && exit 1; \
fi
# Use mount cache for pnpm if pnpm-lock exists
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
if [ -f pnpm-lock.yaml ]; then \
pnpm i --frozen-lockfile; \
fi


# 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/patches ./patches
COPY . .


# build-time only variables
ARG DOCKER_BUILD=true
ENV NEXT_PUBLIC_WEB_URL=http://localhost:3000
ENV NEXT_PUBLIC_CAP_AWS_BUCKET=capso
ENV NEXT_PUBLIC_CAP_AWS_REGION=us-east-1

RUN corepack enable pnpm && pnpm i && pnpm run build:web

# We re-install packages instead of copy from deps due to an issue with pnpm and the way it installs app packages under certain conditions
RUN corepack enable pnpm
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm i
RUN pnpm run build:web


# 3. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001


# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
COPY --from=builder --chown=nextjs:nodejs /app/packages/database/migrations ./apps/web/migrations


USER nextjs

EXPOSE 3000

CMD HOSTNAME="0.0.0.0" node apps/web/server.js
85 changes: 85 additions & 0 deletions apps/web/actions/screenshots/get-screenshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { db } from "@cap/database";
import { s3Buckets, videos } from "@cap/database/schema";
import { eq } from "drizzle-orm";
import {
S3Client,
ListObjectsV2Command,
GetObjectCommand,
} from "@aws-sdk/client-s3";
import { getCurrentUser } from "@cap/database/auth/session";
import { createS3Client, getS3Bucket } from "@/utils/s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { S3_BUCKET_URL } from "@cap/utils";
import { buildEnv, serverEnv } from "@cap/env";

export async function getScreenshot(userId: string, screenshotId: string) {
if (!userId || !screenshotId) {
throw new Error("userId or screenshotId not supplied");
}

const query = await db()
.select({ video: videos, bucket: s3Buckets })
.from(videos)
.leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id))
.where(eq(videos.id, screenshotId));

if (query.length === 0) {
throw new Error("Video does not exist");
}

const result = query[0];
if (!result?.video) {
throw new Error("Video not found");
}

const { video, bucket } = result;

if (video.public === false) {
const user = await getCurrentUser();

if (!user || user.id !== video.ownerId) {
throw new Error("Video is not public");
}
}

const Bucket = await getS3Bucket(bucket);
const screenshotPrefix = `${userId}/${screenshotId}/`;

try {
const [s3Client] = await createS3Client(bucket);

const objectsCommand = new ListObjectsV2Command({
Bucket,
Prefix: screenshotPrefix,
});

const objects = await s3Client.send(objectsCommand);

const screenshot = objects.Contents?.find((object) =>
object.Key?.endsWith(".png")
);

if (!screenshot) {
throw new Error("Screenshot not found");
}

let screenshotUrl: string;

if (video.awsBucket !== serverEnv().CAP_AWS_BUCKET) {
screenshotUrl = await getSignedUrl(
s3Client,
new GetObjectCommand({
Bucket,
Key: screenshot.Key,
}),
{ expiresIn: 3600 }
);
} else {
screenshotUrl = `${S3_BUCKET_URL}/${screenshot.Key}`;
}

return { url: screenshotUrl };
} catch (error) {
throw new Error(`Error generating screenshot URL: ${error}`);
}
}
82 changes: 82 additions & 0 deletions apps/web/actions/videos/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use server";

import { getCurrentUser } from "@cap/database/auth/session";
import { s3Buckets, videos } from "@cap/database/schema";
import { db } from "@cap/database";
import { and, eq } from "drizzle-orm";
import { DeleteObjectsCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { createS3Client, getS3Bucket } from "@/utils/s3";

export async function deleteVideo(videoId: string) {
try {
const user = await getCurrentUser();
const userId = user?.id;

if (!videoId || !userId) {
return {
success: false,
message: "Missing required data",
};
}

const query = await db()
.select({ video: videos, bucket: s3Buckets })
.from(videos)
.leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id))
.where(eq(videos.id, videoId));

if (query.length === 0) {
return {
success: false,
message: "Video does not exist",
};
}

const result = query[0];
if (!result) {
return {
success: false,
message: "Video not found",
};
}

await db()
.delete(videos)
.where(and(eq(videos.id, videoId), eq(videos.ownerId, userId)));

const [s3Client] = await createS3Client(result.bucket);
const Bucket = await getS3Bucket(result.bucket);
const prefix = `${userId}/${videoId}/`;

const listObjectsCommand = new ListObjectsV2Command({
Bucket,
Prefix: prefix,
});

const listedObjects = await s3Client.send(listObjectsCommand);

if (listedObjects.Contents?.length) {
const deleteObjectsCommand = new DeleteObjectsCommand({
Bucket,
Delete: {
Objects: listedObjects.Contents.map((content: any) => ({
Key: content.Key,
})),
},
});

await s3Client.send(deleteObjectsCommand);
}

return {
success: true,
message: "Video deleted successfully",
};
} catch (error) {
console.error("Error deleting video:", error);
return {
success: false,
message: "Failed to delete video",
};
}
}
24 changes: 12 additions & 12 deletions apps/web/actions/videos/edit-date.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'use server';
"use server";

import { getCurrentUser } from "@cap/database/auth/session";
import { videos } from "@cap/database/schema";
Expand All @@ -9,13 +9,13 @@ import { revalidatePath } from "next/cache";

export async function editDate(videoId: string, date: string) {
const user = await getCurrentUser();

if (!user || !date || !videoId) {
throw new Error("Missing required data for updating video date");
}

const userId = user.id;
const query = await db.select().from(videos).where(eq(videos.id, videoId));
const query = await db().select().from(videos).where(eq(videos.id, videoId));

if (query.length === 0) {
throw new Error("Video not found");
Expand All @@ -33,30 +33,30 @@ export async function editDate(videoId: string, date: string) {
try {
const newDate = new Date(date);
const currentDate = new Date();

// Prevent future dates
if (newDate > currentDate) {
throw new Error("Cannot set a date in the future");
}

// Store the custom date in the metadata field
const currentMetadata = video.metadata as VideoMetadata || {};
const currentMetadata = (video.metadata as VideoMetadata) || {};
const updatedMetadata: VideoMetadata = {
...currentMetadata,
customCreatedAt: newDate.toISOString(),
};
await db

await db()
.update(videos)
.set({
metadata: updatedMetadata,
})
.where(eq(videos.id, videoId));

// Revalidate paths to update the UI
revalidatePath('/dashboard/caps');
revalidatePath('/dashboard/shared-caps');
revalidatePath("/dashboard/caps");
revalidatePath("/dashboard/shared-caps");

return { success: true };
} catch (error) {
console.error("Error updating video date:", error);
Expand All @@ -65,4 +65,4 @@ export async function editDate(videoId: string, date: string) {
}
throw new Error("Failed to update video date");
}
}
}
16 changes: 8 additions & 8 deletions apps/web/actions/videos/edit-title.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'use server';
"use server";

import { getCurrentUser } from "@cap/database/auth/session";
import { videos } from "@cap/database/schema";
Expand All @@ -8,13 +8,13 @@ import { revalidatePath } from "next/cache";

export async function editTitle(videoId: string, title: string) {
const user = await getCurrentUser();

if (!user || !title || !videoId) {
throw new Error("Missing required data for updating video title");
}

const userId = user.id;
const query = await db.select().from(videos).where(eq(videos.id, videoId));
const query = await db().select().from(videos).where(eq(videos.id, videoId));

if (query.length === 0) {
throw new Error("Video not found");
Expand All @@ -30,15 +30,15 @@ export async function editTitle(videoId: string, title: string) {
}

try {
await db
await db()
.update(videos)
.set({ name: title })
.where(eq(videos.id, videoId));

revalidatePath('/dashboard/caps');
revalidatePath('/dashboard/shared-caps');
revalidatePath("/dashboard/caps");
revalidatePath("/dashboard/shared-caps");
revalidatePath(`/s/${videoId}`);

return { success: true };
} catch (error) {
console.error("Error updating video title:", error);
Expand All @@ -47,4 +47,4 @@ export async function editTitle(videoId: string, title: string) {
}
throw new Error("Failed to update video title");
}
}
}
6 changes: 3 additions & 3 deletions apps/web/actions/videos/get-analytics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'use server';
"use server";

import { dub } from "@/utils/dub";
import { ClicksCount } from "dub/models/components";
Expand All @@ -9,7 +9,7 @@ export async function getVideoAnalytics(videoId: string) {
}

try {
const response = await dub.analytics.retrieve({
const response = await dub().analytics.retrieve({
domain: "cap.link",
key: videoId,
});
Expand All @@ -28,4 +28,4 @@ export async function getVideoAnalytics(videoId: string) {
console.error("Error fetching video analytics:", error);
return { count: 0 };
}
}
}
Loading