Skip to content

Commit 312fed3

Browse files
committed
Add batch presigned upload endpoint
1 parent 9604265 commit 312fed3

1 file changed

Lines changed: 70 additions & 0 deletions

File tree

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,78 @@ import { stringOrNumberOptional } from "@/utils/zod";
2020
import { withAuth } from "../../utils";
2121
import { parseVideoIdOrFileKey } from "../utils";
2222

23+
function contentTypeForSubpath(subpath: string): string {
24+
if (subpath.endsWith(".json")) return "application/json";
25+
if (subpath.endsWith(".mp4") || subpath.endsWith(".m4s")) return "video/mp4";
26+
if (subpath.endsWith(".jpg") || subpath.endsWith(".jpeg"))
27+
return "image/jpeg";
28+
if (subpath.endsWith(".aac")) return "audio/aac";
29+
if (subpath.endsWith(".webm")) return "audio/webm";
30+
if (subpath.endsWith(".m3u8")) return "application/x-mpegURL";
31+
return "application/octet-stream";
32+
}
33+
2334
export const app = new Hono().use(withAuth);
2435

36+
app.post(
37+
"/batch",
38+
zValidator(
39+
"json",
40+
z.object({
41+
videoId: z.string(),
42+
subpaths: z
43+
.array(
44+
z
45+
.string()
46+
.refine(
47+
(s) => !s.includes("..") && !s.startsWith("/"),
48+
"Invalid subpath",
49+
),
50+
)
51+
.min(1)
52+
.max(50),
53+
}),
54+
),
55+
async (c) => {
56+
const user = c.get("user");
57+
const { videoId, subpaths } = c.req.valid("json");
58+
59+
try {
60+
const [customBucket] = await db()
61+
.select()
62+
.from(Db.s3Buckets)
63+
.where(eq(Db.s3Buckets.ownerId, user.id));
64+
65+
const urls = await Effect.gen(function* () {
66+
const [bucket] = yield* S3Buckets.getBucketAccess(
67+
Option.fromNullable(customBucket?.id),
68+
);
69+
70+
const entries = yield* Effect.all(
71+
subpaths.map((subpath) => {
72+
const fileKey = `${user.id}/${videoId}/${subpath}`;
73+
return bucket
74+
.getPresignedPutUrl(
75+
fileKey,
76+
{ ContentType: contentTypeForSubpath(subpath) },
77+
{ expiresIn: 1800 },
78+
)
79+
.pipe(Effect.map((url) => [subpath, url] as const));
80+
}),
81+
{ concurrency: "unbounded" },
82+
);
83+
84+
return Object.fromEntries(entries);
85+
}).pipe(runPromise);
86+
87+
return c.json({ urls });
88+
} catch (error) {
89+
console.error("Batch signed URL generation failed:", error);
90+
return c.json({ error: "Internal server error" }, 500);
91+
}
92+
},
93+
);
94+
2595
app.post(
2696
"/",
2797
zValidator(

0 commit comments

Comments
 (0)