first commit
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# @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).
|
||||
|
||||
## Configure
|
||||
|
||||
```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://<acct>.r2.cloudflarestorage.com",
|
||||
accessKeyId: process.env.S3_KEY!,
|
||||
secretAccessKey: process.env.S3_SECRET!,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
```
|
||||
|
||||
## Upload (server)
|
||||
|
||||
```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`.
|
||||
|
||||
## Widget (client)
|
||||
|
||||
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
|
||||
<div
|
||||
data-uploader="public"
|
||||
data-endpoint="/api/upload"
|
||||
data-accept="image/*"
|
||||
data-max="10000000"
|
||||
data-multiple
|
||||
></div>
|
||||
```
|
||||
|
||||
Or via the first-party UI component:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-component="file-upload"
|
||||
store="public"
|
||||
endpoint="/api/upload"
|
||||
accept="image/*"
|
||||
multiple="true"
|
||||
></div>
|
||||
```
|
||||
|
||||
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 files
|
||||
|
||||
- **Public + local** → served automatically at `/__wrnexus/uploads/<store>/<key>` (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
|
||||
|
||||
| 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).
|
||||
Reference in New Issue
Block a user