# How to Implement R2 Read-Only Mounts in the Computer Workspace Filesystem

> Learn to implement R2 read-only mounts in your workspace filesystem. Import R2Bucket from cloudflare computer and register it in the Workspace constructor for seamless integration.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: how-to-guide
- Published: 2026-08-08

---

**Create a read-only R2 mount by importing `R2Bucket` from `@cloudflare/computer`, passing your R2 binding and options, and registering it in the `Workspace` constructor under a `/workspace` path.**

The `cloudflare/computer` repository provides a virtual filesystem that maps external storage into a workspace. Implementing **R2 read-only mounts** allows you to expose Cloudflare R2 buckets as directories under `/workspace` while enforcing strict write protection at the provider level.

## Understanding the R2 Mount Architecture

### The R2Bucket Provider Implementation

The core logic resides in [`packages/computer/src/mounts/providers/r2.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/providers/r2.ts). The `R2Bucket` function (starting at line 110) returns an `EagerMount` that implements only the read-only R2 API methods: `head`, `get`, and `list`. This provider deliberately omits `put` and `delete` operations, ensuring the mount cannot modify bucket contents.

According to the source code, the provider streams objects directly from R2 using a `ReadableStream`, piping the body into the virtual filesystem at lines 139-140. If an object disappears between the `list` and `get` phases, the provider throws an explicit error at line 135.

### Mount Type Definitions

Type safety and configuration shapes are defined in [`packages/computer/src/mounts/types.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/types.ts). This file specifies the `R2BucketOptions` interface, which includes:
- `prefix`: Optional string to filter R2 keys
- `listLimit`: Number controlling R2 `list()` page size (defaults to 1000)

### Workspace Integration

The `Workspace` class in [`packages/computer/src/workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts) (mount handling begins around line 397) accepts a `Record<string, Mount>` mapping. Each key represents a mount point under `/workspace`, and each value is a provider instance like the one returned by `R2Bucket`.

## Step-by-Step Implementation

### Step 1: Configure the R2 Binding

Expose your R2 bucket through the worker environment. In your Wrangler configuration, bind an R2 bucket to a variable (commonly `env.Bucket`).

### Step 2: Create the Read-Only Mount

Import `R2Bucket` from the package entry point (re-exported at line 37 of [`packages/computer/src/index.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/index.ts)) and instantiate it with your binding:

```typescript
import { R2Bucket } from "@cloudflare/computer";

const mount = R2Bucket(env.Bucket, {
  prefix: "assets/",
  listLimit: 500
});

```

The returned mount enforces read-only constraints natively. Attempts to write or delete will throw `ENOSYS` (operation not supported).

### Step 3: Register with Workspace

Pass the mount to the `Workspace` constructor under a `/workspace` subpath:

```typescript
import { Workspace } from "@cloudflare/computer";

const ws = new Workspace({
  "/workspace/r2": R2Bucket(env.Bucket, { prefix: "public/" })
});

```

### Step 4: Read Files from R2

Access objects using standard filesystem methods. The mount transparently handles R2 `get` operations:

```typescript
const logo = await ws.readFile("/workspace/r2/logo.png");
const contents = await logo.arrayBuffer();

```

## Configuration Options and Behavior

### Prefix Handling

The `prefix` option at line 34 of [`r2.ts`](https://github.com/cloudflare/computer/blob/main/r2.ts) strips the configured prefix from all R2 keys when mapping them to relative paths. This allows you to expose a specific subdirectory of your bucket as the mount root without exposing the full key structure.

### List Limits and Pagination

The `listLimit` parameter controls the page size for R2 `list()` calls. Adjust this for large buckets to manage memory usage. The default of 1000 items works well for most use cases, but reducing this to 200-500 prevents excessive buffer allocation when listing millions of objects.

### Write Protection Mechanism

Unlike file-system-level permissions, this mount implements write protection at the API level. The provider only exposes `head`, `get`, and `list` methods. When the `Workspace` attempts a write operation, it encounters undefined write methods, resulting in an `ENOSYS` error. This architectural guarantee prevents accidental modifications even if the consuming code has bugs.

## Complete Working Example

This example, patterned after [`examples/worker-shell/src/index.ts`](https://github.com/cloudflare/computer/blob/main/examples/worker-shell/src/index.ts) (lines 34-82), demonstrates mounting an R2 bucket and serving its contents:

```typescript
import { Workspace, R2Bucket } from "@cloudflare/computer";

export interface Env {
  Bucket: R2Bucket;
}

export default {
  async fetch(request: Request, env: Env) {
    const ws = new Workspace({
      "/workspace/r2": R2Bucket(env.Bucket, {
        prefix: "static/",
        listLimit: 200
      })
    });

    const url = new URL(request.url);
    
    try {
      const file = await ws.readFile(url.pathname);
      return new Response(file, {
        headers: { "Content-Type": "application/octet-stream" }
      });
    } catch (e) {
      return new Response("Not found", { status: 404 });
    }
  }
};

```

Attempting to write to this mount fails predictably:

```typescript
try {
  await ws.writeFile("/workspace/r2/new.txt", "hello");
} catch (e) {
  console.error(e.message); // "ENOSYS: operation not supported on read-only mount"
}

```

## Summary

- **`R2Bucket`** in [`packages/computer/src/mounts/providers/r2.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/providers/r2.ts) creates the read-only mount by implementing only `head`, `get`, and `list` methods
- **Workspace registration** requires passing the mount in a `Record<string, Mount>` to the `Workspace` constructor, typically keyed under `/workspace/r2`
- **Configuration options** include `prefix` for path stripping and `listLimit` for pagination control
- **Write protection** is enforced at the provider level, throwing `ENOSYS` for any mutation attempts
- **Streaming** happens directly from R2 to the virtual filesystem without buffering entire objects in memory

## Frequently Asked Questions

### What happens if I try to write to a read-only R2 mount?

The operation throws an `ENOSYS` error with the message "operation not supported on read-only mount". Because the `R2Bucket` provider in [`r2.ts`](https://github.com/cloudflare/computer/blob/main/r2.ts) does not implement `put` or `delete` methods, the `Workspace` class cannot perform write operations, providing hardware-level protection against accidental modifications.

### Can I mount multiple R2 buckets in the same workspace?

Yes. The `Workspace` constructor accepts multiple mount points. You can instantiate separate `R2Bucket` providers for different buckets and register each under a unique path, such as `/workspace/r2-assets` and `/workspace/r2-backups`, using distinct environment bindings.

### How does the prefix option affect file paths?

The `prefix` option filters which R2 objects appear in the mount and strips that prefix from visible paths. If your bucket contains `assets/images/logo.png` and you set `prefix: "assets/"`, the file appears at `/workspace/r2/images/logo.png` rather than `/workspace/r2/assets/images/logo.png`.

### Is there a performance difference between reading small and large objects?

Small objects transfer in a single round trip, while large objects stream through a `ReadableStream` as implemented at lines 139-140 of [`r2.ts`](https://github.com/cloudflare/computer/blob/main/r2.ts). The provider does not buffer entire objects in memory, so memory usage remains constant regardless of object size, though large objects naturally take longer to transfer over the network.