How to Implement R2 Read-Only Mounts in the Computer Workspace Filesystem
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. 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. This file specifies the R2BucketOptions interface, which includes:
prefix: Optional string to filter R2 keyslistLimit: Number controlling R2list()page size (defaults to 1000)
Workspace Integration
The Workspace class in 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) and instantiate it with your binding:
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:
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:
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 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 (lines 34-82), demonstrates mounting an R2 bucket and serving its contents:
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:
try {
await ws.writeFile("/workspace/r2/new.txt", "hello");
} catch (e) {
console.error(e.message); // "ENOSYS: operation not supported on read-only mount"
}
Summary
R2Bucketinpackages/computer/src/mounts/providers/r2.tscreates the read-only mount by implementing onlyhead,get, andlistmethods- Workspace registration requires passing the mount in a
Record<string, Mount>to theWorkspaceconstructor, typically keyed under/workspace/r2 - Configuration options include
prefixfor path stripping andlistLimitfor pagination control - Write protection is enforced at the provider level, throwing
ENOSYSfor 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 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. 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →