bunsy: Sync Local Folders to Bunny CDN Storage

4 min read 675 words Source

I run this website and a few other projects on Bunny CDN: the production setup is a Storage Zone for assets and pull-through to edge. After each build I need the same loop over and over — walk the local output tree, compare it to what Bunny already has, upload what changed, and optionally remove objects that no longer exist locally. That workflow is second nature with aws s3 sync; for Bunny’s HTTP storage API I wanted something equally boring and reliable, not a one-off shell script.

I wrote bunsy (BUNny sync) for that: a small Rust CLI that talks to Bunny’s storage API, does size-based diffing, runs uploads and deletes with bounded concurrency, and can show a live progress UI on a real terminal. A Docker image sbstjn/bunsy (Debian bookworm-based) is on Docker Hub so CI can run the same binary without installing Rust. The sources live on Codeberg under the MIT licence.

Getting Started

The storage zone password is the same value Bunny shows under FTP & API Access in the dashboard. Export it as BUNNY_STORAGE_ZONE_PASSWORD (bunsy does not read CLI flags for secrets).

Basic usage: pass the local directory and the remote target. The remote argument is either the zone name alone or zone-name/path/inside/zone:

$ > BUNNY_STORAGE_ZONE_PASSWORD='…' \
  bunsy ./dist my-website-files

$> BUNNY_STORAGE_ZONE_PASSWORD='…' \
  bunsy ./dist my-website-files/assets/v2

The client uses HTTPS against storage.bunnycdn.com by default; you can point --endpoint at a regional hostname if Bunny assigns one for your zone.

Sync Behaviour

bunsy lists the local tree and fetches the remote listing (via the storage API), then builds a plan keyed by relative path and compared by file size only — no content hash is involved in deciding whether to upload unless you opt into a full re-upload path.

In practice:

  • Upload if the object is missing remotely, or if local and remote sizes differ.
  • Skip if the object exists and the size matches (unless you pass --ignore-size, which queues every local file for upload even when sizes match — useful when content changed without a size change).
  • Delete remote objects that have no corresponding local path, unless --no-delete is set (then orphans stay on the server).

Uploads run first; deletes run after all uploads finish, so you do not remove remote files while replacements are still in flight.

--dry prints the plan (counts and per-file lines when not quiet) without mutating storage.

Concurrency and Progress

Parallel work is bounded by a semaphore: --concurrency defaults to 8. Each upload or delete acquires a permit; failures propagate like any other error.

For long runs on an interactive terminal, --progress draws an overall bar plus one line per active slot on stderr (via indicatif). If stderr is not a TTY (pipelines, logs), the UI is suppressed so you do not get ANSI noise. --progress and --quiet / -q are mutually exclusive — the binary exits with a configuration error if both are set.

$ > BUNNY_STORAGE_ZONE_PASSWORD='…' \
  bunsy ./dist my-zone --progress --concurrency 12

Checksum Verification

Bunny accepts a Checksum header on PUT with uppercase SHA-256 hex. With --checksum, bunsy hashes each file before upload and sends that header so the API can reject corrupted transfers.

$ > BUNNY_STORAGE_ZONE_PASSWORD='…' \
  bunsy ./dist my-zone --checksum

There is a real cost: every queued upload reads the full file from disk for hashing. For large trees, use it when you care about integrity guarantees or debugging flaky networks.

CI Integration

The Docker image packages the same CLI as cargo install bunsy. In Woodpecker (or similar), wire the password from a secret and invoke bunsy after your build step:

# Woodpecker: publish artefacts after build
publish:
  image: sbstjn/bunsy
  commands:
    - bunsy ./dist my-website-files
  environment:
    BUNNY_STORAGE_ZONE_PASSWORD:
      from_secret: BUNNY_STORAGE_ZONE_PASSWORD
  when:
    event: [push]
  depends_on:
    - build

./dist is whatever directory your pipeline produces; adjust my-website-files to your storage zone name (and optional sub-path).

If Bunny-powered static hosting is part of your workflow, give bunsy a try!

You you can clone or install it from Codeberg 🎉