- Shell 100%
|
|
||
|---|---|---|
| .gitignore | ||
| AGENTS.md | ||
| cdn-mirror | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.md | ||
cdn-mirror
A Bash script that maintains a mirror of remote files in a Cloudflare R2 bucket for CDN distribution. Files are only re-uploaded when their content has changed (detected by SHA-256 comparison), making it safe to run frequently or on a cron schedule.
Requirements
| Tool | Purpose |
|---|---|
bash ≥ 3.2 |
Shell runtime |
curl |
Downloading source files |
jq |
Parsing the JSON config |
aws CLI v2 |
Uploading to R2 via the S3-compatible API |
sha256sum |
Content-change detection (part of GNU coreutils) |
Installing dependencies
# Debian / Ubuntu
sudo apt-get install curl jq coreutils
# AWS CLI v2 — see https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html
Installation
sudo curl -fsSL https://code.paulg.it/paulgit/cdn-mirror/raw/branch/main/cdn-mirror -o /usr/local/bin/cdn-mirror && sudo chmod +x /usr/local/bin/cdn-mirror
Configuration
The script reads a JSON configuration file. The default location is:
~/.config/cdn-mirror/config.json
If the file does not exist, the script will offer to create a template. The config file should be readable only by its owner:
chmod 600 ~/.config/cdn-mirror/config.json
Config file format
{
"r2": {
"account_id": "YOUR_CLOUDFLARE_ACCOUNT_ID",
"access_key_id": "YOUR_R2_ACCESS_KEY_ID",
"secret_access_key": "YOUR_R2_SECRET_ACCESS_KEY",
"bucket": "YOUR_BUCKET_NAME",
"region": "auto"
},
"files": [
{
"urls": [
"https://cdn1.example.com/path/to/file.js",
"https://cdn2.example.com/path/to/file.js"
],
"destination": "js/file.js"
},
{
"url": "https://example.com/path/to/style.css",
"destination": "css/style.css"
}
]
}
| Field | Required | Description |
|---|---|---|
r2.account_id |
Yes | Your Cloudflare account ID |
r2.access_key_id |
Yes | R2 API token access key ID |
r2.secret_access_key |
Yes | R2 API token secret access key |
r2.bucket |
Yes | R2 bucket name |
r2.region |
No | R2 region (default: "auto") |
files[].urls |
Yes* | Array of source URLs to try in order (fallback) |
files[].url |
Yes* | Single source URL (backward-compatible alternative) |
files[].destination |
Yes | Object key in the R2 bucket |
* Either urls or url must be provided. If urls is present, the
script tries each URL in order until one succeeds, retrying each up to
three times before moving to the next.
R2 API tokens can be created in the Cloudflare dashboard under R2 → Manage R2 API Tokens. The token needs Object Read & Write permission on the target bucket.
Usage
Usage: cdn-mirror [OPTIONS]
Options:
-c, --config <path> Path to JSON config file.
Default: ~/.config/cdn-mirror/config.json
--cronmode Suppress all output except errors.
-v, --version Show version number and exit.
-h, --help Show this help message and exit.
Normal run (verbose)
./cdn-mirror
Custom config path
./cdn-mirror --config /path/to/my-config.json
Cron mode
Suppresses all output except errors, making it suitable for unattended
execution. The script exits with code 1 if any file failed to download
or upload.
./cdn-mirror --cronmode
Example crontab entry (run every hour)
0 * * * * /usr/local/bin/cdn-mirror --cronmode
How it works
- For each file listed in the config, the script downloads the source URL to a temporary file. If multiple URLs are configured, it tries each in order until one succeeds (retrying each up to three times).
- It computes the SHA-256 hash of the downloaded content.
- The hash is compared against the value recorded from the previous run,
stored in
~/.local/share/cdn-mirror/hashes.json. - If the hash has changed (or no previous run exists), the file is uploaded to R2 and the new hash is saved.
- If the hash matches, the upload is skipped.
Exit codes
| Code | Meaning |
|---|---|
0 |
All files processed successfully |
1 |
One or more files failed to download or upload |
License
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE — see LICENSE.