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);
FieldContents
transferredSource keys copied to the destination.
skippedKeys skipped because they already existed. Omitted when none.
errorsPer-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})`);
  },
});

On this page