Architecture
What S5W is made of, where the data lives and what happens to a file from upload to deletion. A page for the architect and for the DevOps engineer who needs to understand exactly what they are running.
Processes in the container
The root process of the container is the s6 supervisor. It starts and restarts everything
else and, on docker stop, stops the parts in the right order.
| Process | What it does | If it crashes |
|---|---|---|
| PostgreSQL 18 | Stores the metadata. Listens only on 127.0.0.1 and a unix socket — not exposed. |
s6 restarts it. |
Application (winter-http) |
The client API and file delivery on port 9090. |
Restarted by its own supervisor. |
| Scheduler | Scheduled maintenance tasks. | Restarted. |
Admin panel (AdminUi) |
Serves the web panel on 9091 and proxies its requests to the API inside the container. |
Restarted; its crash does not affect the API or delivery. |
The state of the parts is shown by:
docker exec s5w su-exec winter php call app statusThe output lists every part with its state, PID, how long it has been running and how many times it has restarted.
Start-up order
- If the database directory is empty —
initdb, creating thes5wrole and database, creating the tables. - PostgreSQL starts.
- The application starts: it checks the code, builds its cache and only then accepts requests. If building the cache fails, the application does not start and writes the reason to the log.
Shutdown order
The application stops first: it finishes requests and components, up to 35 seconds. Then
PostgreSQL — with a fast shutdown, without waiting for other sessions. That is why the
container needs at least 45 seconds to stop: --stop-timeout 45 or stop_grace_period: 45s.
If Docker kills the database earlier, it will recover from its journal on the next start.
Data model
An isolated space: its own quota, its own tokens, its own cache policy. Files of different buckets never overlap, neither in the database nor on disk.
An optional group of files inside a bucket: visibility, retention, cache policy. Folders do not nest.
A record with a name, folder, visibility and expiry. It refers to the content — a blob.
The content itself, one per unique SHA-256 in the bucket. It knows how many files refer to it.
There are also tokens (API access to a bucket), uploads (unfinished chunked uploads), temporary links and traffic statistics per day.
File storage
On disk the content is stored by hash, not by name:
/var/lib/s5w/
postgresql/ the PostgreSQL cluster — metadata
buckets/
<bucketId>/
<sha256> the content, named by its hash
staging/
<uploadId> an unfinished chunked upload
.runtime_secrets the generated WINTER_KEYWhat follows from this:
- Names, folders and visibility live only in the database. Renaming and moving a file do not touch the disk.
- Without the database the files on disk mean nothing. A backup must include both the
database and
buckets/— taken at the same moment. The simplest way is to copy the whole volume. - File names on disk are safe. Whatever the client sends as a name, only the hash reaches the disk.
Deduplication
Identical content is stored in a bucket once. On upload the server computes SHA-256; if such a blob already exists in the bucket, the new file simply refers to it.
- A copy costs a database row, not disk space.
- The quota counts blobs, not files: ten copies of one file take the quota once.
- A blob has a reference counter. Deleting a file decrements it; when no references are left, the blob is deleted and the space goes back to the quota.
- Deduplication works within a bucket. Identical files in different buckets are stored separately — that keeps buckets isolated.
A client can check whether the content is there before uploading: POST /v1/uploads with
sha256. If it already exists, the file is created at once, without sending the bytes.
Details are in chunked upload.
Quotas and disk space
- Every bucket has a quota (100 MiB by default, changed in the panel). An upload that would
exceed it is rejected with
413and a message saying how much space is free. - A chunked upload checks the quota and the free disk space up front, at start. The server
keeps a reserve of 512 MiB: if less would be left on disk after the upload, it is rejected
with
507. - Used space is kept as a counter and recalculated from scratch once a day, to fix discrepancies after failures.
Chunked upload
Chunks are written to staging/<uploadId>. When all have arrived, the file is checked
against its hash and moves to buckets/ by rename. A rename is atomic only within one
filesystem, so staging/ and buckets/ must be on the same volume. If they are not, the
server logs a warning and copies the file instead of renaming it — slower, and with double
the space used while copying.
An abandoned upload expires 24 hours after its last accepted chunk (every chunk extends the term); the sweeper, which runs hourly, deletes it together with its chunks.
Traffic statistics
Counters of egress, ingress and requests accumulate in the process’s shared memory and are flushed to the database every 20 seconds. On a crash, at most 20 seconds of statistics are lost; the files and metadata themselves are not affected.
Scheduled maintenance
| When | What | Why |
|---|---|---|
| every 20 s | flush traffic counters to the database | statistics |
| every 5 min | delete files whose folder retention expired | retention |
| hourly | delete abandoned chunked uploads | space in staging/ |
| 03:15 | delete expired and revoked temporary links | a clean database |
| 03:30 | delete blobs nothing refers to | disk space |
| Sun 03:45 | delete files on disk the database does not know | disk space |
| 04:00 | delete directories of deleted buckets | disk space |
| 04:10 | delete statistics older than a year | database size |
| 04:20 | recalculate used space per bucket | accurate quotas |
Times are in UTC: the scheduler runs by it, and TIME_ZONE does not affect the schedule.
Tasks are protected from concurrent runs: the same task never runs twice at the same time.