Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

copy

Copy an object from one key to another - server-side where the provider supports it, with a streaming read + write fallback otherwise.

files.copy(from, to, options?)

Copies the object at from to to and resolves to void. Where the provider has a native copy primitive (S3 CopyObject, GCS, Supabase, Vercel Blob, Dropbox, Azure, and others) the copy happens entirely server-side — no bytes travel through your process. Adapters without one fall back to a read + write, and a few providers can’t copy at all and throw.

await files.copy("avatars/abc.png", "avatars/abc.bak.png");

Conditional copy

Check a source generation and a destination create/replace predicate in one provider request:

await files.copy("staging/report.json", "published/report.json", {
  condition: {
    source: { etag: staging.etag! },
    destination: { type: "create" },
  },
});

Conditional copy never uses the read-and-write fallback. The adapter must explicitly declare the requested source and destination checks and that they are atomic together; otherwise the call fails before provider I/O. See Conditional operations.

Both keys are validated, and on a client with a prefix both from and to are resolved against it. The destination is overwritten if it already exists — there is no built-in guard against clobbering. The object’s contentType and user metadata travel with the copy; lastModified is set to the time of the copy, since the destination is a fresh write rather than a clone of the source’s timestamp. A missing from throws FilesError with code: "NotFound".

The read + write fallback

Adapters with no server-side copy primitive — FTP, SFTP, Bun S3, Bunny Storage, Netlify Blobs, PocketBase, Appwrite, UploadThing, and the R2 Workers binding — emulate copy by downloading the source and re-uploading it to the destination. Bun S3, Bunny Storage, and the R2 binding stream the source straight through, so even multi-GB copies stay within serverless memory limits; the others buffer the whole object in memory first, so size large copies accordingly. Cloudinary has no native copy either: it re-uploads by URL, so Cloudinary fetches the source itself and creates a new asset (see Provider gaps). Two things differ from a native copy:

  • It costs an egress download plus an ingest upload. For large or frequent copies on these providers, consider doing the copy at the application layer with a storage strategy that copies server-side.
  • It is not atomic. Concurrent writes to from between the read and the write are not detected, so the destination may reflect an in-flight change to the source.

Each adapter’s Compatibility section marks whether it copies natively, falls back, or throws.

Providers without copy

Some backends have no way to copy to a caller-chosen key. Convex, for instance, assigns immutable storage ids, so copy throws FilesError with code: "Provider" rather than silently misbehaving. When you need the operation anyway, download() the source and upload() it back under a new key, or reach for the escape hatch.

Options

copy accepts the shared OperationOptions — signal, timeout, and retries. As elsewhere, only Provider failures are retried; deterministic errors like NotFound are returned immediately.

await files.copy("avatars/abc.png", "avatars/abc.bak.png", {
  signal: controller.signal,
  retries: 3,
});

On a FileHandle

files.file(key) exposes the same operation bound to one key, as copyTo (the handle is the source) and copyFrom (the handle is the destination):

const avatar = files.file("avatars/abc.png");

await avatar.copyTo("avatars/abc.bak.png"); // copy(key, "avatars/abc.bak.png")
await avatar.copyFrom("uploads/new.png"); // copy("uploads/new.png", key)

Was this page helpful?