How it works · s5w

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:

bash
docker exec s5w su-exec winter php call app status

The output lists every part with its state, PID, how long it has been running and how many times it has restarted.

Start-up order

  1. If the database directory is empty — initdb, creating the s5w role and database, creating the tables.
  2. PostgreSQL starts.
  3. 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

Bucket

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.

Folder

An optional group of files inside a bucket: visibility, retention, cache policy. Folders do not nest.

File

A record with a name, folder, visibility and expiry. It refers to the content — a blob.

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:

text
/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_KEY

What 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 413 and 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.