> ## 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.

# Google Drive

> Mount Google Drive folders and files, including Docs, Sheets, and Slides, as a Mirage virtual filesystem.

The Google Drive VFS exposes a Google Drive account as a virtual
filesystem mounted at some prefix such as `/gdrive/`.

For Google OAuth setup, see [Google Workspace Setup](/home/setup/google).

## Config

```python theme={null}
import os

from mirage import MountMode, Workspace
from mirage.vfs.gdrive import GoogleDriveConfig, GoogleDriveVFS

config = GoogleDriveConfig(
    client_id=os.environ["GOOGLE_CLIENT_ID"],
    client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
    refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"],
)
vfs = GoogleDriveVFS(config=config)
ws = Workspace({"/gdrive": vfs}, mode=MountMode.WRITE)
```

To mount a subfolder instead of the whole Drive, set `folder_id`.
The mount root becomes that folder. The folder may live in My Drive,
be shared with you, sit inside a Shared Drive, or be a Shared Drive
id itself; the drive scope is resolved automatically. Scoped mounts
do not surface other shared drives:

```python theme={null}
config = GoogleDriveConfig(
    client_id=os.environ["GOOGLE_CLIENT_ID"],
    client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
    refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"],
    folder_id="1AbCdEfFolderId",
)
```

## Filesystem Layout

```text theme={null}
/gdrive/
  <folder>/
    <file>
    <subfolder>/
      ...
    <name>.gdoc.json
    <name>.gsheet.json
    <name>.gslide.json
  <shared-drive>/
    <file>
    <folder>/
```

Example:

```text theme={null}
/gdrive/
  Projects/
    spec.pdf
    roadmap.gsheet.json
    Budget/
      Q1.gsheet.json
      Q2.gsheet.json
  Notes/
    meeting.gdoc.json
  Presentations/
    quarterly_review.gslide.json
  Team Drive/
    shared-spec.pdf
  data.csv
  report.pdf
```

The mount mirrors the actual Google Drive folder hierarchy. The root
contains the Drive API `root` folder plus each Shared Drive visible to
the user. Shared Drives appear as top-level directories. Duplicate names
receive a `[Shared Drive]` suffix and, when needed, a numeric suffix.
Subfolders appear as directories, and regular files keep their original names.

### Synthetic Extensions

Google Workspace files cannot be downloaded as raw bytes, so they
are exposed with synthetic extensions and read via their respective
APIs:

| Extension | Type | Read via |
| - | - | - |
| `.gdoc.json` | Google Docs | Docs API |
| `.gsheet.json` | Google Sheets | Sheets API |
| `.gslide.json` | Google Slides | Slides API |

Regular files (PDFs, images, CSVs, etc.) are downloaded directly
from Drive. Large regular files use streaming.

## Write Support

Under `MountMode.WRITE` the standard write commands work on regular
files and folders: `tee`, `cp`, `mv`, `rm`, `mkdir`, `rmdir`,
`touch`, `truncate`, and in-place editors such as `sed -i`. Semantics
follow the other object-store mounts (GNU check-then-act: `EEXIST`
on mkdir over an existing name, `mv` onto a non-empty directory
fails with `ENOTEMPTY`, `cp -r` merges).

Google-native files (`.gdoc.json`, `.gsheet.json`, `.gslide.json`)
are read-only as bytes; writing to them fails with `EACCES`. Mutate
them through the `gws` commands below instead.

Drive access is per-item (shared-drive roles, folder-level grants), so
a write mount can still hold items you may not edit. A mutation the
API denies fails with `EACCES` (Permission denied) on that operand,
like a real filesystem; the rest of the mount keeps working.

A write records the `md5Checksum` (else the `headRevisionId`) its upload
reply names, so a `fresh` read right after a `tee` or `sed -i` downloads
nothing. See [Writes](/home/cache#writes).

### Writing inside a Shared Drive

A Shared Drive is not a read-only corner of the mount. Every Drive call
mirage makes carries `supportsAllDrives`, so create, write, rename, copy
and delete work the same inside a Shared Drive as in My Drive, and the
commands above behave identically there.

What you may actually do is decided by Drive, not by mirage: a Shared
Drive has its own role model (`viewer`, `commenter`, `contributor`,
`content manager`, `manager`), and some organizations restrict deletion
or moving content out of the drive. mirage does not attempt to predict
those rules. It issues the operation and reports the answer, so a role
that forbids the change fails with `EACCES` on that operand, exactly like
an unwritable item in My Drive.

The practical consequence is that a write mount spanning Shared Drives is
partly writable, and the boundary follows your roles rather than the
mount. If you want a mount that cannot write at all, use
`MountMode.READ`; if you want to scope one to a single drive, set
`folder_id` to the Shared Drive id.

## Snapshots

The VFS supports workspace snapshots. Recorded reads capture the
file's Drive revision (`headRevisionId`), and a loaded snapshot pins
reads to that revision via the Drive Revisions API, so replay serves
the exact bytes the agent saw. A path the agent wrote carries its write's
token and no revision: loading checks the live file against that token,
and replay reads the live file rather than a pinned revision.

## Cache

The Google Drive VFS uses `IndexCacheStore` (same as Slack,
Gmail, and other mounts). Index entries store folder IDs, file
IDs, and file metadata. There is no separate content cache -- file
content caching is handled by the workspace `IOResult` mechanism.

## Example

```python theme={null}
import asyncio
import os

from dotenv import load_dotenv

from mirage import MountMode, Workspace
from mirage.commands.cli.builtin.gws import GWS
from mirage.vfs.gdrive import GoogleDriveConfig, GoogleDriveVFS

load_dotenv(".env.development")

config = GoogleDriveConfig(
    client_id=os.environ["GOOGLE_CLIENT_ID"],
    client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
    refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"],
)
vfs = GoogleDriveVFS(config=config)


async def main():
    ws = Workspace({"/gdrive": vfs}, mode=MountMode.WRITE)
    ws.register_cli("gws", GWS, config.model_dump())

    # List root
    r = await ws.shell("ls /gdrive/ | head -n 10")
    print(await r.stdout_str())

    # Browse a subfolder
    r = await ws.shell("ls /gdrive/Projects/")
    print(await r.stdout_str())

    # Read a regular file
    r = await ws.shell("cat /gdrive/Projects/spec.pdf | head -c 200")
    print(await r.stdout_str())

    # Read a Google Doc title
    r = await ws.shell('jq ".title" /gdrive/Notes/meeting.gdoc.json')
    print(await r.stdout_str())

    # Read a Google Sheet title
    r = await ws.shell(
        'jq ".properties.title" /gdrive/Projects/roadmap.gsheet.json')
    print(await r.stdout_str())

    # Read a Google Slides deck length
    r = await ws.shell(
        'jq ".slides | length"'
        " /gdrive/Presentations/quarterly_review.gslide.json")
    print(await r.stdout_str())

    # Search across all files
    r = await ws.shell('rg "quarterly" /gdrive/Projects/')
    print(await r.stdout_str())

    # Tree view
    r = await ws.shell("tree -L 2 /gdrive/")
    print(await r.stdout_str())

    # Create a new Google Doc
    r = await ws.shell(
        "gws docs documents create"
        ' --json \'{"title": "New Doc from MIRAGE"}\'')
    print(await r.stdout_str())


if __name__ == "__main__":
    asyncio.run(main())
```

See `examples/google/gdrive.py` for the full working example.

## Shell Commands

Standard commands available on the mounted Google Drive tree:

| Command | Notes |
| - | - |
| `ls` | List folders and files |
| `cat` | Read file content (regular or Workspace) |
| `head` / `tail` | First/last N lines or bytes |
| `grep` / `rg` | Pattern search (file or directory level) |
| `jq` | Query JSON fields on Workspace files |
| `wc` | Line/word/byte counts |
| `stat` | File metadata (name, size, type) |
| `find` | Recursive search with `-name`, `-maxdepth` |
| `tree` | Directory tree view |
| `basename` | Extract filename from path |
| `dirname` | Extract directory from path |
| `realpath` | Resolve path to absolute form |
| `nl` | Number lines of output |
| `sort` | Sort lines |
| `uniq` | Deduplicate adjacent lines |
| `cut` | Extract fields/columns |
| `awk` | Pattern-directed scanning |
| `sed` | Stream editor |
| `tr` | Translate/delete characters |
| `diff` | Compare two files |
| `rev` | Reverse lines |
| `tac` | Reverse file line order |
| `paste` | Merge lines of files |
| `join` | Join lines on a common field |
| `column` | Columnate output |
| `comm` | Compare sorted files line by line |
| `fold` | Wrap lines to a given width |
| `fmt` | Reformat paragraph text |
| `expand` | Convert tabs to spaces |
| `unexpand` | Convert spaces to tabs |
| `du` | Estimate file space usage |
| `shuf` | Randomly permute lines |
| `look` | Display lines beginning with a prefix |
| `strings` | Extract printable strings from binary |
| `base64` | Base64 encode/decode |
| `md5` | MD5 checksum |
| `sha256sum` | SHA-256 checksum |
| `xxd` | Hex dump |
| `zcat` | Read compressed files |
| `zgrep` | Search compressed files |
| `readlink` | Print resolved symbolic links |
| `cmp` | Compare two files byte by byte |
| `tsort` | Topological sort |
| `file` | Detect file type |

## Acting on Drive

Acting on Drive files by id (create, update, copy, delete, share,
export, plus the Docs/Sheets/Slides helpers) goes through the
[gws CLI](/python/cli/gws) when installed.


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