# Downloading to disk
How to get files out of OPFS into a real download or a save picker. Always start with `readBlob` — it returns a disk-backed `Blob` without copying the whole file into memory. Details: [streaming](./streaming.md).
```typescript
import { createOPFS } from 'opfs-worker';
const fs = createOPFS({ root: '/my-app' });
```
## Anchor download (``)
Works everywhere. Trigger a browser download from the blob URL:
```typescript
async function download(path: string) {
const blob = await fs.readBlob(path);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = path.split('/').pop() || 'download';
a.click();
URL.revokeObjectURL(url);
}
await download('/reports/q1.pdf');
```
Same pattern as the [live demo](https://kachurun.github.io/opfs-worker/) (`demo/components/FileBrowser/`).
## Save picker (`showSaveFilePicker`)
Lets the user choose where to save (Chrome / Edge). Stream the OPFS blob into the writable:
```typescript
async function saveAs(path: string) {
const blob = await fs.readBlob(path);
const handle = await window.showSaveFilePicker({
suggestedName: path.split('/').pop() || 'download',
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
}
```
Safari / Firefox don’t support this API well — keep the `` path as a fallback.
## Folders
Walk only the folder you care about with `readDir` (no need to scan the whole FS via `index`):
```typescript
/** Every file under `dir` → [absolutePath, pathRelativeToDir] */
async function walkFiles(dir: string, prefix = ''): Promise<[string, string][]> {
const out: [string, string][] = [];
for (const entry of await fs.readDir(dir)) {
const abs = dir === '/' ? `/${entry.name}` : `${dir}/${entry.name}`;
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory) {
out.push(...await walkFiles(abs, rel));
}
else {
out.push([abs, rel]);
}
}
return out;
}
```
### Directory picker (`showDirectoryPicker`)
User picks a destination folder; you recreate that OPFS subtree under it (Chrome / Edge; needs `mode: 'readwrite'`):
```typescript
async function exportDirectory(opfsDir: string) {
const dest = await window.showDirectoryPicker({ mode: 'readwrite' });
for (const [abs, rel] of await walkFiles(opfsDir)) {
const parts = rel.split('/');
const fileName = parts.pop()!;
let dir = dest;
for (const part of parts) {
dir = await dir.getDirectoryHandle(part, { create: true });
}
const file = await dir.getFileHandle(fileName, { create: true });
const writable = await file.createWritable();
await writable.write(await fs.readBlob(abs));
await writable.close();
}
}
await exportDirectory('/project'); // only /project/**, not the whole root
```
### Zip fallback
Where the directory picker isn’t available, pack that same walk into a zip, then download it. Example with [`fflate`](https://github.com/101arrowz/fflate):
```typescript
import { zip } from 'fflate';
async function zipDirectory(opfsDir: string): Promise {
const files: Record = {};
for (const [abs, rel] of await walkFiles(opfsDir)) {
files[rel] = new Uint8Array(await (await fs.readBlob(abs)).arrayBuffer());
}
const packed = await new Promise((resolve, reject) => {
zip(files, { level: 6 }, (err, data) => (err ? reject(err) : resolve(data)));
});
return new Blob([packed.slice()], { type: 'application/zip' });
}
async function downloadDirectory(opfsDir: string) {
const blob = await zipDirectory(opfsDir);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${opfsDir.split('/').pop() || 'root'}.zip`;
a.click();
URL.revokeObjectURL(url);
}
await downloadDirectory('/project');
```
Note: zipping loads each file’s bytes into memory. For huge trees prefer the directory picker above. The demo uses the same idea — see `zipDirectory` in `demo/components/FileBrowser/upload.ts`.