repomatic.cloudflare_r2 module¶

Serve the files a static site cannot upload to Cloudflare Pages from R2.

Cloudflare Pages Direct Upload rejects any file over 25 MiB, and wrangler fails the whole deploy on the first one it meets. The --offload mode of repomatic cloudflare-r2 runs between the build and the upload. It moves each file over the limit to an R2 bucket, and the built _redirects sends the old path to the copy. The site’s sources do not change, so the same step serves a Sphinx tree, a Pelican blog or a hand-built site.

Note

Each design choice below prevents a specific failure:

  • A redirect, not a rewritten link. A redirect acts on the URL, so it needs no knowledge of the markup that produced the link. _redirects cannot proxy to another host (status 200 only rewrites a relative path). A Pages Function could, but it puts a Worker in front of every request of a static site.

  • Keys named by content. An object lives at {sha256}/{name}. A key that exists already holds the same bytes, so a re-run uploads nothing. A preview deploy cannot overwrite the object that production serves, and a browser can cache the object forever.

  • Status ``302``. The target changes each time the file changes. A browser that cached a 301 would keep fetching the old bytes.

  • Generated rules first. An exact rule is free of the 100-rule dynamic budget only above the first dynamic rule. The merged file goes through repomatic.pages_redirects before it is written.

  • HTML stays in the tree. A page served from the bucket’s host would resolve its relative links against that host.

  • The bucket is never pruned. Old Pages deployments stay online, and they still redirect to the objects they knew.

Uploads use the S3 API with a key pair limited to the one bucket (Object Read & Write), read from CLOUDFLARE_R2_ACCESS_KEY_ID and CLOUDFLARE_R2_SECRET_ACCESS_KEY. Cloudflare scopes a credential to a bucket on the S3 API only: its REST API needs Workers R2 Storage Write, which reaches every bucket of the account. The request signer is AWS Signature Version 4 on the standard library, tested against AWS’s published vectors, so no S3 SDK joins the dependencies. The S3 endpoint embeds the account ID, which comes from CLOUDFLARE_API_TOKEN the same way repomatic.cloudflare derives it.

--create and --check manage the bucket itself through the REST API. They run on a maintainer’s machine with the wrangler login session: its workers:write scope covers R2 (verified on 2026-09-30 with wrangler 4.128.0).

The offload leaves the tree ready to deploy, whatever happens. A file it cannot move (no bucket declared, credentials missing, an upload error, an HTML page) is deleted from the tree with an error annotation that says how to serve it from R2, and the command exits 1. The Docs workflow publishes everything else first, then fails the job: the dropped file’s links are dead, and a green run would hide that. A silent trim once hid such a 404 for three years.

repomatic.cloudflare_r2.PAGES_MAX_FILE_SIZE: Final = 26214400¶

Largest file Cloudflare Pages Direct Upload accepts, in bytes.

repomatic.cloudflare_r2.S3_MAX_OBJECT_SIZE: Final = 5363466240¶

Largest object one S3 PUT writes to R2, in bytes: 5 MiB short of 5 GiB.

A bigger file needs a multipart upload, which this module does not implement.

repomatic.cloudflare_r2.ACCESS_KEY_ID_ENV: Final = 'CLOUDFLARE_R2_ACCESS_KEY_ID'¶

Environment variable holding the bucket-scoped S3 access key ID.

repomatic.cloudflare_r2.CACHE_CONTROL: Final = 'public, max-age=31536000, immutable'¶

Cache header of every uploaded object. Its key names its bytes, so it never changes.

repomatic.cloudflare_r2.EMPTY_PAYLOAD_SHA256: Final = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'¶

Payload hash of a request without a body, like HEAD.

repomatic.cloudflare_r2.MIN_TLS: Final = '1.2'¶

TLS floor of the bucket’s custom domain. R2 defaults to 1.0.

repomatic.cloudflare_r2.OFFLOAD_DOCS_URL: Final = 'https://repomatic.net/cloudflare#files-over-25-mib'¶

Where the error for a dropped file sends the reader to set up R2.

repomatic.cloudflare_r2.PAGE_SUFFIXES: Final = frozenset({'.htm', '.html'})¶

Extensions of pages, which stay out of the bucket. See the module docstring.

repomatic.cloudflare_r2.REDIRECT_STATUS: Final = 302¶

Status of the generated redirects. See the module docstring.

repomatic.cloudflare_r2.REDIRECTS_HEADER: Final = '# Files over the 25 MiB Cloudflare Pages limit, served from R2. Written by `repomatic cloudflare-r2 --offload`.'¶

Comment line above the generated rules in the built _redirects.

repomatic.cloudflare_r2.S3_REGION: Final = 'auto'¶

Region of the signature’s credential scope. R2 has no regions.

repomatic.cloudflare_r2.S3_SERVICE: Final = 's3'¶

Service of the signature’s credential scope.

repomatic.cloudflare_r2.S3_TIMEOUT: Final = 300¶

Socket timeout in seconds. An upload of hundreds of megabytes takes a while.

repomatic.cloudflare_r2.SECRET_ACCESS_KEY_ENV: Final = 'CLOUDFLARE_R2_SECRET_ACCESS_KEY'¶

Environment variable holding the bucket-scoped S3 secret access key.

repomatic.cloudflare_r2.sign_v4(method, path, headers, payload_sha256, *, access_key_id, secret_access_key, amz_date, region='auto', service='s3')[source]¶

Sign one request with AWS Signature Version 4.

headers is the complete set of headers to sign, host and x-amz-date included. The signer encodes the path once, and never normalizes it: S3 reads // and . segments literally. The query string stays empty, because no request here has one.

Parameters:
  • method (str) – HTTP method.

  • path (str) – Request path, not encoded yet.

  • headers (Mapping[str, str]) – Names and values of the headers to sign.

  • payload_sha256 (str) – Hex SHA-256 of the request body.

  • access_key_id (str) – Access key ID.

  • secret_access_key (str) – Secret access key.

  • amz_date (str) – Time of the request, as YYYYMMDDTHHMMSSZ.

  • region (str) – Region of the credential scope.

  • service (str) – Service of the credential scope.

Return type:

str

Returns:

The value of the Authorization header.

class repomatic.cloudflare_r2.R2Bucket(account_id, name, access_key_id, secret_access_key)[source]¶

Bases: object

One R2 bucket, reached through the S3 API with a bucket-scoped key pair.

account_id: str¶

Account that holds the bucket. The S3 endpoint embeds it.

name: str¶

Bucket name.

access_key_id: str¶

S3 access key ID of an Object Read & Write token.

secret_access_key: str¶

S3 secret access key of the same token.

property host: str¶

Host of the account’s S3 endpoint.

exists(key)[source]¶

Whether the bucket holds an object at key.

Return type:

bool

put(key, path, payload_sha256)[source]¶

Upload file path to key, with its content type and cache header.

The body streams from the file with its length declared: without the length, urllib switches to chunked encoding, which S3 refuses on a signed request.

Return type:

None

class repomatic.cloudflare_r2.Offload(path, size, action, url='', reason='')[source]¶

Bases: object

What happened to one file over the Pages limit.

path: str¶

Path the site served the file at, like /downloads/atlas.zip.

size: int¶

Size in bytes.

action: ReportAction¶

UPLOADED, SKIPPED when the bucket already held it, or DROPPED.

url: str = ''¶

Where the file is served from now. Empty when dropped.

reason: str = ''¶

Why the file was dropped. Empty otherwise.

repomatic.cloudflare_r2.oversized_files(root)[source]¶

Files under root that Cloudflare Pages Direct Upload rejects.

Return type:

list[Path]

repomatic.cloudflare_r2.offload(root, files, *, bucket, domain, unavailable='')[source]¶

Move files out of the built tree root, to bucket where possible.

Every file leaves the tree: Pages cannot take it either way. The ones the bucket holds get a rule in root/_redirects.

Parameters:
  • root (Path) – Built site, as wrangler pages deploy would upload it.

  • files (Sequence[Path]) – Files under root over the Pages limit.

  • bucket (R2Bucket | None) – Where the files go. None drops them all.

  • domain (str) – Host that serves bucket.

  • unavailable (str) – Why bucket is None, repeated in each warning.

Return type:

list[Offload]

Returns:

One entry per file, in the order of files.

repomatic.cloudflare_r2.run_offload(root, *, bucket, domain, project)[source]¶

Move every file over the Pages limit out of root.

Credentials and the account resolve only when a file needs them, so a site with nothing oversized makes no network call.

Parameters:
  • root (Path) – Built site, before wrangler pages deploy uploads it.

  • bucket (str) – site.cloudflare-r2-bucket, empty to drop every file.

  • domain (str) – site.cloudflare-r2-domain.

  • project (str) – Pages project, which resolves the account.

Return type:

int

Returns:

1 when a file was dropped, else 0. The tree is ready to deploy either way.

repomatic.cloudflare_r2.run_cloudflare_r2(project, bucket, domain, *, check=False, create=False)[source]¶

Create or check the bucket that serves the site’s oversized files.

Exactly one of check and create must be set; the CLI enforces that.

Parameters:
  • project (str) – Pages project of the site, which resolves the account.

  • bucket (str) – site.cloudflare-r2-bucket.

  • domain (str) – site.cloudflare-r2-domain.

  • check (bool) – Report drift from the declared state, and exit 1 on any.

  • create (bool) – Create the bucket when missing, attach the domain with the TLS floor, and turn the r2.dev URL off. An existing bucket is reused and brought to the same state, so a re-run is safe.

Return type:

int

Returns:

Exit code: 0 when the bucket matches the declared state, 1 on drift or on a step left to do by hand.