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.cloudflare.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
repomatic.cloudflare.api.account() derives from CLOUDFLARE_API_TOKEN.
--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.
- 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 a message about a dropped file or a missing key sends the reader to set up R2.
- repomatic.cloudflare.r2.OFFLOAD_DOCS_LINK: Final = '[big file offloading](https://repomatic.net/cloudflare#files-over-25-mib)'¶
OFFLOAD_DOCS_URLas the Markdown link that ends a dropped file’s reason.
- 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.SUMMARY_HEADING: Final = '### Files over the Cloudflare Pages size limit'¶
Heading of the table
OffloadReport.markdown()renders.
- class repomatic.cloudflare.r2.R2Mode(*values)[source]¶
Bases:
StrEnumWhat one run of
cloudflare-r2does, named after its flag.- CHECK = 'check'¶
Compare the live bucket against the declared state.
- CREATE = 'create'¶
Create the bucket when missing, and bring it to the declared state.
- OFFLOAD = 'offload'¶
Move the files over the Pages limit out of a built site, to the bucket.
- 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.
- class repomatic.cloudflare.r2.OffloadReport(root, results=())[source]¶
Bases:
objectWhat
--offloaddid to the files of one built site.- property exit_code: int¶
1when a file was dropped, else0.The tree is ready to deploy either way: the code only says whether some links are now dead.
- announce()[source]¶
Print where each file went, with an error annotation for each drop.
- Return type:
- markdown()[source]¶
Render one table row per file, under
SUMMARY_HEADING.- Return type:
- Returns:
The Markdown report, empty when no file was over the limit.
- repomatic.cloudflare.r2.oversized_files(paths)[source]¶
Keep the files Cloudflare Pages Direct Upload rejects.
- Parameters:
paths (
Iterable[Path]) – Candidate paths, like therglob("*")of a built site. Directories are skipped.- Return type:
- Returns:
The files over
PAGES_MAX_FILE_SIZE, sorted.
- 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 as each file’s reason.
- 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. Prints where each file went, and leaves the summary table to the caller: see
OffloadReport.markdown().- Parameters:
- Return type:
- Returns:
What happened to each file.
- repomatic.cloudflare.r2.run_cloudflare_r2(project, bucket, domain, mode)[source]¶
Create or check the bucket that serves the site’s oversized files.
--createmakes the bucket when missing, attaches the domain with the TLS floor, and turns ther2.devURL off. An existing bucket is reused and brought to the same state, so a re-run is safe.- Parameters:
project (
str) – Pages project of the site, which resolves the account.bucket (
str) –site.cloudflare-r2-bucket.domain (
str) –site.cloudflare-r2-domain.mode (
R2Mode) –R2Mode.CHECKto report drift from the declared state, orR2Mode.CREATEto fix it.run_offload()serves the third mode.
- Return type:
- Returns:
Exit code:
0when the bucket matches the declared state,1on drift or on a step left to do by hand.