Skip to content
Tikab's Toolkit

Storage

The django-storages counterpart: one small file API, two drivers behind it. Calling code never knows which driver is active.

The API

Live from the source:

/** Map a supported image content-type to a file extension, or null. */
export function extForImage(contentType: string): string | null {
  return EXT_BY_TYPE[contentType] ?? null;
}
 
/** A collision-resistant object key under a prefix. */
export function uploadKey(prefix: string, ext: string): string {
  return `${prefix}/${randomUUID()}.${ext}`;
}
 
export async function putObject(key: string, body: Buffer, contentType: string): Promise<void> {
  await store().put(key, body, contentType);
}
 
export async function getObject(
  key: string,
): Promise<{ body: Buffer; contentType: string } | null> {
  try {
    return await store().get(key);
  } catch {
    return null;
  }
}
 
export async function removeObject(key: string): Promise<void> {
  try {
    await store().remove(key);
  } catch {
    // Best-effort cleanup — a failed delete shouldn't break the caller.
  }
}
 
/**
 * The app path that serves a stored object (via the file-proxy route).
 *
 * The key travels as a query param (not a path segment) on purpose: Vite's dev
 * server intercepts request paths ending in an asset extension (`.png`, `.jpg`,
 * …) and 404s them before they reach our server route. Keeping the extension in
 * the query string sidesteps that, and behaves identically in production.
 */
export function fileUrl(key: string): string {
  return `/api/files?key=${encodeURIComponent(key)}`;
}

getObject returns null on any miss, removeObject is best-effort — uploads and deletes never crash a user action over storage hiccups.

Driver selection

EnvironmentDriver
AZURE_STORAGE_ACCOUNT setAzure Blob (account-key auth via AZURE_STORAGE_KEY)
otherwiseMinIO / any S3-compatible via S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET

Local development and on-prem both run MinIO (it is in the compose file); a cloud deployment flips to Blob by setting two app settings. Same code path either way — that is the point of the seam.

The file proxy

Browsers never talk to the object store directly. fileUrl(key) returns an app path (/api/files?key=…) and the consumer mounts a small proxy route that streams the object through getObject — access control stays in the app, storage credentials stay on the server. The example's src/routes/api.files.ts is the reference implementation.

Loading diagram...

Conventions

  • Key layout: uploadKey(prefix, ext)prefix/<uuid>.<ext>; prefix by domain (avatars/<userId>, attachments/<projectId>/<taskId>) so cleanup and access checks are scoped lookups.
  • Server-only. The package's browser export is a throwing stub; URLs for the client are produced server-side with fileUrl.