Transfers API
client.transfers moves whole files and folders: uploads, downloads, copies within a host, and pastes from the system file clipboard. Your app never sees the bytes or the user's local paths — it prepares a transfer, runs it, and watches its progress.
Availability
Each preparation method needs its own permission, and a connected workspace with the matching capability:
| Method | Permission | Also needs |
|---|---|---|
upload(destination) | files.upload | — |
upload(destination, { folder: true }) | files.upload | the files.folders capability |
download(entry), downloadMany(entries) | files.download | — |
copy(entry, destination) | files.copy | — |
pasteClipboard(destination) | files.upload and system.clipboard.files.read | a native file clipboard (Windows) |
Running, watching, cancelling and closing a transfer need no permission: holding the handle is the authority.
Interface
interface TransferProgress { bytes: number; total: number; items?: number; phase: "preparing" | "running" | "finishing" }
interface TransferResult {
status: "completed" | "canceled" | "failed";
bytes: number; total: number;
message?: string | null;
destination?: RemoteFileLocation; // remote destinations only, never a local path
}
interface TransferSnapshot {
revision: number;
state: "queued" | "running" | "canceling" | "cancel-failed" | "completed" | "canceled" | "failed";
progress: TransferProgress;
result?: TransferResult;
cancellationError?: string;
}
interface RemoteTransfer {
readonly binding: string;
readonly name: string;
readonly size: number;
readonly direction: "upload" | "download" | "copy" | "move";
run(signal?: AbortSignal): Promise<TransferResult>;
status(): Promise<TransferSnapshot>;
watch(signal?: AbortSignal): AsyncIterable<TransferSnapshot>;
cancel(): Promise<void>;
close(): Promise<void>;
}
interface AppTransfersAPI {
upload(destination: RemoteFileLocation, options?: { folder?: boolean }, signal?: AbortSignal): Promise<RemoteTransfer[]>;
download(entry: RemoteEntryLocation, signal?: AbortSignal): Promise<RemoteTransfer | null>;
downloadMany(entries: readonly RemoteEntryLocation[], signal?: AbortSignal): Promise<RemoteTransfer[]>;
copy(entry: RemoteEntryLocation, destination: RemoteFileLocation, signal?: AbortSignal): Promise<RemoteTransfer>;
pasteClipboard(destination: RemoteFileLocation, signal?: AbortSignal): Promise<RemoteTransfer[]>;
}Preparation opens the desktop's own file chooser where one is needed, so the user — not your app — picks what leaves or enters their computer. download resolves to null when the user cancels the dialog; the array methods resolve to an empty array.
Examples
Upload files the user chooses into the current folder, showing progress:
const transfers = await client.transfers.upload({ binding, path: folder.path });
for (const transfer of transfers) {
void (async () => {
try {
const watching = (async () => {
for await (const snapshot of transfer.watch()) {
const { bytes, total, phase } = snapshot.progress;
render(transfer.name, phase, total > 0 ? bytes / total : 0);
}
})();
const result = await transfer.run();
await watching;
report(transfer.name, result.status, result.message ?? "");
} finally {
await transfer.close(); // always release the handle
}
})();
}watch() ends by itself once the transfer reaches an outcome. Only one watcher may wait per transfer.
Copy a file within the host and wait for the result:
const copy = await client.transfers.copy(
{ binding, path: entry.path, revision: entry.revision! },
{ binding, path: destinationFolder.path },
);
try {
const result = await copy.run();
if (result.status === "completed") show(`Copied to ${result.destination?.path}`);
} finally {
await copy.close();
}Cancelling is a request, not a guarantee:
await transfer.cancel();
const snapshot = await transfer.status();
if (snapshot.state === "cancel-failed") warn(snapshot.cancellationError ?? "Cancellation failed.");Errors
| Code | Message | Meaning |
|---|---|---|
invalid | Supply the documented transfer parameters only. | Unexpected keys or missing revisions. |
invalid | Download entries must share one accepted binding and include their revisions. | Mixed or stale entries in downloadMany. |
invalid | Copy source and destination must share the same binding. | Cross-connection copy. |
unavailable | This transfer service is unavailable. | The provider offers no transfers. |
unavailable | Native file clipboard is unavailable. | pasteClipboard outside Windows. |
busy | Finish the active chooser or release existing transfers first. | One chooser at a time; at most 32 preparations and 32 jobs per window. |
busy | Only one progress watcher may wait per transfer. | A second watch(). |
closed | Transfer handle is closed or belongs to another window. | Used after close(), or from the wrong window. |
closed | The workspace binding has changed. | The connection changed during preparation. |
aborted | Transfer preparation canceled. | The signal fired, or the user dismissed the chooser. |
A failed transfer resolves with status: "failed" and a message rather than throwing — the throwing cases are the ones above, which are about starting work.
Lifecycle and permissions
Handles belong to the window that prepared them. run() starts the work once and remembers its outcome, so calling it twice returns the same result. close() cancels anything still running, waits for the outcome and then releases the handle; always call it in a finally.
Existing destinations are never replaced: an upload or copy onto an existing name fails instead of overwriting, and a folder transfer needs a destination root that does not exist yet. After a cancellation or failure, completed items stay where they are — partial results are not rolled back.
If the workspace changes connection, or a capability disappears, the desktop cancels your jobs and your handles report closed. Local download paths are never exposed to apps: TransferResult.destination is populated for remote destinations only.
Related guides
- Files API — locations, entries and revisions.
- Clipboard API — publishing files to the system clipboard.
- Upload, download and copy — the same machinery from the user's side.
- App lifecycle and services — cleanup duties and error codes.