Skip to content

Commit 8ccb474

Browse files
authored
Dev: Adds ability to run cap complextely local by adding s3 docker container (#251)
* docker: add local s3 * add .env s3 bucket url * add support for SH custom s3 * add s3 bucket url shared CONST * clean up docker start and package names * fix s3 auth and image render * docker autostart/stop * fix docker stop * cleanup env example * fix rust errors * turbo tui
1 parent 265ed8b commit 8ccb474

File tree

31 files changed

+313
-140
lines changed

31 files changed

+313
-140
lines changed

.env.example

+7-4
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@
1717
# - REQUIRED ****************
1818

1919
NEXT_PUBLIC_ENVIRONMENT=development
20-
NEXT_PUBLIC_URL=http://localhost:3000
2120
NEXT_PUBLIC_PORT=3000
21+
NEXT_PUBLIC_URL=http://localhost:${NEXT_PUBLIC_PORT}
2222
NEXT_PUBLIC_TASKS_URL=http://localhost:3002
2323

24-
NEXT_PUBLIC_URL=http://localhost:${NEXT_PUBLIC_PORT}
2524
NEXTAUTH_URL=${NEXT_PUBLIC_URL}
2625
VITE_SERVER_URL=${NEXT_PUBLIC_URL}
2726
VITE_ENVIRONMENT=${NEXT_PUBLIC_ENVIRONMENT}
@@ -53,9 +52,13 @@ NEXTAUTH_SECRET=
5352
# -- aws ****************
5453
## For use with AWS S3, to upload recorded caps. You can retrieve these credentials from your own AWS account.
5554
## Uses CAP_ prefix to avoid conflict with env variables set in hosting environment. (e.g. Vercel)
56-
CAP_AWS_ACCESS_KEY=
57-
CAP_AWS_SECRET_KEY=
55+
CAP_AWS_ACCESS_KEY=capS3root
56+
CAP_AWS_SECRET_KEY=capS3root
57+
# If using AWS S3 - leave ENDPOINT blank
58+
NEXT_PUBLIC_CAP_AWS_ENDPOINT=http://localhost:3902
5859
NEXT_PUBLIC_CAP_AWS_BUCKET=capso
60+
# If using a Proxy such as Cloudfront, set the URL here to the full URL of the bucket
61+
NEXT_PUBLIC_CAP_AWS_BUCKET_URL=
5962
NEXT_PUBLIC_CAP_AWS_REGION=us-east-1
6063

6164
# -- Deepgram (for transcription) ****************

apps/desktop/src-tauri/src/upload.rs

+24-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub struct S3UploadMeta {
2424
aws_region: String,
2525
#[serde(default, deserialize_with = "deserialize_empty_object_as_string")]
2626
aws_bucket: String,
27+
#[serde(default)]
28+
aws_endpoint: String,
2729
}
2830

2931
fn deserialize_empty_object_as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
@@ -82,12 +84,23 @@ impl S3UploadMeta {
8284
&self.aws_bucket
8385
}
8486

85-
pub fn new(id: String, user_id: String, aws_region: String, aws_bucket: String) -> Self {
87+
pub fn aws_endpoint(&self) -> &str {
88+
&self.aws_endpoint
89+
}
90+
91+
pub fn new(
92+
id: String,
93+
user_id: String,
94+
aws_region: String,
95+
aws_bucket: String,
96+
aws_endpoint: String,
97+
) -> Self {
8698
Self {
8799
id,
88100
user_id,
89101
aws_region,
90102
aws_bucket,
103+
aws_endpoint,
91104
}
92105
}
93106

@@ -100,6 +113,10 @@ impl S3UploadMeta {
100113
self.aws_bucket =
101114
std::env::var("NEXT_PUBLIC_CAP_AWS_BUCKET").unwrap_or_else(|_| "capso".to_string());
102115
}
116+
if self.aws_endpoint.is_empty() {
117+
self.aws_endpoint = std::env::var("NEXT_PUBLIC_CAP_AWS_ENDPOINT")
118+
.unwrap_or_else(|_| "https://s3.amazonaws.com".to_string());
119+
}
103120
}
104121
}
105122

@@ -110,6 +127,7 @@ struct S3UploadBody {
110127
file_key: String,
111128
aws_bucket: String,
112129
aws_region: String,
130+
aws_endpoint: String,
113131
}
114132

115133
#[derive(serde::Serialize)]
@@ -197,6 +215,7 @@ pub async fn upload_video(
197215
file_key: file_key.clone(),
198216
aws_bucket: s3_config.aws_bucket().to_string(),
199217
aws_region: s3_config.aws_region().to_string(),
218+
aws_endpoint: s3_config.aws_endpoint().to_string(),
200219
},
201220
)?;
202221

@@ -350,6 +369,7 @@ pub async fn upload_image(app: &AppHandle, file_path: PathBuf) -> Result<Uploade
350369
file_key: file_key.clone(),
351370
aws_bucket: s3_config.aws_bucket,
352371
aws_region: s3_config.aws_region,
372+
aws_endpoint: s3_config.aws_endpoint,
353373
},
354374
};
355375

@@ -417,6 +437,7 @@ pub async fn upload_audio(app: &AppHandle, file_path: PathBuf) -> Result<Uploade
417437
file_key: file_key.clone(),
418438
aws_bucket: s3_config.aws_bucket.clone(),
419439
aws_region: s3_config.aws_region.clone(),
440+
aws_endpoint: s3_config.aws_endpoint.clone(),
420441
},
421442
)?;
422443

@@ -717,6 +738,7 @@ pub async fn upload_individual_file(
717738
file_key: file_key.clone(),
718739
aws_bucket: s3_config.aws_bucket.clone(),
719740
aws_region: s3_config.aws_region.clone(),
741+
aws_endpoint: s3_config.aws_endpoint.clone(),
720742
};
721743

722744
let (upload_url, mut form) = if is_audio {
@@ -783,6 +805,7 @@ async fn prepare_screenshot_upload(
783805
file_key: file_key.clone(),
784806
aws_bucket: s3_config.aws_bucket.clone(),
785807
aws_region: s3_config.aws_region.clone(),
808+
aws_endpoint: s3_config.aws_endpoint.clone(),
786809
},
787810
};
788811

apps/desktop/src/utils/tauri.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,8 @@ export type RequestRestartRecording = null
285285
export type RequestStartRecording = null
286286
export type RequestStopRecording = null
287287
export type Resolution = { width: number; height: number }
288-
export type S3UploadMeta = { id: string; user_id: string; aws_region?: string; aws_bucket?: string }
289-
export type ScreenCaptureTarget = ({ variant: "window" } & CaptureWindow) | ({ variant: "screen" } & CaptureScreen) | ({ variant: "area" } & CaptureArea)
288+
export type S3UploadMeta = { id: string; user_id: string; aws_region?: string; aws_bucket?: string; aws_endpoint?: string }
289+
export type ScreenCaptureTarget = ({ variant: "window" } & CaptureWindow) | ({ variant: "screen" } & CaptureScreen)
290290
export type SegmentRecordings = { display: Video; camera: Video | null; audio: Audio | null }
291291
export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordings; path: string; prettyName: string }
292292
export type SharingMeta = { id: string; link: string }

apps/embed/app/view/[videoId]/_components/ShareVideo.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useRouter } from "next/navigation";
1919
import toast from "react-hot-toast";
2020
import moment from "moment";
2121
import { Toolbar } from "./Toolbar";
22+
import { S3_BUCKET_URL } from "@cap/utils";
2223

2324
declare global {
2425
interface Window {
@@ -297,7 +298,7 @@ export const ShareVideo = ({
297298
transcriptionUrl = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&fileType=transcription`;
298299
} else {
299300
// For default Cap storage
300-
transcriptionUrl = `https://v.cap.so/${data.ownerId}/${data.id}/transcription.vtt`;
301+
transcriptionUrl = `${S3_BUCKET_URL}/${data.ownerId}/${data.id}/transcription.vtt`;
301302
}
302303

303304
try {
@@ -443,8 +444,8 @@ export const ShareVideo = ({
443444
data.source.type === "MediaConvert")
444445
? `${process.env.NEXT_PUBLIC_URL}/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=master`
445446
: data.source.type === "MediaConvert"
446-
? `https://v.cap.so/${data.ownerId}/${data.id}/output/video_recording_000.m3u8`
447-
: `https://v.cap.so/${data.ownerId}/${data.id}/combined-source/stream.m3u8`
447+
? `${S3_BUCKET_URL}/${data.ownerId}/${data.id}/output/video_recording_000.m3u8`
448+
: `${S3_BUCKET_URL}/${data.ownerId}/${data.id}/combined-source/stream.m3u8`
448449
}
449450
/>
450451
</div>

apps/embed/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "embed",
2+
"name": "@cap/embed",
33
"version": "0.3.1",
44
"private": true,
55
"scripts": {
@@ -48,4 +48,4 @@
4848
"engines": {
4949
"node": "20"
5050
}
51-
}
51+
}

apps/storybook/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"private": true,
44
"type": "module",
55
"scripts": {
6-
"dev": "storybook dev -p 6006",
7-
"build": "storybook build"
6+
"dev:storybook": "storybook dev -p 6006",
7+
"build:storybook": "storybook build"
88
},
99
"dependencies": {
1010
"@cap/ui-solid": "workspace:*",
@@ -26,4 +26,4 @@
2626
"vite": "^5.3.4",
2727
"vite-plugin-solid": "^2.10.2"
2828
}
29-
}
29+
}

apps/tasks/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "tasks",
2+
"name": "@cap/tasks",
33
"version": "0.3.1",
44
"private": true,
55
"main": "src/index.ts",
@@ -44,4 +44,4 @@
4444
"engines": {
4545
"node": "20"
4646
}
47-
}
47+
}

apps/web/app/api/playlist/route.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "@/utils/video/ffmpeg/helpers";
1616
import { getHeaders, CACHE_CONTROL_HEADERS } from "@/utils/helpers";
1717
import { createS3Client, getS3Bucket } from "@/utils/s3";
18+
import { S3_BUCKET_URL } from "@cap/utils";
1819

1920
export const revalidate = 3599;
2021

@@ -87,7 +88,7 @@ export async function GET(request: NextRequest) {
8788
status: 302,
8889
headers: {
8990
...getHeaders(origin),
90-
Location: `https://v.cap.so/${userId}/${videoId}/result.mp4`,
91+
Location: `${S3_BUCKET_URL}/${userId}/${videoId}/result.mp4`,
9192
...CACHE_CONTROL_HEADERS,
9293
},
9394
});
@@ -98,13 +99,13 @@ export async function GET(request: NextRequest) {
9899
status: 302,
99100
headers: {
100101
...getHeaders(origin),
101-
Location: `https://v.cap.so/${userId}/${videoId}/output/video_recording_000.m3u8`,
102+
Location: `${S3_BUCKET_URL}/${userId}/${videoId}/output/video_recording_000.m3u8`,
102103
...CACHE_CONTROL_HEADERS,
103104
},
104105
});
105106
}
106107

107-
const playlistUrl = `https://v.cap.so/${userId}/${videoId}/combined-source/stream.m3u8`;
108+
const playlistUrl = `${S3_BUCKET_URL}/${userId}/${videoId}/combined-source/stream.m3u8`;
108109
return new Response(null, {
109110
status: 302,
110111
headers: {
@@ -141,7 +142,10 @@ export async function GET(request: NextRequest) {
141142
} catch (error) {
142143
console.error("Error fetching transcription file:", error);
143144
return new Response(
144-
JSON.stringify({ error: true, message: "Transcription file not found" }),
145+
JSON.stringify({
146+
error: true,
147+
message: "Transcription file not found",
148+
}),
145149
{
146150
status: 404,
147151
headers: getHeaders(origin),

apps/web/app/api/screenshot/route.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ import { type NextRequest } from "next/server";
22
import { db } from "@cap/database";
33
import { s3Buckets, videos } from "@cap/database/schema";
44
import { eq } from "drizzle-orm";
5-
import { S3Client, ListObjectsV2Command, GetObjectCommand } from "@aws-sdk/client-s3";
5+
import {
6+
S3Client,
7+
ListObjectsV2Command,
8+
GetObjectCommand,
9+
} from "@aws-sdk/client-s3";
610
import { getCurrentUser } from "@cap/database/auth/session";
711
import { getHeaders } from "@/utils/helpers";
812
import { createS3Client, getS3Bucket } from "@/utils/s3";
913
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
14+
import { S3_BUCKET_URL } from "@cap/utils";
1015

1116
export const revalidate = 0;
1217

@@ -99,13 +104,13 @@ export async function GET(request: NextRequest) {
99104
screenshotUrl = await getSignedUrl(
100105
s3Client,
101106
new GetObjectCommand({
102-
Bucket,
103-
Key: screenshot.Key
107+
Bucket,
108+
Key: screenshot.Key,
104109
}),
105110
{ expiresIn: 3600 }
106111
);
107112
} else {
108-
screenshotUrl = `https://v.cap.so/${screenshot.Key}`;
113+
screenshotUrl = `${S3_BUCKET_URL}/${screenshot.Key}`;
109114
}
110115

111116
return new Response(JSON.stringify({ url: screenshotUrl }), {

apps/web/app/api/thumbnail/route.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { eq } from "drizzle-orm";
66
import { s3Buckets, videos } from "@cap/database/schema";
77
import { createS3Client, getS3Bucket } from "@/utils/s3";
88
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
9+
import { S3_BUCKET_URL } from "@cap/utils";
910

1011
export const revalidate = 0;
1112

@@ -63,8 +64,11 @@ export async function GET(request: NextRequest) {
6364

6465
let thumbnailUrl: string;
6566

66-
if (!result.bucket || video.awsBucket === process.env.NEXT_PUBLIC_CAP_AWS_BUCKET) {
67-
thumbnailUrl = `https://v.cap.so/${prefix}screenshot/screen-capture.jpg`;
67+
if (
68+
!result.bucket ||
69+
video.awsBucket === process.env.NEXT_PUBLIC_CAP_AWS_BUCKET
70+
) {
71+
thumbnailUrl = `${S3_BUCKET_URL}/${prefix}screenshot/screen-capture.jpg`;
6872
return new Response(JSON.stringify({ screen: thumbnailUrl }), {
6973
status: 200,
7074
headers: getHeaders(origin),
@@ -83,7 +87,7 @@ export async function GET(request: NextRequest) {
8387
const listResponse = await s3Client.send(listCommand);
8488
const contents = listResponse.Contents || [];
8589

86-
const thumbnailKey = contents.find((item) =>
90+
const thumbnailKey = contents.find((item) =>
8791
item.Key?.endsWith("screen-capture.jpg")
8892
)?.Key;
8993

@@ -104,7 +108,7 @@ export async function GET(request: NextRequest) {
104108
s3Client,
105109
new GetObjectCommand({
106110
Bucket,
107-
Key: thumbnailKey
111+
Key: thumbnailKey,
108112
}),
109113
{ expiresIn: 3600 }
110114
);

apps/web/app/api/upload/signed/route.ts

+8
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ export async function POST(request: NextRequest) {
143143
}
144144
);
145145

146+
// When not using aws s3, we need to transform the url to the local endpoint
147+
if (process.env.NEXT_PUBLIC_CAP_AWS_ENDPOINT) {
148+
const endpoint = process.env.NEXT_PUBLIC_CAP_AWS_ENDPOINT;
149+
const bucket = process.env.NEXT_PUBLIC_CAP_AWS_BUCKET;
150+
const newUrl = `${endpoint}/${bucket}/`;
151+
presignedPostData.url = newUrl;
152+
}
153+
146154
console.log("Presigned URL created successfully");
147155

148156
// After successful presigned URL creation, trigger revalidation

apps/web/app/api/video/individual/route.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ListObjectsV2Command } from "@aws-sdk/client-s3";
66
import { getCurrentUser } from "@cap/database/auth/session";
77
import { getHeaders } from "@/utils/helpers";
88
import { createS3Client, getS3Bucket } from "@/utils/s3";
9+
import { S3_BUCKET_URL } from "@cap/utils";
910

1011
export const revalidate = 3599;
1112

@@ -93,7 +94,7 @@ export async function GET(request: NextRequest) {
9394
const fileName = key.split("/").pop();
9495
return {
9596
fileName,
96-
url: `https://v.cap.so/${key}`,
97+
url: `${S3_BUCKET_URL}/${key}`,
9798
};
9899
});
99100

apps/web/app/api/video/playlistUrl/route.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { videos } from "@cap/database/schema";
44
import { eq } from "drizzle-orm";
55
import { getHeaders } from "@/utils/helpers";
66
import { CACHE_CONTROL_HEADERS } from "@/utils/helpers";
7+
import { S3_BUCKET_URL } from "@cap/utils";
78

89
export const revalidate = 0;
910

@@ -59,7 +60,7 @@ export async function GET(request: NextRequest) {
5960
}
6061

6162
if (video.jobStatus === "COMPLETE") {
62-
const playlistUrl = `https://v.cap.so/${video.ownerId}/${video.id}/output/video_recording_000_output.m3u8`;
63+
const playlistUrl = `${S3_BUCKET_URL}/${video.ownerId}/${video.id}/output/video_recording_000_output.m3u8`;
6364
return new Response(
6465
JSON.stringify({ playlistOne: playlistUrl, playlistTwo: null }),
6566
{

0 commit comments

Comments
 (0)