Skip to content

How trueno's spec is designed

The README (in English) covers setup and how to use the API. Here I briefly explain, in Japanese, "why it is shaped the way it is."

Premise: run where only CGI exists

The target is a shared rental server where Apache CGI is the only way to execute anything, with no resident processes and no pip. To work there, it sticks to the following.

  • Python 3.6 standard library only. It doesn't depend on the cgi module either (it was removed in 3.13)
  • No DB. All state is the files under files/ and their mtimes
  • Serving is Apache's static file serving. CGI runs only for writes and the listing

The URL tells the lifetime

At upload you choose a retention period (bucket). 1h / 1d / 1w are cleaned up by cron based on mtime; keep is never cleaned up.

bucket URL shape ID
1h 1d 1w /<bucket>/<id>.<ext> 4 characters (base62)
keep /<id>.<ext> 6 characters

Looking at the URL tells you "how long it will be around." The inversion where the shorter one lives longer is deliberate: only permanent things get the short, privileged URL. On collision a 4-character ID is retried, and it is created atomically with O_EXCL.

Sidecars stand in for a DB

The information needed for the listing and deletion (original filename, comment, delete key, time) lives in <id>.<ext>.meta (JSON) next to the file.

  • Apache does not serve .meta (denied in .htaccess)
  • Only the sha256 hash of the delete key is stored. Even if .meta leaks, the key doesn't
  • The file and its .meta share the same mtime, so cron cleans up both at once. No orphan-hunting is needed
  • Old files without a sidecar show up in the listing with their ID as the name
  • The listing links the ID and shows the original filename alongside it. If the original name is just _ (_.jpg etc.), that's taken as an intent to hide it, and it isn't shown

Two levels of authorization

  1. bearer token: one token per line in ~/.trueno-key. Each token can be restricted to certain buckets (no-keep, buckets:1h,1d). Any token can delete (operator privilege)
  2. anonymous: tokenless uploads are accepted only when the operator lists buckets for it. Opening keep to anonymous users isn't intended. Deletion uses the delete key you chose yourself at upload

The default is 1 only. Turn on 2 when you want to recreate "the old-school uploader anyone can post to."

Endpoints

Role State
POST /upload Save. time / comment / delete_key Write
GET /list Listing. Scans files/ and merges in .meta, newest first, paginated Read
POST /delete Deletes the file and .meta with the delete key or bearer Write
GET /info Limits, extensions, buckets open to anonymous users Read
GET /<...> The file itself. Apache static serving —

The listing is public. Since the URLs are already public, the listing reveals no new secrets.

Thumbnails

The image listing is a dedicated page (thumbs.html). The server doesn't make reduced versions (CGI hosts have no image library). The browser loads the original image and shrinks it for display. It works as-is on old files too.

What isn't built

Captcha, IP restrictions, download counts, accounts. Counting downloads would require serving through CGI, which breaks the "serving is static" premise.

Deployment shape

Drop in the docroot as-is. Configuration is a few lines in make.local, and make deploy bakes the public URL, site name, and anonymous buckets into each file. Some hosts ignore SetEnv in .htaccess, so it doesn't rely on environment variables.

Created 2026-10-07 by lamutara · Updated 2026-10-07 by lamutara · 6 revisions · Machine-translated from the Japanese original.