Drivers

Cloudinary Driver

Upload files to Cloudinary media CDN

Uploads files to Cloudinary using pure fetch, FormData, and Web Crypto signatures. Zero Node dependencies — works on any runtime.

import { FilewayClient } from "@fileway/core";
import { CloudinaryDriver } from "@fileway/driver-cloudinary";

const client = new FilewayClient({
  driver: new CloudinaryDriver({
    cloudName: "my-cloud",
    apiKey: process.env.CLOUDINARY_API_KEY!,
    apiSecret: process.env.CLOUDINARY_API_SECRET!,
  }),
});

const result = await client.upload(stream, {
  filename: "avatar.png",
  path: "users/profiles",
});

console.log(result.meta.publicId);
console.log(result.meta.secureUrl);
console.log(result.meta.width, result.meta.height);

Config

OptionTypeDefaultDescription
cloudNamestringCloudinary cloud name
apiKeystringCloudinary API key
apiSecretstringCloudinary API secret
defaultFolderstringDefault upload folder
maxSizeBytesnumberMaximum allowed upload size in bytes (rejected before API call)

Metadata

CloudinaryDriver implements BaseDriver<CloudinaryMeta>

FieldTypeDescription
meta.publicIdstringCloudinary public ID
meta.versionnumberAsset version
meta.formatstringFile format
meta.resourceType"image" | "video" | "raw"Resource type
meta.bytesnumberFile size
meta.secureUrlstringHTTPS URL
meta.widthnumber | undefinedImage width
meta.heightnumber | undefinedImage height

URL Format

getUrl(publicId) returns a URL with the correct resource type and version:

https://res.cloudinary.com/{cloudName}/{resourceType}/upload/v{version}/{publicId}

The resource type (image, video, or raw) and version are resolved in this order:

  1. Explicit hintgetUrl(publicId, { resourceType, version }) skips all lookups.
  2. Upload cache — assets uploaded through this driver instance are tracked from the upload response.
  3. Admin API lookup — one resources/search request (Basic auth) for public IDs the instance hasn't seen, e.g. assets uploaded by another service or before a restart. The result is cached for subsequent calls, and the returned secure_url is used verbatim when available.
  4. image default — when nothing else matches, the URL is built assuming an image resource type.

Because a Cloudinary public ID does not encode its resource type, getUrl/delete of a video or raw asset uploaded outside this instance would otherwise silently build an incorrect /image/ URL or fail to delete. The Admin lookup resolves those cases without any caller cooperation.

Download

Stream an asset back from the CDN with get(publicId, options?) — it fetches the asset's public URL and returns the response body as a WHATWG ReadableStream<Uint8Array>:

const stream = await client.get(result.meta.publicId);
const bytes = new Uint8Array(await new Response(stream).arrayBuffer());

The optional options argument is the same { resourceType, version } hint used by getUrl.

stream is a WHATWG ReadableStream<Uint8Array>, so you can also pipe it straight to its destination (stream.pipeTo(writable)) with no buffering. Use .arrayBuffer()/.blob() for binary files — .text() would decode binary data as UTF-8 and corrupt it.

Throws with the HTTP status on a non-2xx response (e.g. 404 for a missing asset).

Presigned URLs

getPresignedUrl(publicId, { expiresIn, deliveryType }) returns a Cloudinary signed delivery URL valid until expiresIn seconds from now:

const url = await client.getPresignedUrl(result.meta.publicId, { expiresIn: 300 });
// https://res.cloudinary.com/demo/image/upload/s--BUKMRPuV--/v123/sample?expires_at=...
  • The signature is the first 8 characters of a URL-safe base64 SHA-1 digest of the resource path (everything after the s-- component, e.g. v123/sample) concatenated with the API secret. It is generated purely with Web Crypto — no SDK and no network call. expiresIn defaults to 3600 and must be a positive integer.
  • The URL uses the same resolution chain as getUrl ({ resourceType, version } hints, upload cache, Admin API lookup) to pick the correct resource_type and version for the signed path.
  • Security caveat: the signature only restricts access for assets with authenticated or private delivery — pass deliveryType: "authenticated" (or "private") for those. CloudinaryDriver.upload creates public upload assets, so the default signed URL resolves but does not restrict anything (the asset is already public). The deliveryType must match the asset's actual delivery type or Cloudinary returns 404.

Delete

delete(publicId, options?) removes an asset via the Cloudinary Admin API, using the same resolution chain as getUrl to pick the correct resource_type path (/resources/{type}/upload). Returns true only when Cloudinary confirms the deletion, and false when the asset is unknown or the request fails.

Cancellation

Pass an AbortSignal in UploadOptions to cancel an upload in flight:

const controller = new AbortController();

const upload = client.upload(stream, {
  filename: "large.mp4",
  signal: controller.signal,
});

// later...
controller.abort();

The signal is forwarded to the Cloudinary API request; on abort the driver rejects with a DOMException whose name is AbortError:

try {
  await upload;
} catch (err) {
  if (err instanceof DOMException && err.name === "AbortError") {
    // cancelled
  }
}

Note: the source stream is buffered into a Blob before the API call (required to compute the SHA-1 signature), so cancellation takes effect during the network request rather than while reading the stream.

Progress

Pass onProgress to receive { bytes, total?, progress? } as the stream is buffered for signing. Cloudinary's upload fetch itself exposes no progress, so events track this local buffering phase (with options.size, progress reaches exactly 1 once the Blob is ready):

const result = await client.upload(stream, {
  filename: "large.mp4",
  size: totalBytes,
  onProgress: ({ bytes, progress }) => {
    console.log(`${Math.round((progress ?? 0) * 100)}%`);
  },
});

Errors

All failures are StorageErrors from @fileway/core (see the error reference for the full contract — codes, statusCode, provider, cause, and isAbortError). For this driver:

CodeWhen
validationMalformed input: missing/oversized filename, path separators, absolute path, null bytes, malformed mimeType, upload size exceeding maxSizeBytes, non-positive expiresIn.
auth-failedHTTP 401/403 — invalid or expired apiKey/apiSecret, or restricted permissions.
bucket-not-foundUpload-time HTTP 404 — the cloud does not exist.
not-foundDownload-time HTTP 404 — the asset does not exist at the CDN URL.
size-exceededHTTP 413.
networkfetch failed before an HTTP response; the original error is preserved as cause. Safe to retry.
provider-errorAny other non-2xx response (e.g. HTTP 500). Safe to retry.

On this page