---
title: transfer
description: Stream every object under a prefix from one Files instance to another - the cross-provider migration 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.

```ts lineNumbers
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, total, key }) =>
    console.log(`${done}/${total} — ${key}`),
});
```

Both arguments are full [`Files`](/api) instances, not raw adapters, so each leg honors its own instance's `prefix`, retries, timeouts, and [hooks](/api/onaction). Each object is streamed download-to-upload, so the destination never sees a buffered copy of a large file.

`transfer` is a one-shot copy. For an incremental, optionally-pruning mirror — skip-unchanged, delete extraneous keys, dry-run the plan — reach for [`sync`](/api/sync) instead.

## 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`](/api/download) 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](/bulk), `transfer` does **not** throw on a partial failure. Successes, skips, and failures come back separated, in walk order:

```ts lineNumbers
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`](/api/errors).

## Options

```ts lineNumbers
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, carrying a running `done` count, the `total`, the `key`, and whether it was `transferred` or `skipped`. The source is walked in full before any transfer begins, so `total` is the final denominator from the very first event.

```ts lineNumbers
await transfer(from, to, {
  onProgress: ({ done, total, key, status }) => {
    console.log(`${done}/${total}: ${key} (${status})`);
  },
});
```
