> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Dropbox

> Mount Dropbox as a read/write MIRAGE VFS from Node or the browser via OAuth2.

Mirage ships `DropboxVFS` in **two runtimes**:

* `@struktoai/mirage-node`, uses `client_id` + `client_secret` + long-lived refresh token to fetch
  short-lived access tokens server-side.
* `@struktoai/mirage-browser`, supports the same refresh token via PKCE (no client secret in the bundle).

Both runtimes hit the same [Dropbox v2 HTTP API](https://www.dropbox.com/developers/documentation/http/documentation):
`/2/files/list_folder` for directories, `/2/files/download` for content, and
`/2/files/upload` / `create_folder_v2` / `delete_v2` / `move_v2` / `copy_v2` for writes
(single-call uploads cap at \~150 MB). Credentials are obtained the same way in both runtimes, see
[Dropbox Credentials](https://docs.mirage.strukto.ai/home/setup/dropbox.md). The Python package ships the same
backend, see [Dropbox (Python)](https://docs.mirage.strukto.ai/python/vfs/dropbox.md).

## Node (server-side)

```bash theme={null}
pnpm add @struktoai/mirage-node
```

```ts theme={null}
import { DropboxVFS, MountMode, Workspace } from '@struktoai/mirage-node'

const dropbox = new DropboxVFS({
  clientId: process.env.DROPBOX_APP_KEY!,
  clientSecret: process.env.DROPBOX_APP_SECRET!,
  refreshToken: process.env.DROPBOX_REFRESH_TOKEN!,
})

const ws = new Workspace({ '/dropbox': dropbox }, { mode: MountMode.READ })
const res = await ws.shell('ls /dropbox/')
console.log(res.stdoutText)
```

The `DropboxTokenManager` caches the access token in memory and refreshes it \~5 minutes before
expiry, so cold-start API calls don't pay the refresh round-trip.

### Config

| Field | Description |
| - | - |
| `timeout` default `DEFAULT_TIMEOUT` | Seconds a request may wait to connect, or between two reads of its answer, before it fails; a download that keeps flowing is never cut off. |
| `clientId` required | Dropbox app key. |
| `clientSecret` required, secret | Dropbox app secret. |
| `refreshToken` required, secret | Long-lived OAuth refresh token; mirage mints short-lived access tokens from it. |
| `rootPath` | Dropbox folder the mount uses as its root, such as `/Projects`; unset or `/` is the account root. `..` segments are rejected. |
| `endpoint` | Base URL that replaces Dropbox's OAuth, API and content hosts, such as a test server. Unset uses Dropbox's hosts. |
| `refreshFn` secret | Function that takes the refresh token and returns `{ accessToken, expiresIn }` (seconds), called instead of Dropbox's token endpoint, for example so the app secret stays on a backend. |

Fields marked secret are redacted in snapshots.

## Mount a subfolder

Pass `rootPath` to expose a single Dropbox folder as the mount root instead of the whole
account. Every command and FUSE/VFS op is scoped to that folder; paths outside it are
unreachable.

```ts theme={null}
const dropbox = new DropboxVFS({
  clientId: process.env.DROPBOX_APP_KEY!,
  clientSecret: process.env.DROPBOX_APP_SECRET!,
  refreshToken: process.env.DROPBOX_REFRESH_TOKEN!,
  rootPath: '/Team/data',
})

const ws = new Workspace({ '/dropbox': dropbox }, { mode: MountMode.READ })
await ws.shell('ls /dropbox/') // lists the contents of /Team/data
```

`rootPath` accepts `Team/data`, `/Team/data`, or `/Team/data/` (all normalized the same way);
`..` segments are rejected. Config dictionaries (e.g. via the VFS registry) may spell it
`root_path`. Omitting it (or passing `/`) mounts the account root as before.

## VFS.md

The [VFS.md](https://docs.mirage.strukto.ai/home/setup/vfs-md.md) an agent reads with Dropbox mounted at `/<path>`.

**Rendered**

### Virtual Filesystem

| Path | Description |
| - | - |
| `/<path>` | `dropbox`, write |

#### Supported Unix Commands for VFS

Unix commands work on each path as on a local filesystem, as far as the path supports them:

| Path | Supported |
| - | - |
| `/<path>` | Almost all Unix commands. `grep -r` and `rg` with `-w` ask `dropbox` which files may match. |

#### Limitations of Unix Commands for VFS

Where a path differs from a local filesystem:

| Path | Limitations |
| - | - |
| `/<path>` | Without `-w`, `grep -r` and `rg` read every file. `grep -r`, `find` and `du` stop after 1000 files; a glob matching more, `tar -c`, `cp -r` and `rm -r` are refused. |

Run `man` to list every command, and `man <cmd>` or `<cmd> --help` for one command's usage.

**Raw**

```markdown theme={null}
---
name: workspace-filesystem
description: The virtual filesystem layout of this workspace.
---

# Virtual Filesystem

| Path | Description |
| --- | --- |
| `/<path>` | `dropbox`, write |

## Supported Unix Commands for VFS

Unix commands work on each path as on a local filesystem, as far as the path supports them:

| Path | Supported |
| --- | --- |
| `/<path>` | Almost all Unix commands. `grep -r` and `rg` with `-w` ask `dropbox` which files may match. |

## Limitations of Unix Commands for VFS

Where a path differs from a local filesystem:

| Path | Limitations |
| --- | --- |
| `/<path>` | Without `-w`, `grep -r` and `rg` read every file. `grep -r`, `find` and `du` stop after 1000 files; a glob matching more, `tar -c`, `cp -r` and `rm -r` are refused. |

Run `man` to list every command, and `man <cmd>` or `<cmd> --help` for one command's usage.
```

## Integration

What Dropbox answers through its own API, so fewer files are read. [Integration](https://docs.mirage.strukto.ai/home/integration/overview.md) explains each command.

| Command | On Dropbox |
| - | - |
| `grep -r`, `rg` | Dropbox search, for whole words. It can miss Office files, PDFs, archives, the end of large files, words Dropbox splits differently (`foo.bar`) and recent writes; file contents on plans below Professional. |
| `find`, `du`, `ls -R`, `tree` | One recursive listing |

The [Python Dropbox docs](https://docs.mirage.strukto.ai/python/vfs/dropbox.md#integration) list when each search answers and what it can miss.

## Names missing from a listing

A name missing from a cached listing that the running command did not
fetch is looked up on Dropbox by path before mirage answers that it does
not exist: one `get_metadata` for a `stat`, or the download itself for a
read. A file another client created after the listing is therefore found,
and `>>`, `touch` and `sed -i` act on it instead of replacing it.

A listing the running command fetched itself is trusted as is, for the
whole command. Each command of a script is its own command, but one
long-running command, such as a program a runtime runs, does not see a
file another client creates after it listed the folder.

Some lookups run outside any shell command: FUSE, programmatic calls,
`[ ]` and `[[ ]]` tests, and a redirect on anything but a simple command
(a brace group, subshell, loop, `if` or `case`). No listing is the
caller's own there, so each lookup of a name missing from a cached
listing costs one request.

## Reads under `fresh`

Under `read: fresh`, a cached file is checked against its `content_hash`
before it is served: one `get_metadata` per warm read, never a folder
listing. A download names the hash in its `Dropbox-API-Result` header, full
or ranged, so a read stamps it with no extra request. `server_modified` is
not used: Dropbox repeats it across same-size rewrites.

## Writes

A write records the `content_hash` its upload reply names, so a `fresh` read
right after a `tee` or `sed -i` downloads nothing.

## Conditional writes

A Dropbox mount accepts `write: conditional`.
A write that holds a version looks the file up once (`get_metadata`),
refuses a file whose `content_hash` changed since it was read, and uploads
in `update` mode with the file's `rev`, so Dropbox refuses (409
`path/conflict`) a change that lands in between; a delete sends the `rev` as
`parent_rev`. `rm -r` walks the folder file by file, and a `mv` within the
mount is Dropbox's own move, which is not refused when its source changed
(a `mv` to another mount is a copy and a conditioned delete, so there a
changed source stays and `mv` reports the failed removal).

## Browser (PKCE, no client secret)

```bash theme={null}
pnpm add @struktoai/mirage-browser
```

```ts theme={null}
import { DropboxVFS, MountMode, Workspace } from '@struktoai/mirage-browser'

// Refresh token obtained via the PKCE flow, see examples/browser/vfs/dropbox/dropbox_pkce.ts.
const dropbox = new DropboxVFS({
  clientId: import.meta.env.VITE_DROPBOX_APP_KEY,
  refreshToken: refreshTokenFromLocalStorage,
})

const ws = new Workspace({ '/dropbox': dropbox }, { mode: MountMode.READ })
await ws.shell('ls /dropbox/')
```

Set the redirect URI on your Dropbox app's **Settings** tab to your dev/prod origin
(e.g. `http://localhost:5173/vfs/dropbox/dropbox_pkce.html`). The bundled
[`examples/browser/vfs/dropbox/dropbox_pkce.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/browser/vfs/dropbox/dropbox_pkce.ts)
runs the full PKCE dance end-to-end and persists the refresh token to `localStorage`.

## VFS mode (`patchNodeFs`)

Mirage exposes a `patchNodeFs(workspace)` shim that routes `fs` calls under a mount
through the workspace, so any library that uses Node's `fs` API can read directly from Dropbox
without code changes.

```ts theme={null}
import { createRequire } from 'node:module'
import { DropboxVFS, MountMode, patchNodeFs, Workspace } from '@struktoai/mirage-node'

const require = createRequire(import.meta.url)
const fs = require('fs') as typeof import('fs')

const dropbox = new DropboxVFS({
  clientId: process.env.DROPBOX_APP_KEY!,
  clientSecret: process.env.DROPBOX_APP_SECRET!,
  refreshToken: process.env.DROPBOX_REFRESH_TOKEN!,
})

const ws = new Workspace({ '/dropbox': dropbox }, { mode: MountMode.READ })
const restore = patchNodeFs(ws)

const entries = await fs.promises.readdir('/dropbox/')
const stat = await fs.promises.stat('/dropbox/data')
const bytes = await fs.promises.readFile('/dropbox/data/example.parquet')

restore()
```

The async forms (`fs.promises.*` and the callbacks) are routed. Sync forms like `fs.statSync` and
`fs.readFileSync` refuse a mounted path because remote reads can't block the event loop.

## FUSE mode

For tools that need a real filesystem path (CLI tools, editors, system utilities), mount the
workspace under FUSE:

```ts theme={null}
import { DropboxVFS, Mount, MountBackend, MountMode, Workspace } from '@struktoai/mirage-node'

const dropbox = new DropboxVFS({
  clientId: process.env.DROPBOX_APP_KEY!,
  clientSecret: process.env.DROPBOX_APP_SECRET!,
  refreshToken: process.env.DROPBOX_REFRESH_TOKEN!,
})

const ws = new Workspace({ '/dropbox': new Mount(dropbox, { mode: MountMode.READ, backend: MountBackend.FUSE }) })
await ws.fuseReady()
const mp = ws.fuseMountpoint

// The dropbox subtree is exposed at the mountpoint root, so `${mp}/` is a real path:
//   ls ${mp}/
//   find ${mp} -name '*.parquet'
//   cat ${mp}/data/example.json | jq .

await ws.close()
```

Requires [macFUSE](https://osxfuse.github.io/) on macOS or libfuse on Linux. See
[FUSE setup](https://docs.mirage.strukto.ai/home/setup/macos.md).

## Available commands

`DropboxVFS` ships the same shell command set as the GDrive VFS:

* **Filesystem**: `ls`, `cat`, `head`, `tail`, `nl`, `wc`, `stat`, `find`, `tree`, `du`, `file`,
  `realpath`, `basename`, `dirname`
* **Write** (on a `WRITE` mount): `tee`, `touch`, `mkdir`, `rm`, `rmdir`, `mv`, `cp`, `truncate`,
  shell redirection (`>` / `>>`)
* **Search/text**: `grep`, `rg`, `awk`, `sed`, `sort`, `uniq`, `cut`, `diff`, `cmp`, `jq`
* **Encoding**: `base64`

## Examples

End-to-end runnable scripts are in
[`examples/typescript/vfs/dropbox/`](https://github.com/strukto-ai/mirage/tree/main/examples/typescript/vfs/dropbox):

* [`dropbox.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/vfs/dropbox/dropbox.ts), `Workspace.shell('ls/stat/cat/tree …')` shell demo
* [`dropbox_vfs.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/vfs/dropbox/dropbox_vfs.ts), `patchNodeFs` + native `fs.promises.*` calls
* [`dropbox_fuse.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/vfs/dropbox/dropbox_fuse.ts), FUSE mount with shell access in another terminal

`grep` classifies the bytes it reads, including downloaded attachments. Use `-I` to skip binary inputs or `-a` (`--binary-files=text`) to search them as text. Text mode bypasses the content-search index and scans files directly.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.