An hourly mirror of all of CTAN on Cloudflare R2.
This repository is an hourly mirror of all of CTAN on Cloudflare R2. The
mirror serves each CTAN path at the root of https://ctan.katoptra.org/. It contains
approximately 514,000 files and 140 GB. A list diff copies them from the master of CTAN.
Each hour, a run gets the listing of upstream and compares it with the files in the bucket.
Then the run moves only the difference.
The cost is approximately $2.10 a month, and all of it is for storage. The runs are GitHub Actions jobs, and an external scheduler starts them.
TeX Live and TinyTeX use tlmgr. To use this mirror, set it as the repository of tlmgr.
Then update TeX Live:
tlmgr option repository https://ctan.katoptra.org/systems/texlive/tlnet/
tlmgr update --self --allFor a new installation, give the same URL to the installer:
install-tl -repository https://ctan.katoptra.org/systems/texlive/tlnet/A directory URL shows the files that the mirror contains in that directory, for example
https://ctan.katoptra.org/systems/knuth/. The root serves the index page of CTAN.
To let CTAN select a mirror again, run tlmgr option repository ctan.
Is it fresh? The master of CTAN writes its clock to timestamp each hour. The mirror copies
timestamp the same as each other file, and the mirror monitor of CTAN reads it:
curl -s https://ctan.katoptra.org/timestampEach hour, a GitHub Actions job runs this pipeline in the toolbox image from katoptra/lib. Each box is a verb of the toolbox or of the rsync engine in lib. This mirror adds no verbs.
flowchart LR
clock --> due --> list --> state --> rebuild --> diff --> split --> prepare --> batches
subgraph b["batches: the first MAX_BATCHES of the delta, each committed before the next"]
direction LR
fetch --> verify --> publish --> checkpoint
end
batches --> b --> delete --> reconcile --> index --> smoke --> report --> ping
This mirror sets these root vars in Taskfile.yml:
SOURCEis the master of CTAN,rsync.dante.ctan.org(dante).HOSTandBUCKETare the domain and the bucket of the mirror.TLis the signed TeX Live subtree,systems/texlive/tlnet.TL_KEYis the fingerprint of the primary key of TeX Live. The engine examines the SHA-512 checksum and the GPG signature oftexlive.tlpdb. It also examines each installer and each updater at the root ofTL, for exampleupdate-tlmgr-latest. On CTAN, only these files have a signature that the mirror can pin. The mirror copies each other file on CTAN byte for byte, with tlcontrib and the ISO images of TeX Live.CEILING_GBis 200. If upstream is more than 200 GB, a run does not start.LIST_FLOORis 460,000, approximately 90% of a listing of CTAN. A listing that does not have more than 460,000 lines stops the run, because a truncated listing must not become a list of deletions.INDEXis<HOST>.directory.index.html. For this mirror, it isctan.katoptra.org.directory.index.html. With it, the engine makes a page for each directory, at two keys:<dir>/ctan.katoptra.org.directory.index.htmlfor/dir/, and<dir>for/dir.PAGE_FOOTputs a link to the same directory on ctan.org at the end of each page.docs/reference.mdsection 7 gives the cause of the two keys.CANARYisbiblio/bibtex/contrib/german/dinat/dinat-index.html. This HTML file hashttp://links (nothttps://) andmailto:addresses, which the HTML rewriters of the zone change. The canary also finds a zone that rejects a Perl client.FRESH_KEYistimestamp, in which the master writes its clock each hour.FRESH_HOURSis 6. The mirror is hourly. Thus, if the clock does not change for six hours, the master stopped, or the copy in the mirror stopped. Then the run stops with an error.
The engine does all other work:
- The list diff
- The batches
- The signature checks
- The state file
- The daily reconcile.
lib, The rsync engine tells how the engine does this work, and how it uses the vars in this section.
Fork katoptra/ctan. In Taskfile.yml, change these two
lines:
HOST: set it to your domain.BUCKET: set it to the name of your bucket.
Do not change SOURCE. CTAN tells each mirror to get its files from the master.
The bucket is the mirror. Each CTAN path is at the root of the bucket, with its name on
CTAN. The bucket also has one reserved prefix, .state/, for the listing that the last run
put there. Storage is the full cost. On R2, the storage cost of approximately 140 GB is
approximately $2.10 a month, at $0.015 for each GB-month.
| What | Why |
|---|---|
| An R2 bucket, or a different S3-compatible bucket | The objects and their state |
| An API token with Object Read & Write, for that bucket only | The three AWS_* values in step 3 |
A custom domain on the bucket, which is HOST |
The clients and the read-back checks fetch from it |
The rsync image of lib has an aws.config at /etc/aws.config. This file makes the AWS CLI
send each file smaller than 4 GiB as one PutObject. The CLI sends the five larger CTAN files
in parts of 512 MiB. lib, Storage tells how the
engine uses a bucket, and it gives the contents of .state/. It also gives the cause for a
state that is only a cache of the bucket.
Put four values in one vault item, with the name ctan:
| Section | Field | What it is | Variable in the run |
|---|---|---|---|
r2 |
access_key_id |
The token from step 2 | AWS_ACCESS_KEY_ID |
r2 |
secret_access_key |
Its secret | AWS_SECRET_ACCESS_KEY |
r2 |
endpoint |
https://<account-id>.r2.cloudflarestorage.com |
AWS_ENDPOINT_URL |
healthcheck |
url |
Optional: a healthchecks.io ping URL | HEALTHCHECK_URL |
Then do these steps:
- Put the UUID of your vault in the four references in
op.env. - Make a service account that can read that vault.
- Store its token as the
OP_SERVICE_ACCOUNT_TOKENsecret, on the organization or on the repository.
No other configuration on GitHub is necessary. lib, Secrets tells you:
- How to find the UUID of a vault
- The cause for a UUID in the references, and not a name
- How to use repository secrets as an alternative.
The zone has four Cloudflare rules. Each rule is applicable only to the hostname of the
mirror. The rules are not part of the pipeline: you set them one time, and the pipeline does
not change them. docs/reference.md section 6 gives the expression of each rule and the
measurement for it.
| Rule | What it does |
|---|---|
| Configuration Rule | It sets Email Obfuscation, Rocket Loader, Automatic HTTPS Rewrites and Browser Integrity Check to off. The first three change the HTML while it goes to the client. The fourth sends a 403 to Perl and Python clients. If one of the four operates again, the canary stops the run. |
| Cache Rule | Bypass. With less than 10M requests a month, a cache does not decrease the cost. A cache also serves a previous copy of /timestamp. |
| Transform Rule | / serves the index.html of CTAN. |
| Transform Rule | /dir/ serves the page of that directory. |
-
On a laptop with go-task, the 1Password CLI, and Docker or Apple
container, run this command:task check # render each command of the pipeline in the image, then compare it with render.txt -
Before the first run, pause the healthchecks.io check. If you do not pause the check, it sends an alert, because the first fill is longer than the grace.
-
In Actions, select the sync workflow.
-
Click Run workflow.
The first run finds an empty bucket. Thus, the delta is the full tree. Each run does four batches of the delta and then starts the next run. This continues until all batches are done. The first fill copies approximately 140 GB from CTAN, in a chain of runs. After the first fill, each run moves the delta of one hour, usually a small number of files.
No file in this repository schedules a run. To start runs, do one of these steps:
- Add a
schedule:trigger to.github/workflows/sync.yml, with a minute that you select. - Dispatch the workflow from an external scheduler, the same as this mirror.
CTAN tells each mirror to get the changes one time each hour, at the same minute.
task with no task name prints the menu. Put the flags of a run after --. Put the same flags
in the vars input of the workflow:
task sync # one run, the same as a run in Actions
task sync -- MAX_BATCHES=8 # more batches in one run
task sync -- RECONCILE=true # a reconcile in this run: make the state again from the bucket, and delete orphans
gh workflow run sync.yml # one run in Actions
gh workflow run sync.yml -f vars='RECONCILE=true' # a run in Actions, with a reconcileEach run adds one table to its job page. The table shows these items:
- When the run started, and how many minutes it continued
- The delta, and the files that the run uploaded
- The state and the storage
- The signature check
- The directory pages that the run made again
- The clock in
timestamp, and its limit of 6 h.
A failed run is the only alert. If a slot gets no ping in the time of the grace, healthchecks.io sends an email. The entries that follow are for two conditions:
- The time in
timestampdid not change for six hours. This shows that dante does not update. Examine mirmon and dante. No change in this repository is necessary. For the other engine verbs that can stop a run, refer to lib, When a run fails. - The run did not start. Nothing in this repository starts a run. Examine the scheduler
(katoptra/dispatch). Then
use
gh workflow list --allto find a manual stop: the state isdisabled_manually. Until the scheduler operates again, start runs withgh workflow run sync.yml.
docs/reference.md section 5 is the runbook of this mirror. It has an
entry for each of these conditions:
- A run that stopped with an error
- The first fill
- A state file that is not correct
- A key to delete
- Directory pages to make again
- A secret to change
- A scheduler that stopped.
docs/reference.md contains the numbers of the mirror. Each number has
the date of its check:
- Baseline: the measured tree, the files that change each month, and the hours with the most changes
- Limits: R2, the Cloudflare zone, Actions, the master of CTAN, the tools
- Cost: the cost of each item, and the cost of the traffic for a part of CTAN
- Monitoring: the healthcheck and its configuration
- Runbook
- Zone configuration: each rule and its expression
- Why directory pages: listings at two keys.
Pull requests are welcome.
MIT licensed. Built by Josh Vaughen.