aboutsummaryrefslogtreecommitdiff

hopper

hopper is a small command-line tool that deploys content-hashed web assets from a gzipped tarball to Bunny CDN Storage, and prunes obsolete files from a storage zone.

How hopper Thinks About Your Assets

Modern site builders produce content-hashed asset filenames. The name embeds a hash of the file's contents, e.g. assets/app.a1b2c3.js. Because the hash changes whenever the bytes change, a given filename always maps to exactly the same bytes forever. hopper calls these files immutable assets.

The exception is HTML files (e.g. index.html, 404.html). They are the entry points that reference the immutable assets, they keep the same name across releases, and their contents change on every deploy. hopper calls these mutable assets.

hopper categorizes every *.html file (case-insensitive) as a mutable asset and every other file as an immutable asset. To save bandwidth, immutable assets are uploaded only if the zone does not contain this asset already or the sizes differ. Mutable assets, on the other hand, are always uploaded, and uploaded only after all immutable assets succeeded. This ensures that an application stays in a consistent state when an immutable asset upload fails. Re-running the upload simply resumes.

Because uploading accumulates files on the storage zone, hopper can prune old files. Immutable assets that are not touched for a very long time still keep their old modification date, so pruning files only on their age would be dangerous. Instead pruning deletes only old files which are not in the current release.

Installation

hopper is distributed as source and built with the Go toolchain, which compiles into a single static binary. To install hopper clone the repository and run make install in its root. This installs the binary to $PREFIX/bin/hopper ($PREFIX defaults to /usr/local).

Quickstart

Deploy a release archive to a Bunny storage zone:

export BUNNY_ZONE=my-zone
export BUNNY_ACCESS_KEY=your-storage-access-key

hopper push dist.tar.gz

Later, clean up files from old releases that are no longer referenced and have aged past the grace window (default: 30 days):

hopper prune dist.tar.gz
# or define a custom grace window
hopper prune --older-than=90d dist.tar.gz

Always preview a destructive run first:

hopper prune --dryrun dist.tar.gz

Usage

hopper [global flags] ⟨subcommand⟩ [flags] ⟨args⟩

hopper has exactly two subcommands: push and prune. The position of the global flags are important. They must be before any subcommand.

Global Flags

Global flags must be defined before any subcommand. Some flags also have a fallback to an environment variable, so command line arguments always overwrite the environment variables. An empty environment variable is treated as unset.

Flag Env Fallback Default Meaning
--zone BUNNY_ZONE (none) The Bunny storage zone name.
--endpoint BUNNY_ENDPOINT storage.bunnycdn.com The Bunny storage endpoint hostname.
--access-key-file BUNNY_ACCESS_KEY_FILE (none) Path to a file containing the storage access key.
--verbose (none) false Extra detail (listing walk, retries, checksums).
--quiet (none) false Print only the final summary and errors.

Note: The access key can also be provided directly using the BUNNY_ACCESS_KEY environment variable. Both, BUNNY_ACCESS_KEY_FILE and --access-key-file can overwrite this access key.

Subcommand push

hopper [global flags] push  [flags] ⟨source⟩

Uploads a new release to the storage zone. It iterates over all files of the ⟨source⟩, uploads all new immutable assets, and then uploads all mutable assets. After successfully uploading the assets, hopper checks the ratio of the number of files contained in the storage zone to the number of files in the given source. If this ratio becomes too large, a hint will be printed suggesting to run hopper prune.

Note: An empty source uploads nothing and prints a warning. It is not considered an error (i.e. it exits with code 0), because the operation is non-destructive.

Flags

Flag Meaning
--concurrency The number of concurrent uploads. By default it auto-detects a value based on the number of CPUs.
--prefix A path prefix in the storage zone. Every asset is uploaded into this directory. Default: none.
--root The subtree of the source that should be uploaded. Default: All source files.
--dryrun Run the command without any mutating operations. This flag can be used to verify the upload.

Subcommand prune

hopper [global flags] prune [flags] ⟨args⟩

Deletes obsolete files. It builds the live set from the given source and a view of the storage zone. It then deletes every remote file that is not in the live set and whose last modification is older than a certain grace window. Finally, it removes all empty directories it can find (failures here do not affect the exit code). All deletes are independent. If one fails, hopper keeps going deleting all other obsolete files.

Note: If the live set is empty, the prune operation refuses to run, because it could delete the entire zone. In this case, please check your --root flag and the source.

Flags

Flag Meaning
--concurrency The number of concurrent deletes. By default it auto-detects a value based on the number of CPUs.
--prefix A path prefix in the storage zone. Only assets in this directory are deleted. Default: none.
--root The subtree of the source that defines the live set. Default: All source files.
--older-than The grace window that is used to determine obsolete files. Only files older than this will be deleted. Default: 30 days.
--dryrun Run the command without any mutating operations. This flag can be used to verify the deletes.

Retry Policy

If Bunny rate limits your requests, hopper receives a 429 and reports it. In case the response contains the Retry-After header, hopper honors this header and automatically retries up to three times. Every other HTTP failure or a 429 without Retry-After fails immediately and reports an error message.

License

MIT.