Buckets and Object CRUD in Lioran S3
Object storage becomes useful when the abstractions stay boring: buckets contain objects, object keys are identifiers, metadata is inspectable, large transfers stream, and listing does not explode memory.
That is the model used by Lioran S3 V1 Pre-Alpha, built by Lioran Developer Solutions under Lioran Group.
I’m Swaraj Puppalwar, Founder & CTO, and this article documents the current TypeScript bucket/object API.
Create a client
import { BastionClient } from "@liorans3/driver";
const client = new BastionClient(
process.env.LIORAN_S3_URI!
);
Bucket lifecycle
Create:
await client.buckets.create("media", {
versioning: false,
quotaBytes: 50 * 1024 * 1024 * 1024,
});
List:
const buckets = await client.buckets.list();
for (const bucket of buckets) {
console.log(bucket);
}
Inspect:
const info = await client.buckets.get("media");
console.log(info);
Update:
await client.buckets.update("media", {
quotaBytes: 100 * 1024 * 1024 * 1024,
tags: {
environment: "development",
owner: "backend",
},
});
Delete an empty bucket:
await client.buckets.delete("media");
Force deletion:
await client.buckets.delete("media", {
force: true,
});
Use forced deletion carefully because it removes objects.
Scoped bucket handle
const bucket = client.bucket("media");
The handle owns the bucket-specific object API.
PUT
From text:
await bucket.put(
"readme.txt",
"hello",
{ contentType: "text/plain" }
);
From a buffer:
const bytes = Buffer.from("binary-data");
await bucket.put(
"data/blob.bin",
bytes,
{ contentType: "application/octet-stream" }
);
From a file stream:
import fs from "node:fs";
await bucket.put(
"images/avatar.png",
fs.createReadStream("./avatar.png"),
{ contentType: "image/png" }
);
Streaming is important because Lioran S3's storage design is built around bounded-memory I/O.
GET
const object = await bucket.get(
"images/avatar.png"
);
Write to disk:
await object.writeToFile(
"./downloads/avatar.png"
);
Read into memory only when appropriate:
const buffer = await object.buffer();
Text:
const text = await object.text();
JSON:
const config = await object.json<{
enabled: boolean;
}>();
HEAD
Use HEAD when you need metadata without object content.
const metadata = await bucket.head(
"images/avatar.png"
);
console.log(metadata.contentType);
console.log(metadata.contentLength);
console.log(metadata.etag);
console.log(metadata.sha256);
DELETE
await bucket.delete(
"images/avatar.png"
);
Byte-range reads
Range requests are useful for media playback, resumable downloads, large archives, and any client that needs only part of an object.
First MiB:
const part = await bucket.getRange(
"videos/demo.mp4",
{
start: 0,
end: 1024 * 1024 - 1,
}
);
Suffix:
const tail = await bucket.getRange(
"logs/server.log",
{
suffix: 4096,
}
);
Cursor pagination
Avoid APIs that return millions of object records in one response.
Fetch a page:
let cursor: string | undefined;
do {
const page = await bucket.listPage({
prefix: "uploads/2026/",
limit: 100,
cursor,
});
for (const object of page.objects) {
console.log(object.key);
}
cursor = page.next_cursor;
} while (cursor);
The cursor remains opaque to the application.
Prefixes are logical
A key such as:
uploads/2026/10/avatar.png
looks hierarchical, but treat it as an object-storage key, not as permission to assume the raw physical filesystem contains the same path.
Lioran S3 internally separates user-controlled keys from physical storage placement.
That reduces path traversal risk and gives the storage engine freedom to choose safer layouts.
Server-side image optimization
The current driver also exposes an experimental optimization API.
const result = await bucket.optimize(
"photos/hero.png",
{
width: 1200,
height: 800,
fit: "cover",
format: "webp",
quality: 80,
targetKey:
"photos/hero-1200x800.webp",
}
);
console.log(result.variant.target_key);
Quotas
Bucket quotas provide a hard storage boundary.
Example:
await client.buckets.update("media", {
quotaBytes: 25 * 1024 * 1024 * 1024,
});
Applications should still handle quota errors explicitly.
import {
QuotaExceededError,
} from "@liorans3/driver";
try {
await bucket.put("large.bin", hugeStream);
} catch (error) {
if (error instanceof QuotaExceededError) {
console.error("Storage quota exceeded");
}
}
Design rule
Your application should generally:
- stream large inputs
- stream large outputs
- use
HEADinstead ofGETfor metadata - paginate listings
- keep keys deterministic
- avoid embedding credentials into source code
- use quotas intentionally
The API surface is straightforward by design. The interesting engineering belongs underneath it.
Top comments (0)