organize deployment configs and documentation
This commit is contained in:
@@ -1,234 +1,46 @@
|
||||
# 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.
|
||||
`ushort` is a compact, self-contained URL shortener written in Rust and backed
|
||||
by SQLite. It serves an embedded web interface and JSON API from a single
|
||||
statically linked executable.
|
||||
|
||||
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.
|
||||
It supports deployment at either a dedicated host such as `s.example.com` or
|
||||
under a path such as `example.com/s`. Images are available for Linux AMD64 and
|
||||
ARM64.
|
||||
|
||||
## Quick start
|
||||
|
||||
Requirements: Docker with the Compose plugin.
|
||||
|
||||
```bash
|
||||
cp config.example.toml config.toml
|
||||
# Edit base_url and api_key, then:
|
||||
docker compose up --build -d
|
||||
# Set a private api_key in config.toml.
|
||||
|
||||
mkdir -p data
|
||||
USHORT_UID="$(id -u)" USHORT_GID="$(id -g)" \
|
||||
docker compose -f deploy/docker-compose.build.yml up --build -d
|
||||
```
|
||||
|
||||
The root `docker-compose.yml` is the local-development definition and builds
|
||||
the checked-out source. `deploy/compose.production.yml` is the source-free
|
||||
server definition and pulls a versioned image; copy it to the deployment host
|
||||
as `docker-compose.yml`.
|
||||
The example starts ushort at <http://localhost:18082/s/> and stores its SQLite
|
||||
database in `./data/urlshort.db`.
|
||||
|
||||
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`.
|
||||
Check it and stop it with:
|
||||
|
||||
```bash
|
||||
ushort config.toml
|
||||
ushort legacy-config.json
|
||||
curl http://localhost:18082/s/api/health
|
||||
docker compose -f deploy/docker-compose.build.yml down
|
||||
```
|
||||
|
||||
| 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 a production server, use the published image through
|
||||
`deploy/docker-compose.prod.yml`; it does not build or require a source
|
||||
checkout.
|
||||
|
||||
The same binary supports both common reverse-proxy layouts:
|
||||
## Documentation
|
||||
|
||||
- `base_url = "https://s.example.com"` serves the frontend, API, and short
|
||||
codes at the subdomain root, producing `https://s.example.com/aB3xYz`.
|
||||
- `base_url = "https://example.com/go/links"` limits routing to `/go/links`
|
||||
and produces `https://example.com/go/links/aB3xYz`. Requests outside that
|
||||
prefix return 404.
|
||||
|
||||
A trailing slash on `base_url` is accepted in either layout and is normalized
|
||||
when short URLs are generated.
|
||||
|
||||
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 by every frontend, API, metadata, deletion, and short-code route,
|
||||
including when a query string follows the slash. 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:
|
||||
|
||||
```json
|
||||
{"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:
|
||||
|
||||
```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.
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```sql
|
||||
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 a path deployment such as the existing `/s`, place the location blocks
|
||||
from `nginx.conf` in the public vhost. For a dedicated subdomain, use
|
||||
`deploy/nginx-subdomain.conf` in its `server` block. Both examples proxy the
|
||||
frontend, API, and redirects while retaining `no-store` HTML and `no-cache`
|
||||
asset policies.
|
||||
|
||||
## Build and test
|
||||
|
||||
The repository intentionally does not require a host Rust installation.
|
||||
|
||||
```bash
|
||||
docker build -t ushort:local .
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Run the Rust test suite in an isolated build stage:
|
||||
|
||||
```bash
|
||||
docker build --target tester .
|
||||
```
|
||||
|
||||
Run the real HTTP trailing-slash smoke test against any disposable deployment:
|
||||
|
||||
```bash
|
||||
sh tests/smoke.sh https://s.example.com "$API_KEY"
|
||||
sh tests/smoke.sh https://example.com/s "$API_KEY"
|
||||
```
|
||||
|
||||
The Dockerfile builds against musl and uses `scratch` for the runtime image.
|
||||
Docker Buildx can publish both required architectures from the same source:
|
||||
|
||||
```bash
|
||||
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.
|
||||
- [Configuration](docs/configuration.md)
|
||||
- [API reference](docs/api.md)
|
||||
- [Embedded frontend](docs/frontend.md)
|
||||
- [Data and database compatibility](docs/data.md)
|
||||
- [Production deployment and rollback](docs/deployment.md)
|
||||
- [Development, testing, and releases](docs/development.md)
|
||||
- [Security model](docs/security.md)
|
||||
|
||||
Reference in New Issue
Block a user