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.
_redirectscannot proxy to another host (status200only 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
301would 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_redirectsbefore 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
PUTwrites 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_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,
hostandx-amz-dateincluded. 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, asYYYYMMDDTHHMMSSZ.region (
str) – Region of the credential scope.service (
str) – Service of the credential scope.
- Return type:
- Returns:
The value of the
Authorizationheader.
- class repomatic.cloudflare_r2.R2Bucket(account_id, name, access_key_id, secret_access_key)[source]¶
Bases:
objectOne R2 bucket, reached through the S3 API with a bucket-scoped key pair.
- class repomatic.cloudflare_r2.Offload(path, size, action, url='', reason='')[source]¶
Bases:
objectWhat happened to one file over the Pages limit.
- action: ReportAction¶
UPLOADED,SKIPPEDwhen the bucket already held it, orDROPPED.
- repomatic.cloudflare_r2.oversized_files(root)[source]¶
Files under root that Cloudflare Pages Direct Upload rejects.
- 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, aswrangler pages deploywould upload it.files (
Sequence[Path]) – Files under root over the Pages limit.bucket (
R2Bucket|None) – Where the files go.Nonedrops them all.domain (
str) – Host that serves bucket.unavailable (
str) – Why bucket isNone, repeated in each warning.
- Return type:
- 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:
- Return type:
- Returns:
1when a file was dropped, else0. 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 exit1on any.create (
bool) – Create the bucket when missing, attach the domain with the TLS floor, and turn ther2.devURL off. An existing bucket is reused and brought to the same state, so a re-run is safe.
- Return type:
- Returns:
Exit code:
0when the bucket matches the declared state,1on drift or on a step left to do by hand.