Skip to content

Data model

PutFS stores files on a plain directory hierarchy and enforces no structure: PUT /hello.txt works. For multi-tenant deployments there is a convention:

tenant    →  a PutFS deployment (a ZFS pool or data root)
└── dataset   →  first path segment (a ZFS dataset, or "bucket" by convention)
    └── object    →  everything after (a file)
https://acme.putfs.example.org/invoices/q1-2024.pdf
       ^^^^                    ^^^^^^^^ ^^^^^^^^^^^^
       tenant                  dataset  object path

On disk:

/srv/putfs/
├── invoices/           ← dataset
│   └── q1-2024.pdf     ← object
└── contracts/          ← dataset
    └── nda.pdf         ← object

The filesystem is the source of truth

No metadata database, no index, no sidecar files: the bytes on disk are the state PutFS exposes, in both directions. A PUT is visible to ls the next instant; an mv or rm on disk shows up in the next listing. rsync, zfs send and snapshot rollbacks work without knowing PutFS exists. The flipside: anything needing persistent metadata (object tags, custom ACLs, per-object version history) is out of scope by construction.

Write atomicity and durability

Every PUT streams into a private temp file and is published with an atomic rename (plain keys) or a create-only link (WORM keys), so a key only ever appears on disk as a complete object. A reader or a racing PUT sees the old object or the new one, never a partial one, and a failed upload leaves only its own temp. This holds on any filesystem.

Durability is separate: PutFS issues no fsync. A 201 means "atomically visible", not "on stable storage"; durability is the filesystem's job, like redundancy and checksums.

A 201 is not a power-loss barrier by default

On ZFS with sync=standard, a just-acknowledged PUT sits in the open transaction group until the next commit (zfs_txg_timeout, default 5s). A power loss in that window drops the whole object, never a torn one. Set sync=always on the dataset to make an acknowledged PUT survive power loss; the ZFS tuning guide covers the latency trade-off.

Tenants

One PutFS deployment: one server, one nginx, one data root. Isolation is at the deployment level (separate servers, DNS and auth), or separate data roots and ZFS pools for several instances on one server. A tenant maps to an application, a team or a customer.

Datasets

The first path segment, a convention PutFS never parses, like the bucket of path-style S3 addressing. Map it to a ZFS dataset (zfs create tank/putfs/invoices) for per-dataset quotas, snapshots and encryption, or leave it a directory. Datasets are created implicitly on the first PUT.

Objects

Everything after the dataset prefix, nested as deep as you like (/invoices/2024/q1/report.pdf). Directories are created automatically and removed with their last key.