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");

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 — UploadThing, Netlify Blobs, Cloudinary, and the R2 binding without HTTP credentials — emulate copy by downloading the source and re-uploading it to the destination. The source is streamed through rather than buffered, so even multi-GB copies stay within serverless memory limits, but 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 OperationOptionssignal, 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)

On this page