# @wrnexus/uploader Config-driven file uploads + serving for [WrNexus](https://www.npmjs.com/org/wrnexus). Declare named **storage stores** (local disk or any S3-compatible backend) in `wrnexus.config.ts`, upload with one function call, drop a drag-and-drop widget on a page, and serve files back — public or private. Zero external dependencies (S3 is signed with a built-in AWS SigV4 implementation, like the rest of the framework). ## Usage ### Configure local and S3 stores ```ts // wrnexus.config.ts import type { AppConfig } from "@wrnexus/styles"; const config: AppConfig = { storage: { default: "public", stores: { // Local disk, world-readable — served by the framework with a 1-year cache. public: { driver: "local", dir: "uploads/public", // relative to the app root (dev) / cwd (prod) access: "public", maxBytes: 10_000_000, accept: ["image/*", ".pdf"], // MIME, "type/*" wildcards, or ".ext" }, // Private S3 (works with AWS, Cloudflare R2, Backblaze B2, MinIO, DO Spaces). docs: { driver: "s3", access: "private", bucket: "my-bucket", region: "auto", endpoint: "https://.r2.cloudflarestorage.com", accessKeyId: process.env.S3_KEY!, secretAccessKey: process.env.S3_SECRET!, }, }, }, }; export default config; ``` ### Upload from an API route or server function ```ts // app/api/upload.ts — one-liner import { handleUpload } from "@wrnexus/uploader"; export const POST = handleUpload({ store: "public" }); // → { ok: true, files: [{ key, url, name, type, size }] } ``` ```ts // or drive it yourself, anywhere you have the request import { upload, getStore } from "@wrnexus/uploader"; const { files } = await upload("docs", ctx.req, { prefix: "invoices" }); await getStore("docs").driver.delete(files[0].key); ``` Uploads are validated (size + type), stored under a random, collision-proof, path-safe key (the client filename is never used as a path), and — for public stores — returned with a servable `url`. ### Add a client upload widget Drop the element anywhere; the runtime (drag-and-drop, per-file progress, success/failed states) is auto-injected on pages that contain `data-uploader`: ```html
``` Or via the first-party UI component: ```html
``` It dispatches bubbling events you can listen for: - `wrnexus:upload` — `detail: { file, result: { key, url, name, size, type } }` - `wrnexus:upload-error` — `detail: { file, error }` ### Serve private files behind application authentication - **Public + local** → served automatically at `/__wrnexus/uploads//` (immutable cache). - **Public + S3** → `url` is the bucket/CDN URL directly. - **Private** (any driver) → mount a route and gate it with your auth middleware: ```ts // app/api/files/[key].ts import { serveFromStore } from "@wrnexus/uploader"; export const GET = serveFromStore("docs"); // your middleware decides who gets in ``` ## API Uploads can participate in security and media pipelines without changing storage drivers. Pass a `scan` hook to reject malware/DLP findings before storage, and `afterStore` to enqueue image/video processing or indexing. If post-processing throws, WRNexus deletes the newly written object so a partially accepted upload is never left behind. | Export | What | | --------------------------------------- | --------------------------------------------------------------- | | `handleUpload(opts)` | POST route handler → JSON `{ ok, files }` | | `upload(store, req, opts)` | Parse + validate + store; returns `{ files }` | | `serveFromStore(store)` | Route handler that streams an object back (gate it for private) | | `getStore(name?)` / `hasStorage(name?)` | Reach a store's `driver` (`put`/`get`/`delete`/`publicUrl`) | | `configureStorage(config, root)` | Build the registry (the framework calls this at startup) | | `s3Driver` / `localDriver` / `signS3` | Lower-level building blocks | ## Notes - Uploads count against the server's `maxBodyBytes`; per-file limits use each store's `maxBytes`. - SigV4 signing is implemented from scratch (no `@aws-sdk`); tested against local S3 semantics. Live AWS/R2 connectivity depends on your credentials + bucket policy. - v1 buffers each file in memory up to its size cap (fine for images/docs up to tens of MB). ## Helper and component kit Use `formatFileSize`, `uploadAccept`, `uploadedFileMap`, `uploaderAttributes`, and `assertUploadedFiles` to keep upload forms and server validation consistent. Enable `uploaderPlugin()` for: - `` - `` The complete blocks compose `Card`, `Alert`, and `Badge` from `@wrnexus/ui`; the specialized upload runtime remains responsible for the native file input and secure transport behavior. Large files can use `createResumableUploadManager`. Sessions are bounded and expiring; chunks may arrive out of order, carry SHA-256 checksums, and are idempotent when retried. Conflicting retries reject, and the object is assembled only after every exact-sized chunk is present. ```ts const uploads = createResumableUploadManager({ driver: getStore("documents").driver, sessions: redisUploadSessionStore, chunkSize: 5 * 1024 * 1024, maxBytes: 500 * 1024 * 1024, accept: ["application/pdf"], }); const session = await uploads.create({ name: "report.pdf", size, type }); await uploads.uploadChunk(session.id, index, bytes, sha256); ``` The included memory session store is intended for one-process apps and tests. Multi-instance production deployments should implement `ResumableSessionStore` with shared durable storage and atomic session updates, and periodically call `prune()` for abandoned uploads.