Upload & download
Move a file into the browser and save a browser-generated download.
Install the TypeScript SDK and Playwright using the Quickstart. This controlled page uses no external website or credentials.
Save as files.mts and run node files.mts.
import { setTimeout as delay } from "node:timers/promises";
import { open } from "node:fs/promises";
import { chromium } from "playwright";
import { TabSail } from "tabsail";
const client = new TabSail();
await client.withSession({ timeoutSeconds: 300 }, async (session) => {
if (!session.downloadDirectory) throw new Error("Browser is not ready");
const grant = await client.sessions.connections.create(session.id, { type: "cdp" });
const browser = await chromium.connectOverCDP(grant.url, { noDefaults: true });
try {
const downloadPolicy = await browser.newBrowserCDPSession();
try {
await downloadPolicy.send("Browser.setDownloadBehavior", {
behavior: "allow",
downloadPath: session.downloadDirectory,
});
} finally {
await downloadPolicy.detach();
}
const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();
await page.setContent('<input id="document" type="file"><a id="export">Download</a>');
const uploaded = await client.sessions.files.upload(session.id, {
filename: "input.csv",
contentType: "text/csv",
file: new TextEncoder().encode("name,value\nexample,1\n"),
});
const cdp = await context.newCDPSession(page);
try {
const document = await cdp.send("DOM.getDocument");
const input = await cdp.send("DOM.querySelector", {
nodeId: document.root.nodeId,
selector: "#document",
});
if (!input.nodeId) throw new Error("File input missing");
await cdp.send("DOM.setFileInputFiles", {
nodeId: input.nodeId,
files: [uploaded.browserPath],
});
} finally {
await cdp.detach();
}
const selected = await page.locator("#document").inputValue();
if (!selected.endsWith("input.csv")) throw new Error("Upload not selected");
await page.evaluate(() => {
const link = document.querySelector<HTMLAnchorElement>("#export");
if (!link) throw new Error("Download link missing");
link.download = "result.csv";
link.href = URL.createObjectURL(new Blob(["status\ncomplete\n"], {
type: "text/csv",
}));
});
await page.locator("#export").click();
const deadline = Date.now() + 20_000;
let fileId: string | undefined;
while (!fileId && Date.now() < deadline) {
let cursor: string | undefined;
do {
const files = await client.sessions.files.list(session.id, {
origin: "download",
cursor,
});
fileId = files.items.find(
(file) => file.status === "ready" && file.filename === "result.csv",
)?.id;
cursor = files.nextCursor ?? undefined;
} while (!fileId && cursor);
if (!fileId) await delay(250);
}
if (!fileId) throw new Error("Download did not become ready");
const output = await open("result.csv", "w");
try {
const download = await client.sessions.files.download(session.id, fileId);
try {
for await (const chunk of download) await output.writeFile(chunk);
} finally {
await download.close();
}
} finally {
await output.close();
}
console.log("Saved result.csv");
} finally {
await browser.close();
}
});Expected: the remote input selects input.csv, your application saves result.csv containing status and complete, then the session is released.
Replace the synthetic page with your website's input and export action. See file limits.