---
title: Dropbox
description: Mount a Dropbox account (or a single folder of it) as a read/write Mirage filesystem with async access and shell commands.
icon: dropbox
---

The Dropbox resource mounts a Dropbox account at some prefix such as
`/dropbox/`. All operations involve network I/O to the
[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. Files are served as raw bytes. Both `READ` and `WRITE` modes are
supported; single-call uploads cap at ~150 MB (Dropbox's `upload` limit).
`rmdir` fails `ENOTEMPTY` on a non-empty folder instead of using the API's
recursive delete; `rm -r` is the recursive path.

For credential setup (app key, secret, refresh token), see the
[Dropbox Setup](/home/setup/dropbox) guide. The TypeScript packages ship the
same backend for Node and the browser, see [Dropbox (TypeScript)](/typescript/dropbox).

## Config

```python
import os

from mirage import MountMode, Workspace
from mirage.resource.dropbox import DropboxConfig, DropboxResource

config = DropboxConfig(
    client_id=os.environ["DROPBOX_APP_KEY"],
    client_secret=os.environ["DROPBOX_APP_SECRET"],
    refresh_token=os.environ["DROPBOX_REFRESH_TOKEN"],
    # Optional:
    # root_path="/Team/data",  # mount a sub-folder as the root
)

resource = DropboxResource(config)
ws = Workspace({"/dropbox": resource}, mode=MountMode.WRITE)
```

The access token is fetched from the long-lived refresh token and cached in
memory, refreshing ~5 minutes before expiry.

### Config Reference

| Field            | Required | Description                                                             |
| ---------------- | -------- | ----------------------------------------------------------------------- |
| `client_id`      | Yes      | Dropbox app key                                                         |
| `client_secret`  | Yes      | Dropbox app secret                                                      |
| `refresh_token`  | Yes      | Long-lived OAuth2 refresh token                                         |
| `root_path`      | No       | Mount a sub-folder of the account as the root (default `/`, the account root) |
| `content_search` | No       | Let `grep`/`rg` narrow recursive scans via `/2/files/search_v2` (default `False`; see below) |
| `endpoint`       | No       | Base URL overriding the real Dropbox hosts (test fakes)                 |

## Mount a subfolder

Pass `root_path` 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.

```python
config = DropboxConfig(
    client_id=os.environ["DROPBOX_APP_KEY"],
    client_secret=os.environ["DROPBOX_APP_SECRET"],
    refresh_token=os.environ["DROPBOX_REFRESH_TOKEN"],
    root_path="/Team/data",
)
```

`root_path` accepts `Team/data`, `/Team/data`, or `/Team/data/` (all
normalized the same way); `..` segments are rejected.

## Search push-down

By default a recursive `grep`/`rg` walks the tree and downloads every file.
With `content_search=True`, both commands first ask
[`/2/files/search_v2`](https://www.dropbox.com/developers/documentation/http/documentation#files-search)
which files contain the pattern's literal, then download and scan only those
candidates — the output stays exactly GNU because the local scan still decides
every match. Regex patterns narrow on an extracted required literal; flags
whose output must see every file in scope (`grep -v`, `grep -c`, `rg -v`,
`rg --type/--glob`) always take the full walk, as do file operands and
multi-pattern (`-e`/`-f`) runs. An empty or failed search also falls back to
the full walk.

```python
config = DropboxConfig(
    client_id=os.environ["DROPBOX_APP_KEY"],
    client_secret=os.environ["DROPBOX_APP_SECRET"],
    refresh_token=os.environ["DROPBOX_REFRESH_TOKEN"],
    content_search=True,
)
```

The knob is off by default for two reasons: full-text content search is
plan-gated (Dropbox Professional/Essentials/Business and up — on other plans
`search_v2` silently matches file names only, so a narrowed scan would miss
content matches), and Dropbox's search index lags recent writes by a short
delay, so a push-down may miss files written moments earlier. Only enable it
when the account's plan includes full-text search and slightly stale results
are acceptable.

## Cache

The Dropbox resource caches directory listings via `IndexCacheStore`
(24-hour TTL) to reduce repeated API calls during traversal; file reads go
through the standard read cache (`caches_reads = True`).

## Example

```python
import asyncio
import os

from mirage import MountMode, Workspace
from mirage.resource.dropbox import DropboxConfig, DropboxResource

config = DropboxConfig(
    client_id=os.environ["DROPBOX_APP_KEY"],
    client_secret=os.environ["DROPBOX_APP_SECRET"],
    refresh_token=os.environ["DROPBOX_REFRESH_TOKEN"],
)

resource = DropboxResource(config)


async def main() -> None:
    ws = Workspace({"/dropbox": resource}, mode=MountMode.WRITE)

    r = await ws.execute("ls /dropbox/")
    print(await r.stdout_str())

    r = await ws.execute("grep -rl report /dropbox/docs/")
    print(await r.stdout_str())

    await ws.close()


asyncio.run(main())
```
