SDK — Uploads

The SDK gives you two upload surfaces:

  1. apulodi.files.upload() — high-level; pick your input, done.
  2. apulodi.uploads — explicit multipart control for custom flows.

High-level upload

ts
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:

  1. POST /v1/files/upload → PENDING file + presigned URL
  2. PUT the bytes directly to storage — never through APULODI's servers
  3. POST /v1/files/:id/complete → server verifies size/content-type

Files over the multipart threshold (8 MiB by default) automatically use the multipart flow:

ts
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

ts
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

ts
const fresh = await apulodi.uploads.regeneratePartUrls(session.id, [1, 2]);
const stored = await apulodi.uploads.listStoredParts(session.id);

Abort

ts
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:

ts
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.