S3 multipart upload for large videos, EISDIR fix

This commit is contained in:
CallMeVerity
2026-06-03 03:30:54 +01:00
parent ba9752d33c
commit dc021e4856
4 changed files with 217 additions and 43 deletions
+75 -12
View File
@@ -13,7 +13,10 @@ import {
uploadObject,
deleteObject,
getObjectUrl,
getPresignedUploadUrl,
createMultipartUpload,
getPresignedUploadPartUrl,
completeMultipartUpload,
abortMultipartUpload,
} from "../services/rustfs";
import { parseMtvFile } from "../services/mtv-parser";
import { getMapInfo } from "../services/momentum-api";
@@ -75,14 +78,23 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" })
const videoFileName = formData.get("videoFileName") as string | null;
const videoContentType =
(formData.get("videoContentType") as string | null) || "video/webm";
const videoFileSizeStr = formData.get("videoFileSize") as string | null;
const runDate = formData.get("runDate") as string | null;
if (!mtv || !videoFileName) {
if (!mtv || !videoFileName || !videoFileSizeStr) {
return status(400, {
error: "mtv file and videoFileName are required",
error: "mtv file, videoFileName, and videoFileSize are required",
});
}
const videoFileSize = parseInt(videoFileSizeStr, 10);
if (isNaN(videoFileSize) || videoFileSize <= 0) {
return status(400, { error: "Invalid videoFileSize" });
}
const CHUNK_SIZE = 80 * 1024 * 1024;
const numParts = Math.ceil(videoFileSize / CHUNK_SIZE);
const mtvBuffer = await mtv.arrayBuffer();
const metadata = parseMtvFile(mtvBuffer);
if (!metadata) {
@@ -176,13 +188,30 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" })
updateVideoPB(id, entry, previousPbs);
const presignedUrls = {
video: await getPresignedUploadUrl(videoKey, videoContentType),
};
const { uploadId } = await createMultipartUpload(
videoKey,
videoContentType,
);
const parts = await Promise.all(
Array.from({ length: numParts }, (_, i) => i + 1).map(
async (partNumber) => ({
partNumber,
url: await getPresignedUploadPartUrl(
videoKey,
uploadId,
partNumber,
),
}),
),
);
return {
...entry,
presignedUrls,
presignedUrls: {
parts,
uploadId,
},
videoUrl: getObjectUrl(videoKey),
mtvUrl: getObjectUrl(mtvKey),
thumbnailUrl: thumbnailKey
@@ -251,23 +280,57 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" })
insertVideo(entry);
const presignedUrls = {
video: await getPresignedUploadUrl(videoKey, videoContentType),
};
const { uploadId } = await createMultipartUpload(
videoKey,
videoContentType,
);
const parts = await Promise.all(
Array.from({ length: numParts }, (_, i) => i + 1).map(
async (partNumber) => ({
partNumber,
url: await getPresignedUploadPartUrl(
videoKey,
uploadId,
partNumber,
),
}),
),
);
return {
...entry,
presignedUrls,
presignedUrls: {
parts,
uploadId,
},
videoUrl: getObjectUrl(videoKey),
mtvUrl: getObjectUrl(mtvKey),
thumbnailUrl: thumbnailKey ? getObjectUrl(thumbnailKey) : undefined,
};
})
.post("/:id/complete", async ({ params: { id } }) => {
.post("/:id/complete", async ({ params: { id }, body }) => {
const video = getVideoById(id);
if (!video) return status(404, { error: "Not found" });
const { parts, uploadId } = body as {
parts: { partNumber: number; eTag: string }[];
uploadId: string;
};
if (!parts || !Array.isArray(parts) || !uploadId) {
return status(400, {
error: "parts array and uploadId are required",
});
}
const s3Parts = parts.map((p) => ({
PartNumber: p.partNumber,
ETag: p.eTag,
}));
await completeMultipartUpload(video.videoKey, uploadId, s3Parts);
return {
...video,
videoUrl: getObjectUrl(video.videoKey),