2026-07-10 04:46:34 +00:00
2026-03-18 02:55:58 +08:00
2026-07-10 04:46:34 +00:00
2026-07-10 04:46:34 +00:00
2026-03-18 04:41:57 +08:00
2026-07-10 04:46:34 +00:00
2026-07-10 04:46:34 +00:00
2026-07-10 04:46:34 +00:00
2026-07-10 04:46:34 +00:00
2026-07-10 04:46:34 +00:00
2026-07-10 04:46:34 +00:00
2026-07-10 04:46:34 +00:00
2026-07-10 04:46:34 +00:00

ushort

ushort is a compact URL shortener written in Rust and backed by SQLite. It is a behavior-compatible replacement for the original stdlib-only Python service: the routes, base-path handling, response shapes, database schema, retention, API-key checks, throttling, redirects, and frontend are preserved.

The frontend under static/ is embedded into the executable at compile time. The final Linux image contains only one statically linked executable and runs without Python, a shell, a package manager, or external static files.

Quick start

cp config.example.toml config.toml
# Edit base_url and api_key, then:
docker compose up --build -d

The service listens on 127.0.0.1:18082 on the host. Its SQLite database is stored at ./data/urlshort.db and survives container replacement.

The container runs as UID/GID 1001:1001 by default. Override USHORT_UID/USHORT_GID if needed, and ensure ./data is writable by that identity.

Configuration

TOML is the preferred format; see config.example.toml. Existing JSON files remain supported, including the old short_length alias for min_short_length.

ushort config.toml
ushort legacy-config.json
Key Required Default Description
base_url yes Public URL. Its path becomes the routing prefix.
api_key yes Secret used by list and delete operations.
host no 0.0.0.0 Bind address.
port no 8080 Bind port.
db_path no data/urlshort.db SQLite database path.
retention_days no 0 Default lifetime for new URLs; zero never expires.
min_short_length no 6 Initial generated code length.
max_short_length no 32 Maximum generated and accepted code length.
max_url_length no 2048 Maximum original URL length.
max_retention_days no 3650 Maximum per-URL retention value.
rate_limit_requests no 60 Requests allowed per client/window.
rate_limit_window no 60 Sliding-window length in seconds.
production no false When true, disable embedded frontend routes for compatibility with deployments that serve static files separately.

For example, base_url = "https://example.com/go/links" limits routing to /go/links and produces URLs such as https://example.com/go/links/aB3xYz. Requests outside that prefix return 404.

Real API keys belong only in the deployment's ignored config.toml, never in the repository or container image.

API

All paths below are relative to the path in base_url. Trailing slashes are accepted. Every response includes the legacy CORS and security headers.

Health and frontend

  • GET / serves the byte-identical embedded frontend when production=false.
  • GET /api/health returns HTTP 200:
{"status":"ok","service":"url-shortener"}

When the configured base path is non-empty, a GET of the bare path (for example /s) redirects to /s/.

Create a short URL

POST /api/shorten and GET /api/shorten are both supported and do not require an API key. Fields may be supplied as query parameters or in a JSON body; query parameters take precedence.

Field Required Description
url yes Absolute http:// or https:// URL.
retention_days no Per-URL lifetime, or zero to keep forever.

Success is HTTP 201 with only the short URL as UTF-8 plain text:

https://example.com/s/aB3xYz

Codes begin at min_short_length. After ten collisions at one length the service tries the next length, through max_short_length.

List URLs

GET /api/urls?api_key=<key> returns all rows newest first. A missing or invalid key returns HTTP 403 with an empty body.

{
  "count": 1,
  "urls": [
    {
      "short_code": "aB3xYz",
      "short_url": "https://example.com/s/aB3xYz",
      "original_url": "https://example.org",
      "created_at": 1710000000,
      "visit_count": 5,
      "retention_days": 30
    }
  ]
}

Metadata and lookup

  • GET /api/urls/<code> returns one metadata object without authentication.
  • GET /api/lookup?url=<encoded-url> returns the newest matching metadata object without authentication.
  • A missing record returns HTTP 404 with an empty body.

Delete

DELETE /api/urls/<code>?api_key=<key> returns:

  • HTTP 204 with an empty body on success;
  • HTTP 404 with an empty body when the code does not exist; or
  • HTTP 403 with an empty body when authentication fails.

Redirect

GET /<code> atomically increments visit_count and responds with HTTP 302 to the stored original URL.

Data compatibility

The existing database can be mounted directly; no export/import is required. The schema remains:

CREATE TABLE urls (
    short_code     TEXT    PRIMARY KEY,
    original_url   TEXT    NOT NULL,
    created_at     INTEGER NOT NULL,
    visit_count    INTEGER NOT NULL DEFAULT 0,
    retention_days INTEGER NOT NULL DEFAULT 0
);

Older databases missing retention_days are upgraded in place with a default of zero. Expired records are removed on startup and before each GET, POST, or DELETE request, using the original strict expiry boundary.

Before a production cutover, make a filesystem-consistent backup of the database (or use SQLite's backup command) and test the copy with the new image.

Frontend

The existing index.html, JavaScript, CSS, locally hosted fonts, sorting, pagination, theme selection, URL lookup, copy/delete actions, and admin flow are unchanged. Compile-time embedding makes those files part of the standalone artifact; no bind mount or separate web root is needed.

For the existing /s deployment, place the location blocks from nginx.conf in the public vhost. They proxy all traffic to the binary while retaining the current no-store HTML and no-cache asset policies.

Build and test

The repository intentionally does not require a host Rust installation.

docker build -t ushort:local .
docker compose up -d

Run the Rust test suite in an isolated build stage:

docker build --target tester .

The Dockerfile builds against musl and uses scratch for the runtime image. Docker Buildx can publish both required architectures from the same source:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t sodium/ushort:latest \
  --push .

Security model

  • SQL statements use bound parameters.
  • Short codes are restricted to ASCII alphanumerics.
  • URL, retention, code, and request-body sizes are bounded.
  • API-key comparison is constant-time; protected operations return no body on authentication failure.
  • Per-IP sliding-window throttling applies to API calls and redirects; embedded frontend files do not consume API quota. The app uses X-Real-IP, then the first X-Forwarded-For value, then the TCP peer. Keep the container port bound to loopback and let the trusted reverse proxy overwrite those headers.
  • CORS preflight uses OPTIONS; it does not consume rate-limit quota.
  • The runtime image is read-only, drops Linux capabilities, and runs as a numeric non-root user through Compose.
  • SIGINT/SIGTERM cleanly unblock the server for prompt container shutdown.
S
Description
No description provided
Readme
4.8 MiB
Languages
CSS 84.8%
Rust 10.4%
JavaScript 3.9%
HTML 0.5%
Shell 0.3%
Other 0.1%