@locci/storage
v0.5.0
Published
TypeScript SDK for Locci Storage — UploadThing-style DX on your own infra
Readme
@locci/storage
TypeScript SDK for Locci Storage - buckets and file uploads on your own infrastructure, with UploadThing-style ergonomics.
Uploads go straight from the browser to object storage. Your server signs the request and records the result; the bytes never pass through it.
Install
pnpm add @locci/storage
# npm install @locci/storageSvelte is an optional peer dependency, needed only for the /sveltekit entry point.
Two tokens, two places
The split matters, and getting it wrong is the most common setup mistake:
| | token | where it runs |
| --- | --- | --- |
| LocciStorage | project token - sent as x-locci-storage-project-token | server only |
| LocciUploadClient | the same project token, or a short-lived one you mint | browser |
A project token can create and delete buckets. Do not ship one to a browser
unless you accept that anyone can read it - mint a scoped token with
createToken() for client-side use.
Quick start
import { LocciStorage } from "@locci/storage";
const storage = new LocciStorage({
token: process.env.LOCCI_STORAGE_TOKEN!,
// defaults to https://storage.locci.cloud
baseUrl: "https://storage.locci.cloud",
});
const buckets = await storage.listBuckets();
if (buckets.ok) {
console.log(buckets.value.map((b) => b.name));
} else {
console.error(buckets.error);
}Nothing throws
Every method returns a Result<T, string> from @locci/rusty-utils rather than
throwing. A network failure, a 4xx, and an error in the response body all arrive
the same way - as err. There is no second error channel to remember, and no
try/catch that silently swallows one of the three.
const res = await storage.createBucket({ name: "avatars", maxSizeMB: 8 });
if (!res.ok) return reply(400, res.error);
const bucket = res.value;Browser uploads
import { LocciUploadClient } from "@locci/storage/client";
const client = new LocciUploadClient({ token });
const res = await client.upload({
file, // a File from an <input type="file">
route: "avatars",
onProgress: (pct) => (progress = pct),
});upload() does three things: asks the API to presign, PUTs the bytes directly
to object storage, then calls /upload/complete so the file is recorded. If the
PUT succeeds but complete does not, the object exists but is not yet listed -
retry complete rather than re-uploading.
SvelteKit
<script lang="ts">
import { createUploader } from "@locci/storage/sveltekit";
const uploader = createUploader({ token, route: "avatars" });
</script>
<input type="file" onchange={(e) => uploader.upload(e.currentTarget.files![0])} />
{#if uploader.progress > 0}<progress value={uploader.progress} max="100"></progress>{/if}createMultiUploader() is the same thing for several files at once.
Routes
A route names a bucket and the rules for what may land in it. Limits are checked before presigning, so a rejected file costs no upload.
const router = storage.router({
avatars: { maxSize: "4MB", accept: ["image/*"] },
documents: {
maxSize: "20MB",
accept: ["application/pdf"],
middleware: async ({ req }) => ({ userId: await userFrom(req) }),
onUpload: async (file, ctx) => db.insert(files).values({ ...file, ...ctx }),
},
});
const presigned = await router.presignForRoute("avatars", {
name: file.name,
mimeType: file.type,
sizeBytes: file.size,
});The bucket defaults to the route name. accept takes exact MIME types,
wildcards like image/*, or ["*"]. maxSize is a string - "4MB", "100KB",
"1GB".
API reference
new LocciStorage({ token, baseUrl? })
| method | returns |
| --- | --- |
| listBuckets() | Bucket[] |
| getBucket(id) | Bucket |
| createBucket({ name, maxSizeMB?, allowedMimeTypes?, isPublic? }) | Bucket |
| deleteBucket(id) | { deleted: boolean } |
| listFiles(bucket?, { limit?, offset? }) | StoredFile[] |
| getFile(id) | StoredFile |
| deleteFile(id) | { deleted: boolean } |
| presign({ bucketName, fileName, mimeType, sizeBytes }) | PresignResponse |
| complete(fileId) | UploadCompleteResponse |
| listTokens() | StorageToken[] |
| createToken({ name, expiresAt? }) | StorageToken & { token: string } |
| revokeToken(id) | { revoked: boolean } |
| router(routes) | LocciRouter |
createToken() returns the secret once. It is not retrievable afterwards.
LocciRouter
presignForRoute(name, file), getRoute(name), routeNames().
new LocciUploadClient({ token, apiUrl? })
upload({ file, route, onProgress? }).
@locci/storage/sveltekit
createUploader({ token, route, apiUrl? }), createMultiUploader(...).
All results are Result<T, string>. Types exported: Project, Bucket,
StoredFile, StorageToken, PresignResponse, UploadCompleteResponse,
RouteMap, RouteConfig.
Entry points
| import | for |
| --- | --- |
| @locci/storage | server - buckets, files, tokens, routes |
| @locci/storage/client | browser uploads |
| @locci/storage/sveltekit | Svelte 5 reactive uploaders |
Licence
See the repository.
