Transfer
Stream every object under a prefix from one Files instance to another - the cross-provider migration that the unified surface uniquely enables, built entirely on listAll, download, and upload.
copy and move live inside a single adapter. A migration spans two — and that's the one thing a unified surface uniquely enables. transfer(source, dest, options?) walks every object the source exposes and streams each one straight to the dest, whatever the backends are.
import { Files, transfer } from "files-sdk";
import { s3 } from "files-sdk/s3";
import { r2 } from "files-sdk/r2";
const from = new Files({ adapter: s3({ bucket: "old" }) });
const to = new Files({
adapter: r2({ bucket: "new", accountId, accessKeyId, secretAccessKey }),
});
const { transferred, errors } = await transfer(from, to, {
prefix: "uploads/",
onProgress: ({ done, key }) => console.log(`${done} done — ${key}`),
});Both arguments are full Files instances, not raw adapters, so each leg honors its own instance's prefix, retries, timeouts, and hooks. Each object is streamed download-to-upload, so the destination never sees a buffered copy of a large file.
What travels
The body, content type, and user metadata move with each object. Destination-assigned fields (etag, lastModified) are fresh on the other side, and Cache-Control is not carried — a StoredFile doesn't expose it. Metadata is dropped for adapters with no metadata primitive; a metadata key a destination adapter rejects outright (Bunny, Appwrite, PocketBase) surfaces as a per-key error rather than failing the whole run.
Result shape
Like the bulk actions, transfer does not throw on a partial failure. Successes, skips, and failures come back separated, in walk order:
const { transferred, skipped, errors } = await transfer(from, to);| Field | Contents |
|---|---|
transferred | Source keys copied to the destination. |
skipped | Keys skipped because they already existed. Omitted when none. |
errors | Per-key { key, error } failures. Omitted when every key wins. |
error is always a normalized FilesError.
Options
await transfer(from, to, {
prefix: "uploads/", // only walk keys under this prefix
transformKey: (key) => `archive/${key}`, // remap each key for the destination
overwrite: false, // skip keys already at the destination
concurrency: 16, // keys in flight at once (default 8)
limit: 500, // page size for the underlying walk
stopOnError: true, // bail at the first failure
signal: controller.signal, // abort the whole transfer
onProgress: ({ done, key, status }) => {},
});transformKey maps the logical key — each instance applies its own prefix independently — which makes re-homing under a new namespace (or moving between two prefixed instances) a one-liner. With overwrite: false, every key costs one extra exists() against the destination.
concurrency bounds how many objects stream at once (and therefore memory, since each in-flight key holds one open stream). It's ignored under stopOnError, which runs sequentially and returns the keys transferred so far plus the first error. signal is forwarded to every list / exists / download / upload and stops new keys from being scheduled; keys already in flight may still finish or surface as errors.
Progress
onProgress fires once per key as it settles, with a running done count and whether the key was transferred or skipped. There's no total — the source is walked lazily, so the full count isn't known until the walk finishes.
await transfer(from, to, {
onProgress: ({ done, key, status }) => {
console.log(`${done}: ${key} (${status})`);
},
});