Skip to main content

Filesystem facade for Wrangler local R2 buckets

Project description

Cloudflare Local R2 Filesystem Facade

r2-local-fs gives Wrangler local R2 buckets a normal filesystem facade.

Cloudflare R2 is S3-compatible in production, so tools like the AWS CLI work well against a real bucket:

aws s3 sync ./assets s3://my-bucket/assets \
  --endpoint-url https://<account-id>.r2.cloudflarestorage.com

Local Wrangler R2 is not stored that way. When you run wrangler dev, local R2 data is persisted under:

.wrangler/state/v3/r2/

That directory is Miniflare persistence state, not a bucket-shaped filesystem. Object bodies live as opaque blob files, while object keys and metadata live in SQLite databases.

This tool leaves .wrangler alone and mirrors a local R2 bucket into a normal folder such as:

~/R2/wk-prod/
  blog/2026/may/foo.json
  images/logo.png
  assets/app.css

Files copied into that folder are uploaded into Wrangler local R2. Objects written by the Worker are mirrored back into the folder. Files deleted from the folder are deleted from local R2.

No Worker code changes. No Wrangler config changes. No moving .wrangler.

How It Works

Wrangler exposes Local Explorer while wrangler dev is running:

http://localhost:8787/cdn-cgi/explorer/

The browser UI uses a local API under:

http://localhost:8787/cdn-cgi/explorer/api

r2-local-fs talks to that API instead of writing directly into Miniflare's SQLite/blob internals. The useful R2 endpoints are:

GET    /cdn-cgi/explorer/api/r2/buckets
GET    /cdn-cgi/explorer/api/r2/buckets/{bucket}/objects
GET    /cdn-cgi/explorer/api/r2/buckets/{bucket}/objects/{key}
PUT    /cdn-cgi/explorer/api/r2/buckets/{bucket}/objects/{key}
DELETE /cdn-cgi/explorer/api/r2/buckets/{bucket}/objects

Current Status

This repository contains a working Python implementation using only the Python standard library.

Implemented:

  1. Local Explorer API client.
  2. Bucket discovery.
  3. Project config generation with init.
  4. pull from local R2 to a normal folder.
  5. push from a normal folder to local R2.
  6. Continuous watch/on reconciliation.
  7. Direct remote delete when a mirrored local file is deleted.
  8. Stable-file detection before upload.
  9. Manifest-based drift detection.

Not implemented:

  1. Native filesystem event acceleration.
  2. Concurrent upload/download workers.
  3. S3-compatible AWS CLI endpoint.
  4. Packaged PyPI release.

Requirements

  • Python 3.11+
  • A running Wrangler dev server
  • Local Explorer available at the Wrangler dev endpoint

Run From This Checkout

Start your Worker as usual:

cd /path/to/your-worker
npm run dev

In this repository, list local R2 buckets:

PYTHONPATH=src python3 -m r2_local_fs buckets \
  --endpoint http://localhost:8787

Initialize a facade folder:

PYTHONPATH=src python3 -m r2_local_fs init \
  --endpoint http://localhost:8787 \
  --bucket wk-prod \
  --dir ~/R2/wk-prod

init creates:

.r2-local-fs.json
~/R2/wk-prod/.r2-local-fs/manifest.json

After init, run commands from the same directory without repeating --bucket, --dir, or --endpoint:

PYTHONPATH=src python3 -m r2_local_fs pull
PYTHONPATH=src python3 -m r2_local_fs push
PYTHONPATH=src python3 -m r2_local_fs on

You can also run without config by passing all options:

PYTHONPATH=src python3 -m r2_local_fs watch \
  --endpoint http://localhost:8787 \
  --bucket wk-prod \
  --dir ~/R2/wk-prod

Commands

buckets

Lists local R2 buckets exposed by the running Wrangler dev server.

PYTHONPATH=src python3 -m r2_local_fs buckets \
  --endpoint http://localhost:8787

init

Creates the facade directory, creates an empty manifest if needed, and writes .r2-local-fs.json in the current directory.

If there is exactly one local R2 bucket, --bucket can be omitted.

PYTHONPATH=src python3 -m r2_local_fs init \
  --endpoint http://localhost:8787 \
  --dir ~/R2/wk-prod

pull

Downloads local R2 objects into the facade directory.

PYTHONPATH=src python3 -m r2_local_fs pull

push

Uploads files from the facade directory into local R2.

PYTHONPATH=src python3 -m r2_local_fs push

watch / on

Continuously reconciles the facade directory and local R2.

PYTHONPATH=src python3 -m r2_local_fs on

on is an alias for watch.

By default, watch mode reconciles every 5 seconds:

PYTHONPATH=src python3 -m r2_local_fs on --remote-poll-ms 1000

Sync Behavior

The facade folder is treated as the developer-facing filesystem view of the bucket.

When a file is created or changed:

~/R2/wk-prod/blog/foo.json

the tool uploads it as:

blog/foo.json

When a file is deleted from the facade folder, the corresponding local R2 object is deleted. There is no confirmation prompt and no trash mode.

When the Worker writes to R2:

await env.BUCKET.put("blog/bar.json", body);

the tool downloads it to:

~/R2/wk-prod/blog/bar.json

Directory-marker objects ending in / are ignored as files.

Bulk Copy Behavior

Current watch mode is reconciliation polling, not native filesystem events. Every reconciliation scans the local folder, lists local R2 objects, compares both sides to the manifest, and applies the needed changes.

Before upload, a changed local file must be stable. The default is 1000ms:

PYTHONPATH=src python3 -m r2_local_fs on --stable-file-ms 1000

If hundreds of files are copied into the facade folder over a few minutes, each poll sees the files that are present, waits for changed files to stop changing, and uploads them. Later polls catch files that were still being copied or missed by an earlier reconciliation.

Uploads and downloads are currently sequential.

Manifest

The manifest lives inside the facade folder:

~/R2/wk-prod/.r2-local-fs/manifest.json

It records the last synced remote metadata and local file metadata for each key:

{
  "bucket": "wk-prod",
  "endpoint": "http://localhost:8787",
  "objects": {
    "blog/foo.json": {
      "etag": "3a134f8ae04aae02b05fce3b77550e64",
      "size": 326,
      "last_modified": "2026-05-04T01:22:23.884Z",
      "local_mtime_ns": 1777857743884000000,
      "local_size": 326,
      "synced_at": 1777935600.0
    }
  }
}

The manifest is what lets the tool distinguish:

  • a new local file that should be uploaded
  • a new remote object that should be downloaded
  • a local deletion that should delete remote
  • a remote deletion that should delete local
  • a conflict where both sides changed since the last sync

Conflict remote copies are preserved under:

~/R2/wk-prod/.r2-local-fs/conflicts/

Why Not Edit .wrangler/state/v3/r2 Directly?

Wrangler local R2 is backed by Miniflare persistence internals. In current Wrangler state, the object table contains fields like:

key
blob_id
version
size
etag
uploaded
checksums
http_metadata
custom_metadata

The blob_id points at files in the bucket's blobs/ directory. The visible blob filename is not the R2 object key.

Directly writing those SQLite rows and blob files would be brittle, especially while wrangler dev is running. Cloudflare can change that internal format, and concurrent writes could corrupt local state.

r2-local-fs lets Wrangler own .wrangler/state/v3/r2/ and talks through the same local interface used by Local Explorer.

Development

Run tests:

PYTHONPATH=src python3 -m unittest discover -s tests -v

Compile-check the package:

python3 -m compileall -q src tests

The package exposes a console script in pyproject.toml:

r2-local-fs = "r2_local_fs.cli:main"

Editable install works with standard Python packaging tools:

python3 -m pip install -e .
r2-local-fs --help

Non-Goals

  • Replace Wrangler.
  • Require changes to Worker code.
  • Require changes to wrangler.toml.
  • Move or rewrite .wrangler/state/v3/r2.
  • Write directly to Miniflare SQLite/blob internals during normal sync.
  • Emulate the full S3 API.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

r2_local_fs-0.1.0.tar.gz (17.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

r2_local_fs-0.1.0-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file r2_local_fs-0.1.0.tar.gz.

File metadata

  • Download URL: r2_local_fs-0.1.0.tar.gz
  • Upload date:
  • Size: 17.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for r2_local_fs-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5a76e0c1e740170596a4e8b9e0a3cd03366b1132ba42ea75c8f15ab5d6fffbd3
MD5 2f6da40573d051c9051f3fa95087bfdc
BLAKE2b-256 449fd512b0143d72621bd302e446b01d97cb1e123679a075e90f48015f2df420

See more details on using hashes here.

File details

Details for the file r2_local_fs-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: r2_local_fs-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 15.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for r2_local_fs-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 48984e8813942c0b3ea9c7221052a4c2e2a120990d4b1588d8cda344c6cfb833
MD5 709b98ab07a9dafa929af09ac5dc2e0c
BLAKE2b-256 f3a671b5044861f204012ccf78a53007e2bc49d1b78d16cd76222a57f9277af3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page