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
frombetween 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)