SDK — Uploads
The SDK gives you two upload surfaces:
apulodi.files.upload()— high-level; pick your input, done.apulodi.uploads— explicit multipart control for custom flows.
High-level upload
import { readFile } from "node:fs/promises";
const data = await readFile("avatar.jpg");
const file = await apulodi.files.upload({
file: data, // Buffer | Uint8Array | ArrayBuffer | Blob | ReadableStream
fileName: "avatar.jpg",
contentType: "image/jpeg",
path: "users/avatars", // optional
metadata: { userId: "u_123" }, // optional
});
console.log(file.status); // "uploaded"
This method performs three API calls:
POST /v1/files/upload→ PENDING file + presigned URLPUTthe bytes directly to storage — never through APULODI's serversPOST /v1/files/:id/complete→ server verifies size/content-type
Files over the multipart threshold (8 MiB by default) automatically use the multipart flow:
await apulodi.files.upload({
file: hugeBuffer,
fileName: "video.mp4",
contentType: "video/mp4",
multipartThreshold: 32 * 1024 * 1024, // bump to 32 MiB
});
A failed multipart upload automatically aborts its session first.
Inputs
toBytes() supports Buffer, Uint8Array, ArrayBuffer, Blob and
ReadableStream. Streams are buffered in memory — the API needs the byte
count up front (it's part of the presigned request), so true streaming isn't
possible with the current design.
Explicit multipart flow
const { session, file } = await apulodi.uploads.createMultipart({
filename: "video.mp4",
contentType: "video/mp4",
size: fileBytes.byteLength,
path: "media",
});
// Upload each part; collect the completed-parts list
const parts = [];
for (let i = 0; i < session.parts.length; i++) {
const start = i * session.partSize;
const chunk = fileBytes.subarray(start, start + session.partSize);
parts.push(
await apulodi.uploads.uploadPart(session, i + 1, chunk, {
contentType: "video/mp4",
}),
);
}
// Finalize — server verifies count, contiguity and size
const uploaded = await apulodi.uploads.complete(session.id, parts);
uploadPart() returns { partNumber, etag } automatically (the ETag is read
from the storage response), and it will (re)generate a presigned URL for you
if the part's URL is missing or expired.
Retries & inspection
const fresh = await apulodi.uploads.regeneratePartUrls(session.id, [1, 2]);
const stored = await apulodi.uploads.listStoredParts(session.id);
Abort
await apulodi.uploads.abort(session.id); // { id, aborted: true }
Abort discards parts and marks the file failed. Idempotent; completing an
aborted session returns 409.
Files & folders
Uploads create folders automatically:
await apulodi.files.upload({ file: d, fileName: "a.jpg", path: "users/avatars" });
const inFolder = await apulodi.folders.list("users"); // [{ name: "avatars", … }]
Next: SDK — Error handling.