async function uploadMultipart(projectId: string, file: File, name: string, platform: string) {
const token = process.env.NUNU_API_TOKEN;
// Step 1: Initiate
const initRes = await fetch("https://nunu.ai/api/v1/builds/upload", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": token },
body: JSON.stringify({
project_id: projectId, name, file_name: file.name,
file_size: file.size, platform
}),
});
const { upload_id, build_id, object_key, total_parts, part_size } = await initRes.json();
// Step 2 & 3: Get URLs and upload parts
const parts = [];
const concurrency = 3;
for (let i = 0; i < total_parts; i += concurrency) {
const partNumbers = Array.from(
{ length: Math.min(concurrency, total_parts - i) },
(_, j) => i + j + 1
);
// Get URLs
const urlRes = await fetch(
`https://nunu.ai/api/v1/builds/upload/parts?` +
new URLSearchParams({ upload_id, object_key, part_numbers: partNumbers.join(",") })
);
const { upload_urls } = await urlRes.json();
// Upload parts in parallel
const uploaded = await Promise.all(
upload_urls.map(async ({ part_number, url }) => {
const start = (part_number - 1) * part_size;
const chunk = file.slice(start, Math.min(start + part_size, file.size));
const res = await fetch(url, {
method: "PUT", body: chunk,
headers: { "Content-Type": "application/octet-stream" }
});
return { part_number, etag: res.headers.get("etag")?.replace(/"/g, "") };
})
);
parts.push(...uploaded);
console.log(`Uploaded ${parts.length}/${total_parts} parts`);
}
// Step 4: Complete
await fetch("https://nunu.ai/api/v1/builds/upload/complete", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": token },
body: JSON.stringify({ build_id, upload_id, object_key, parts }),
});
return build_id;
}