Lioran S3 Storage Layout
Suppose an object key is:
users/42/avatar.png
The easiest implementation is:
/data/users/42/avatar.png
Lioran S3 deliberately does not do that.
The Rust engine separates the user-visible object namespace from physical placement.
I’m Swaraj Puppalwar, Founder & CTO of Lioran Group / Lioran Developer Solutions, and this article goes inside StorageLayout.
The directory model
At its core:
<data_root>/
├── staging/
└── objects/
Normal object writes first enter staging/.
Committed payloads live under objects/.
Internal IDs
When a PUT begins, Bastion creates an internal UUID for the object.
Physical placement is derived from that ID, not from the user key.
Example:
object ID:
ab12cd34-....
physical path:
objects/ab/12/ab12cd34-....
The implementation removes hyphens, lowercases the ID, and uses two two-character prefixes.
Why fan out directories?
Putting millions of files into one directory can create ugly filesystem behavior and operational pain.
The two-level prefix layout spreads objects across a large namespace.
Conceptually:
objects/
├── 00/
│ ├── 00/
│ ├── 01/
│ └── ...
├── 01/
├── 02/
└── ...
The object UUID remains the final filename.
Logical key versus physical path
Metadata connects the two worlds:
bucket + logical key
↓
ObjectMetadata
↓
physical relative path
↓
filesystem payload
For example:
logical:
media/images/avatar.png
physical:
objects/ab/12/ab12cd34-....
The application never needs to know that physical path.
Security consequence
The source states a critical invariant:
Raw user object keys are NEVER used directly as filesystem paths.
That eliminates an entire class of naive path-concatenation mistakes.
A key containing:
../../etc/something
should remain a logical key, not become a filesystem traversal instruction.
Staging paths
Temporary normal PUTs use:
staging/<staging-uuid>.tmp
Multipart sessions use:
staging/uploads/<upload-id>/
and individual parts are generated as:
part_00001.tmp
part_00002.tmp
...
Again, external object keys do not define those paths.
Reads
A GET starts from metadata:
let meta = self.metadata.get_object(bucket, key)?;
Then:
let abs_path =
self.layout.resolve_absolute_path(&meta.physical_path);
The stored relative physical path is resolved underneath the configured data root.
Range reads
Range reads use the same physical mapping.
After opening the committed payload, the engine can:
file.seek(SeekFrom::Start(start)).await
and then limit the reader:
file.take(length)
No special “range object” is created.
Video namespace
Media assets have a structured logical namespace, including forms such as:
videos/{video_id}/source/original
videos/{video_id}/poster/poster.webp
videos/{video_id}/renditions/{name}/video.mp4
videos/{video_id}/hls/master.m3u8
videos/{video_id}/hls/{name}/segment_00001.m4s
These are canonical object keys for media workflows.
They should still be understood separately from arbitrary raw filesystem placement.
Why metadata must exist
Once logical keys stop being physical paths, metadata becomes essential.
The metadata record answers:
What object does this key represent?
Where is its payload?
How large is it?
What is its checksum?
Is it committed?
That is why Bastion has a distinct metadata plane.
Tradeoff
This design means you cannot treat /data/objects as a friendly human-readable bucket tree.
That is intentional.
The filesystem is an implementation detail.
The API namespace is the contract.
That separation gives the engine freedom to change placement strategies later without forcing application object keys to change with it.
Top comments (0)