Skip to content

Commit 0ebdda6

Browse files
authored
Allow specifying internal S3 endpoint (#685)
* start supporting internal s3 endpoint * completely rip out createS3Client and the like * fix endpoints * fix icon uploading * handle already loaded videos * cleanup * remove loadedmedtadata dispatchevent * put md5 hash back * fix file key parsing * fix complete upload * remove leading slash * clippy
1 parent 9083654 commit 0ebdda6

File tree

35 files changed

+791
-944
lines changed

35 files changed

+791
-944
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,8 @@ async fn upload_exported_video(
11061106
Ok(UploadResult::Success(uploaded_video.link))
11071107
}
11081108
Err(e) => {
1109+
error!("Failed to upload video: {e}");
1110+
11091111
NotificationType::UploadFailed.send(&app);
11101112
Err(e)
11111113
}

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

Lines changed: 20 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,6 @@ use specta::Type;
3535
#[derive(Deserialize, Serialize, Clone, Type, Debug)]
3636
pub struct S3UploadMeta {
3737
id: String,
38-
user_id: String,
39-
#[serde(default)]
40-
aws_region: String,
41-
#[serde(default, deserialize_with = "deserialize_empty_object_as_string")]
42-
aws_bucket: String,
43-
#[serde(default)]
44-
aws_endpoint: String,
4538
}
4639

4740
fn deserialize_empty_object_as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
@@ -88,62 +81,16 @@ impl S3UploadMeta {
8881
&self.id
8982
}
9083

91-
pub fn user_id(&self) -> &str {
92-
&self.user_id
93-
}
94-
95-
pub fn aws_region(&self) -> &str {
96-
&self.aws_region
97-
}
98-
99-
pub fn aws_bucket(&self) -> &str {
100-
&self.aws_bucket
101-
}
102-
103-
pub fn aws_endpoint(&self) -> &str {
104-
&self.aws_endpoint
105-
}
106-
107-
pub fn new(
108-
id: String,
109-
user_id: String,
110-
aws_region: String,
111-
aws_bucket: String,
112-
aws_endpoint: String,
113-
) -> Self {
114-
Self {
115-
id,
116-
user_id,
117-
aws_region,
118-
aws_bucket,
119-
aws_endpoint,
120-
}
121-
}
122-
123-
pub fn ensure_defaults(&mut self) {
124-
if self.aws_region.is_empty() {
125-
self.aws_region = std::env::var("NEXT_PUBLIC_CAP_AWS_REGION")
126-
.unwrap_or_else(|_| "us-east-1".to_string());
127-
}
128-
if self.aws_bucket.is_empty() {
129-
self.aws_bucket =
130-
std::env::var("NEXT_PUBLIC_CAP_AWS_BUCKET").unwrap_or_else(|_| "capso".to_string());
131-
}
132-
if self.aws_endpoint.is_empty() {
133-
self.aws_endpoint = std::env::var("NEXT_PUBLIC_CAP_AWS_ENDPOINT")
134-
.unwrap_or_else(|_| "https://s3.amazonaws.com".to_string());
135-
}
84+
pub fn new(id: String) -> Self {
85+
Self { id }
13686
}
13787
}
13888

13989
#[derive(serde::Serialize)]
14090
#[serde(rename_all = "camelCase")]
14191
struct S3UploadBody {
142-
user_id: String,
143-
file_key: String,
144-
aws_bucket: String,
145-
aws_region: String,
146-
aws_endpoint: String,
92+
video_id: String,
93+
subpath: String,
14794
}
14895

14996
#[derive(serde::Serialize)]
@@ -201,33 +148,17 @@ pub async fn upload_video(
201148
) -> Result<UploadedVideo, String> {
202149
println!("Uploading video {video_id}...");
203150

204-
let file_name = file_path
205-
.file_name()
206-
.and_then(|name| name.to_str())
207-
.ok_or("Invalid file path")?
208-
.to_string();
209-
210151
let client = reqwest::Client::new();
211152
let s3_config = match existing_config {
212153
Some(config) => config,
213-
None => create_or_get_video(app, false, Some(video_id), None).await?,
154+
None => create_or_get_video(app, false, Some(video_id.clone()), None).await?,
214155
};
215156

216-
let file_key = format!(
217-
"{}/{}/{}",
218-
s3_config.user_id(),
219-
s3_config.id(),
220-
"result.mp4"
221-
);
222-
223157
let body = build_video_upload_body(
224158
&file_path,
225159
S3UploadBody {
226-
user_id: s3_config.user_id().to_string(),
227-
file_key: file_key.clone(),
228-
aws_bucket: s3_config.aws_bucket().to_string(),
229-
aws_region: s3_config.aws_region().to_string(),
230-
aws_endpoint: s3_config.aws_endpoint().to_string(),
160+
video_id: video_id.clone(),
161+
subpath: "result.mp4".to_string(),
231162
},
232163
)?;
233164

@@ -336,17 +267,10 @@ pub async fn upload_image(app: &AppHandle, file_path: PathBuf) -> Result<Uploade
336267
let client = reqwest::Client::new();
337268
let s3_config = create_or_get_video(app, true, None, None).await?;
338269

339-
let file_key = format!("{}/{}/{}", s3_config.user_id, s3_config.id, file_name);
340-
341-
println!("File key: {file_key}");
342-
343270
let body = S3ImageUploadBody {
344271
base: S3UploadBody {
345-
user_id: s3_config.user_id,
346-
file_key: file_key.clone(),
347-
aws_bucket: s3_config.aws_bucket,
348-
aws_region: s3_config.aws_region,
349-
aws_endpoint: s3_config.aws_endpoint,
272+
video_id: s3_config.id.clone(),
273+
subpath: file_name,
350274
},
351275
};
352276

@@ -427,7 +351,6 @@ pub async fn create_or_get_video(
427351
)
428352
})?;
429353

430-
config.ensure_defaults();
431354
Ok(config)
432355
}
433356

@@ -532,18 +455,10 @@ pub async fn prepare_screenshot_upload(
532455
s3_config: &S3UploadMeta,
533456
screenshot_path: PathBuf,
534457
) -> Result<reqwest::Response, String> {
535-
let file_key = format!(
536-
"{}/{}/screenshot/screen-capture.jpg",
537-
s3_config.user_id, s3_config.id
538-
);
539-
540458
let body = S3ImageUploadBody {
541459
base: S3UploadBody {
542-
user_id: s3_config.user_id.clone(),
543-
file_key: file_key.clone(),
544-
aws_bucket: s3_config.aws_bucket.clone(),
545-
aws_region: s3_config.aws_region.clone(),
546-
aws_endpoint: s3_config.aws_endpoint.clone(),
460+
video_id: s3_config.id.clone(),
461+
subpath: "screenshot/screen-capture.jpg".to_string(),
547462
},
548463
};
549464

@@ -632,10 +547,8 @@ impl InstantMultipartUpload {
632547
// --------------------------------------------
633548
// basic constants and info for chunk approach
634549
// --------------------------------------------
635-
let file_name = "result.mp4";
636550
let client = reqwest::Client::new();
637551
let s3_config = pre_created_video.config;
638-
let file_key = format!("{}/{}/{}", s3_config.user_id(), s3_config.id(), file_name);
639552

640553
let mut uploaded_parts = Vec::new();
641554
let mut part_number = 1;
@@ -652,7 +565,7 @@ impl InstantMultipartUpload {
652565
c.post(url)
653566
.header("Content-Type", "application/json")
654567
.json(&serde_json::json!({
655-
"fileKey": file_key,
568+
"videoId": s3_config.id(),
656569
"contentType": "video/mp4"
657570
}))
658571
})
@@ -689,6 +602,7 @@ impl InstantMultipartUpload {
689602
return Err("No uploadId returned from initiate endpoint".to_string());
690603
}
691604
};
605+
692606
if upload_id.is_empty() {
693607
return Err("Empty uploadId returned from initiate endpoint".to_string());
694608
}
@@ -747,7 +661,7 @@ impl InstantMultipartUpload {
747661
&app,
748662
&client,
749663
&file_path,
750-
&file_key,
664+
s3_config.id(),
751665
&upload_id,
752666
&mut part_number,
753667
&mut last_uploaded_position,
@@ -774,7 +688,7 @@ impl InstantMultipartUpload {
774688
&app,
775689
&client,
776690
&file_path,
777-
&file_key,
691+
s3_config.id(),
778692
&upload_id,
779693
&mut 1,
780694
&mut 0,
@@ -800,10 +714,9 @@ impl InstantMultipartUpload {
800714
Self::finalize_upload(
801715
&app,
802716
&file_path,
803-
&file_key,
717+
&s3_config.id(),
804718
&upload_id,
805719
&uploaded_parts,
806-
&video_id,
807720
)
808721
.await?;
809722

@@ -825,7 +738,7 @@ impl InstantMultipartUpload {
825738
app: &AppHandle,
826739
client: &reqwest::Client,
827740
file_path: &PathBuf,
828-
file_key: &str,
741+
video_id: &str,
829742
upload_id: &str,
830743
part_number: &mut i32,
831744
last_uploaded_position: &mut u64,
@@ -929,7 +842,7 @@ impl InstantMultipartUpload {
929842
c.post(url)
930843
.header("Content-Type", "application/json")
931844
.json(&serde_json::json!({
932-
"fileKey": file_key,
845+
"videoId": video_id,
933846
"uploadId": upload_id,
934847
"partNumber": *part_number,
935848
"md5Sum": &md5_sum
@@ -1069,10 +982,9 @@ impl InstantMultipartUpload {
1069982
async fn finalize_upload(
1070983
app: &AppHandle,
1071984
file_path: &PathBuf,
1072-
file_key: &str,
985+
video_id: &str,
1073986
upload_id: &str,
1074987
uploaded_parts: &[UploadedPart],
1075-
video_id: &str,
1076988
) -> Result<(), String> {
1077989
println!(
1078990
"Completing multipart upload with {} parts",
@@ -1106,7 +1018,7 @@ impl InstantMultipartUpload {
11061018
c.post(url)
11071019
.header("Content-Type", "application/json")
11081020
.json(&serde_json::json!({
1109-
"fileKey": file_key,
1021+
"videoId": video_id,
11101022
"uploadId": upload_id,
11111023
"parts": uploaded_parts
11121024
}))

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

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ async fn do_authed_request(
1414
) -> Result<reqwest::Response, reqwest::Error> {
1515
let client = reqwest::Client::new();
1616

17-
let mut req = build(client, url).header(
18-
"Authorization",
19-
format!(
20-
"Bearer {}",
21-
match &auth.secret {
22-
AuthSecret::ApiKey { api_key } => api_key,
23-
AuthSecret::Session { token, .. } => token,
24-
}
25-
),
26-
);
17+
let mut req = build(client, url)
18+
.header(
19+
"Authorization",
20+
format!(
21+
"Bearer {}",
22+
match &auth.secret {
23+
AuthSecret::ApiKey { api_key } => api_key,
24+
AuthSecret::Session { token, .. } => token,
25+
}
26+
),
27+
)
28+
.header("X-Desktop-Version", env!("CARGO_PKG_VERSION"));
2729

2830
if let Some(s) = std::option_env!("VITE_VERCEL_AUTOMATION_BYPASS_SECRET") {
2931
req = req.header("x-vercel-protection-bypass", s);

apps/desktop/src/utils/tauri.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ export type RequestOpenSettings = { page: string }
363363
export type RequestRestartRecording = null
364364
export type RequestStartRecording = null
365365
export type RequestStopRecording = null
366-
export type S3UploadMeta = { id: string; user_id: string; aws_region?: string; aws_bucket?: string; aws_endpoint?: string }
366+
export type S3UploadMeta = { id: string }
367367
export type ScreenCaptureTarget = { variant: "window"; id: number } | { variant: "screen"; id: number } | { variant: "area"; screen: number; bounds: Bounds }
368368
export type SegmentRecordings = { display: Video; camera: Video | null; mic: Audio | null; system_audio: Audio | null }
369369
export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordings; path: string }

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

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { spaces, users, spaceMembers } from "@cap/database/schema";
66
import { inArray, eq, and } from "drizzle-orm";
77
import { nanoId, nanoIdLength } from "@cap/database/helpers";
88
import { revalidatePath } from "next/cache";
9-
import { createS3Client, getS3Bucket } from "@/utils/s3";
10-
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
9+
import { createBucketProvider } from "@/utils/s3";
1110
import { serverEnv } from "@cap/env";
1211
import { v4 as uuidv4 } from "uuid";
1312

@@ -52,6 +51,7 @@ export async function createSpace(
5251
)
5352
)
5453
.limit(1);
54+
5555
if (existingSpace.length > 0) {
5656
return {
5757
success: false,
@@ -89,46 +89,22 @@ export async function createSpace(
8989
user.activeOrganizationId
9090
}/spaces/${spaceId}/icon-${Date.now()}.${fileExtension}`;
9191

92-
// Get S3 client
93-
const [s3Client] = await createS3Client();
94-
const bucketName = await getS3Bucket();
95-
96-
// Create presigned post
97-
const presignedPostData = await createPresignedPost(s3Client, {
98-
Bucket: bucketName,
99-
Key: fileKey,
100-
Fields: {
101-
"Content-Type": iconFile.type,
102-
},
103-
Expires: 600, // 10 minutes
104-
});
105-
106-
// Upload file to S3
107-
const formDataForS3 = new FormData();
108-
Object.entries(presignedPostData.fields).forEach(([key, value]) => {
109-
formDataForS3.append(key, value as string);
110-
});
111-
formDataForS3.append("file", iconFile);
92+
const bucket = await createBucketProvider();
11293

113-
const uploadResponse = await fetch(presignedPostData.url, {
114-
method: "POST",
115-
body: formDataForS3,
94+
await bucket.putObject(fileKey, await iconFile.bytes(), {
95+
contentType: iconFile.type,
11696
});
11797

118-
if (!uploadResponse.ok) {
119-
throw new Error("Failed to upload file to S3");
120-
}
121-
12298
// Construct the icon URL
12399
if (serverEnv().CAP_AWS_BUCKET_URL) {
124100
// If a custom bucket URL is defined, use it
125101
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
126102
} else if (serverEnv().CAP_AWS_ENDPOINT) {
127103
// For custom endpoints like MinIO
128-
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucketName}/${fileKey}`;
104+
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.name}/${fileKey}`;
129105
} else {
130106
// Default AWS S3 URL format
131-
iconUrl = `https://${bucketName}.s3.${
107+
iconUrl = `https://${bucket.name}.s3.${
132108
serverEnv().CAP_AWS_REGION || "us-east-1"
133109
}.amazonaws.com/${fileKey}`;
134110
}

0 commit comments

Comments
 (0)