S3 multipart upload for large videos, EISDIR fix
This commit is contained in:
@@ -26,10 +26,16 @@ export async function fetchVideo(id: string): Promise<VideoDetail> {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface UploadPartUrl {
|
||||
partNumber: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface UploadInitResponse {
|
||||
id: string;
|
||||
presignedUrls: {
|
||||
video: string;
|
||||
parts: UploadPartUrl[];
|
||||
uploadId: string;
|
||||
};
|
||||
videoUrl: string;
|
||||
mtvUrl: string;
|
||||
@@ -45,12 +51,14 @@ export async function uploadVideoInit(
|
||||
mtvFile: File,
|
||||
videoFileName: string,
|
||||
videoContentType: string,
|
||||
videoFileSize: string,
|
||||
runDate?: string,
|
||||
): Promise<UploadInitResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append("mtv", mtvFile);
|
||||
formData.append("videoFileName", videoFileName);
|
||||
formData.append("videoContentType", videoContentType);
|
||||
formData.append("videoFileSize", videoFileSize);
|
||||
if (runDate) formData.append("runDate", runDate);
|
||||
|
||||
const res = await fetch(`${API_BASE}/videos/upload-url`, {
|
||||
@@ -67,10 +75,18 @@ export async function uploadVideoInit(
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function uploadVideoComplete(id: string): Promise<VideoDetail> {
|
||||
export async function uploadVideoComplete(
|
||||
id: string,
|
||||
parts: { partNumber: number; eTag: string }[],
|
||||
uploadId: string,
|
||||
): Promise<VideoDetail> {
|
||||
const res = await fetch(`${API_BASE}/videos/${id}/complete`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
headers: {
|
||||
...authHeaders(),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ parts, uploadId }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.status === 401)
|
||||
|
||||
@@ -252,42 +252,67 @@ export default function UploadForm() {
|
||||
mtvFile,
|
||||
videoFile.name,
|
||||
videoFile.type || "video/webm",
|
||||
videoFile.size.toString(),
|
||||
runDate,
|
||||
);
|
||||
|
||||
const { parts, uploadId } = initRes.presignedUrls;
|
||||
const totalSize = videoFile.size;
|
||||
const CHUNK_SIZE = 80 * 1024 * 1024;
|
||||
const uploadedParts: { partNumber: number; eTag: string }[] = [];
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
if (e.lengthComputable) {
|
||||
setProgress(
|
||||
5 + Math.round((e.loaded / totalSize) * 90),
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]!;
|
||||
const start = i * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, totalSize);
|
||||
const chunk = videoFile.slice(start, end);
|
||||
|
||||
const eTag = await new Promise<string>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const chunkProgress = e.loaded / e.total;
|
||||
const overallUploaded = start + e.loaded;
|
||||
setProgress(
|
||||
5 +
|
||||
Math.round(
|
||||
(overallUploaded / totalSize) * 90,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
xhr.addEventListener("load", () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
const etag = xhr.getResponseHeader("ETag");
|
||||
if (etag) resolve(etag);
|
||||
else
|
||||
reject(
|
||||
new Error("No ETag returned from upload"),
|
||||
);
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Upload failed with status ${xhr.status}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
xhr.addEventListener("error", () =>
|
||||
reject(new Error("Upload failed")),
|
||||
);
|
||||
xhr.open("PUT", part.url);
|
||||
xhr.setRequestHeader(
|
||||
"Content-Type",
|
||||
videoFile.type || "video/webm",
|
||||
);
|
||||
xhr.send(chunk);
|
||||
});
|
||||
xhr.addEventListener("load", () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) resolve();
|
||||
else
|
||||
reject(
|
||||
new Error(
|
||||
`Upload failed with status ${xhr.status}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
xhr.addEventListener("error", () =>
|
||||
reject(new Error("Upload failed")),
|
||||
);
|
||||
xhr.open("PUT", initRes.presignedUrls.video);
|
||||
xhr.setRequestHeader(
|
||||
"Content-Type",
|
||||
videoFile.type || "video/webm",
|
||||
);
|
||||
xhr.send(videoFile);
|
||||
});
|
||||
|
||||
uploadedParts.push({ partNumber: part.partNumber, eTag });
|
||||
}
|
||||
|
||||
setProgress(98);
|
||||
await uploadVideoComplete(initRes.id);
|
||||
await uploadVideoComplete(initRes.id, uploadedParts, uploadId);
|
||||
setProgress(100);
|
||||
|
||||
setSuccess(true);
|
||||
|
||||
Reference in New Issue
Block a user