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
| Option | Type | Default | Description |
|---|---|---|---|
cloudName | string | — | Cloudinary cloud name |
apiKey | string | — | Cloudinary API key |
apiSecret | string | — | Cloudinary API secret |
defaultFolder | string | — | Default upload folder |
maxSizeBytes | number | — | Maximum allowed upload size in bytes (rejected before API call) |
Metadata
CloudinaryDriver implements BaseDriver<CloudinaryMeta>
| Field | Type | Description |
|---|---|---|
meta.publicId | string | Cloudinary public ID |
meta.version | number | Asset version |
meta.format | string | File format |
meta.resourceType | "image" | "video" | "raw" | Resource type |
meta.bytes | number | File size |
meta.secureUrl | string | HTTPS URL |
meta.width | number | undefined | Image width |
meta.height | number | undefined | Image 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:
- Explicit hint —
getUrl(publicId, { resourceType, version })skips all lookups. - Upload cache — assets uploaded through this driver instance are tracked from the upload response.
- Admin API lookup — one
resources/searchrequest (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 returnedsecure_urlis used verbatim when available. imagedefault — 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-1digest of the resource path (everything after thes--component, e.g.v123/sample) concatenated with the API secret. It is generated purely with Web Crypto — no SDK and no network call.expiresIndefaults to3600and 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 correctresource_typeandversionfor the signed path. - Security caveat: the signature only restricts access for assets with
authenticatedorprivatedelivery — passdeliveryType: "authenticated"(or"private") for those.CloudinaryDriver.uploadcreates publicuploadassets, so the default signed URL resolves but does not restrict anything (the asset is already public). ThedeliveryTypemust 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:
| Code | When |
|---|---|
validation | Malformed input: missing/oversized filename, path separators, absolute path, null bytes, malformed mimeType, upload size exceeding maxSizeBytes, non-positive expiresIn. |
auth-failed | HTTP 401/403 — invalid or expired apiKey/apiSecret, or restricted permissions. |
bucket-not-found | Upload-time HTTP 404 — the cloud does not exist. |
not-found | Download-time HTTP 404 — the asset does not exist at the CDN URL. |
size-exceeded | HTTP 413. |
network | fetch failed before an HTTP response; the original error is preserved as cause. Safe to retry. |
provider-error | Any other non-2xx response (e.g. HTTP 500). Safe to retry. |