nuxt-filer 0.0.16 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -5
- package/dist/module.d.mts +19 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +52 -8
- package/dist/runtime/server/handlers/image-service.d.ts +12 -0
- package/dist/runtime/server/handlers/image-service.js +21 -0
- package/dist/runtime/server/handlers/ipx.js +34 -17
- package/dist/runtime/server/plugins/default-storage.d.ts +2 -1
- package/dist/runtime/server/plugins/default-storage.js +2 -0
- package/dist/runtime/server/provider.d.ts +6 -1
- package/dist/runtime/server/provider.js +3 -0
- package/dist/runtime/server/providers/drizzle.d.ts +75 -0
- package/dist/runtime/server/providers/drizzle.js +231 -0
- package/dist/runtime/server/providers/s3.d.ts +5 -0
- package/dist/runtime/server/providers/s3.js +20 -2
- package/dist/runtime/server/providers/unstorage.js +12 -0
- package/dist/runtime/server/utils/image-service-runtime.d.ts +9 -0
- package/dist/runtime/server/utils/image-service-runtime.js +65 -0
- package/dist/runtime/server/utils/image-service.d.ts +36 -0
- package/dist/runtime/server/utils/image-service.js +113 -0
- package/dist/runtime/server/utils/send.js +6 -5
- package/dist/runtime/server/utils/storage.d.ts +1 -0
- package/dist/runtime/server/utils/storage.js +9 -2
- package/dist/runtime/server/utils/thumbnail.d.ts +19 -0
- package/dist/runtime/server/utils/thumbnail.js +54 -0
- package/dist/runtime/server/utils/upload.d.ts +57 -0
- package/dist/runtime/server/utils/upload.js +81 -0
- package/dist/runtime/types.d.ts +5 -0
- package/package.json +37 -27
- package/src/runtime/server/handlers/image-service.ts +35 -0
- package/src/runtime/server/handlers/ipx.ts +58 -23
- package/src/runtime/server/plugins/default-storage.ts +6 -1
- package/src/runtime/server/provider.ts +13 -1
- package/src/runtime/server/providers/drizzle.ts +388 -0
- package/src/runtime/server/providers/s3.ts +28 -2
- package/src/runtime/server/providers/unstorage.ts +20 -0
- package/src/runtime/server/utils/image-service-runtime.ts +87 -0
- package/src/runtime/server/utils/image-service.ts +166 -0
- package/src/runtime/server/utils/image.ts +2 -2
- package/src/runtime/server/utils/send.ts +16 -7
- package/src/runtime/server/utils/storage.ts +19 -4
- package/src/runtime/server/utils/thumbnail.ts +94 -0
- package/src/runtime/server/utils/upload.ts +157 -0
- package/src/runtime/types.ts +5 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ File storage module for Nuxt. Provides a server-side `useFileStorage()` composab
|
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
|
-
- **Pluggable provider architecture** —
|
|
12
|
+
- **Pluggable provider architecture** — built-in unstorage (default), S3 and Drizzle providers, or bring your own
|
|
13
13
|
- **File versioning** — built-in version tracking, latest-version filtering, and duplicate detection
|
|
14
14
|
- **External file sync** — two-way sync with external systems (Jira, SharePoint, etc.) via optional provider interface
|
|
15
15
|
- **Zero-config default** — works out of the box with local filesystem storage, no database required
|
|
@@ -32,7 +32,8 @@ npx nuxi module add nuxt-filer
|
|
|
32
32
|
export default defineNuxtConfig({
|
|
33
33
|
modules: ['nuxt-filer'],
|
|
34
34
|
filer: {
|
|
35
|
-
// Nitro storage mount name (default: 'documents')
|
|
35
|
+
// Nitro storage mount name (default: 'documents'). Mounted on the local fs
|
|
36
|
+
// for every provider unless you configure it yourself under `nitro.storage`.
|
|
36
37
|
storageName: 'documents',
|
|
37
38
|
// Base path for fs-lite driver (default: '.data/documents')
|
|
38
39
|
storagePath: '.data/documents',
|
|
@@ -116,6 +117,34 @@ const res = await transformImage(buffer, { width: 64, format: 'webp' })
|
|
|
116
117
|
|
|
117
118
|
> Image processing requires the optional [`sharp`](https://sharp.pixelplumbing.com/) peer dependency. Install it (`npm i sharp`) only if you use `transform` / `transformImage()` — calling them without `sharp` throws a clear error. Without a `transform`, `upload()` stores the raw bytes unchanged and needs no extra dependency.
|
|
118
119
|
|
|
120
|
+
### Thumbnails and PDF previews with `generateThumbnail()`
|
|
121
|
+
|
|
122
|
+
Builds a preview image from a stored file: images are resized with Sharp, PDFs
|
|
123
|
+
get a page (the first by default) rendered and then resized. It returns
|
|
124
|
+
`null` instead of throwing for unsupported types, unreadable input, or missing
|
|
125
|
+
optional dependencies, so it can run on every upload:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const file = await readUploadedFile(event)
|
|
129
|
+
const id = await storage.upload('docs', file.data, { meta: { name: file.name, mime: file.type, type: 'document', version: 1 } })
|
|
130
|
+
|
|
131
|
+
const thumb = await generateThumbnail(file.data, file.type, { width: 300, height: 200 })
|
|
132
|
+
if (thumb) {
|
|
133
|
+
await storage.upload('docs', thumb.data, {
|
|
134
|
+
meta: { name: `thumb_${file.name}`, mime: thumb.mime, type: 'thumbnail', version: 1 },
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- Options are those of `transformImage()` plus `page` (PDF page, default `1`).
|
|
140
|
+
Defaults: a 300×300 `inside` box, `webp`, and the first frame only for
|
|
141
|
+
animated images (`animated: true` keeps animation).
|
|
142
|
+
- Requires `sharp`; PDFs additionally need [`unpdf`](https://github.com/unjs/unpdf)
|
|
143
|
+
and `@napi-rs/canvas` (`npm i unpdf @napi-rs/canvas`). All are optional peer
|
|
144
|
+
dependencies; when one is missing, a warning is logged once and `null` returned.
|
|
145
|
+
- On-request transforms (the IPX route, or imgproxy) can't render PDFs, so a
|
|
146
|
+
stored preview like this is the way to show one.
|
|
147
|
+
|
|
119
148
|
### `useFileStorage()` API
|
|
120
149
|
|
|
121
150
|
| Method | Description |
|
|
@@ -123,8 +152,9 @@ const res = await transformImage(buffer, { width: 64, format: 'webp' })
|
|
|
123
152
|
| `upload(groupId, data, options?)` | Store a file, returns its ID. `options.transform` runs the bytes through Sharp first (see above) |
|
|
124
153
|
| `list(groupId)` | List all files in a group |
|
|
125
154
|
| `get(groupId, id)` | Get a file with data and metadata |
|
|
155
|
+
| `head(groupId, id)` | Get a file's metadata and timestamps without reading its bytes |
|
|
126
156
|
| `getData(groupId, id)` | Get raw binary data only |
|
|
127
|
-
| `getMeta(id)` | Get metadata only |
|
|
157
|
+
| `getMeta(id)` | Get metadata only. Searches all groups, which is slow on large stores — prefer `head(groupId, id)` |
|
|
128
158
|
| `updateMeta(id, meta)` | Deep-merge metadata update |
|
|
129
159
|
| `remove(groupId, id)` | Delete a file |
|
|
130
160
|
| `clear(groupId)` | Delete all files in a group |
|
|
@@ -137,6 +167,35 @@ const res = await transformImage(buffer, { width: 64, format: 'webp' })
|
|
|
137
167
|
| `external?.push(groupId, id, data, meta)` | Push to external system |
|
|
138
168
|
| `external?.pull(groupId, ref)` | Pull from external system |
|
|
139
169
|
|
|
170
|
+
### Validating uploads with `readUploadedFile()`
|
|
171
|
+
|
|
172
|
+
Reads a file from a `multipart/form-data` request and validates it, throwing a
|
|
173
|
+
400 (no file), 413 (too large) or 415 (type not allowed) with a message naming
|
|
174
|
+
the file and the limit:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// server/api/models.post.ts
|
|
178
|
+
export default defineEventHandler(async (event) => {
|
|
179
|
+
const file = await readUploadedFile(event, {
|
|
180
|
+
types: ['image', '.stl', '.3mf', 'application/pdf'],
|
|
181
|
+
maxSize: '50MB',
|
|
182
|
+
})
|
|
183
|
+
const id = await useFileStorage().upload(file.fields.group ?? 'uploads', file.data, {
|
|
184
|
+
meta: { name: file.name, mime: file.type, type: 'model', version: 1 },
|
|
185
|
+
})
|
|
186
|
+
return { id }
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- `types` uses the syntax of the HTML `accept` attribute: exact MIME types
|
|
191
|
+
(`'image/png'`), families (`'image'` or `'image/*'`) and extensions
|
|
192
|
+
(`'.stl'`). Extensions help for formats browsers send without a MIME type.
|
|
193
|
+
- `maxSize` takes bytes or a string like `'500KB'`, `'2MB'` (1024-based). An
|
|
194
|
+
oversized request is rejected from its `Content-Length` before the body is read.
|
|
195
|
+
- `field` picks the form field (default `'file'`); the result's `fields` holds
|
|
196
|
+
the other, non-file form fields.
|
|
197
|
+
- `readUploadedFiles(event, { ..., max })` reads several files from the same field.
|
|
198
|
+
|
|
140
199
|
### Serving raw files with `sendStoredFile()`
|
|
141
200
|
|
|
142
201
|
The IPX route serves **images** with full HTTP caching. For everything else — original PDFs, non-image downloads, the unprocessed bytes of any file — `sendStoredFile()` streams a stored file back through an H3 event with the same revalidation story.
|
|
@@ -192,7 +251,45 @@ filer: {
|
|
|
192
251
|
},
|
|
193
252
|
```
|
|
194
253
|
|
|
195
|
-
`@nuxt/image` and `ipx` are declared as optional peer dependencies — they only need to be installed if you want to use this integration.
|
|
254
|
+
`@nuxt/image` and `ipx` are declared as optional peer dependencies — they only need to be installed if you want to use this integration. Both ipx 3 and ipx 4 (pulled in by `@nuxt/image` 2.1+) are supported.
|
|
255
|
+
|
|
256
|
+
### External image service (imgproxy / standalone IPX)
|
|
257
|
+
|
|
258
|
+
Image processing can run in a separate service instead of this server, so
|
|
259
|
+
`sharp` and `ipx` aren't needed here and one service can serve many apps:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
filer: {
|
|
263
|
+
image: {
|
|
264
|
+
service: 'imgproxy', // or 'ipx' for a standalone `npx ipx serve`; default 'local'
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
NUXT_FILER_IMAGE_BASE_URL=https://img.example.com
|
|
271
|
+
NUXT_FILER_IMAGE_KEY=... # imgproxy signing key + salt (hex); unsigned URLs when unset
|
|
272
|
+
NUXT_FILER_IMAGE_SALT=...
|
|
273
|
+
NUXT_FILER_IMAGE_SOURCE_URL=http://app:3000 # how the service reaches this app
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- `<NuxtImg provider="filer">` works unchanged. The image route redirects to a
|
|
277
|
+
signed service URL, so the signing key never reaches the browser.
|
|
278
|
+
- The service fetches originals from `/_filer-ipx/_/<groupId>/<fileId>`, which
|
|
279
|
+
serves the stored bytes. `sourceURL` is the origin it uses for that — e.g. the
|
|
280
|
+
app's address on a private Docker network. Without it, the request's origin
|
|
281
|
+
is used.
|
|
282
|
+
- Upload-time transforms (`upload(..., { transform })`) go through the service
|
|
283
|
+
too: the original is staged under the `_filer-transform` group, the variant
|
|
284
|
+
is fetched, and the staged copy is removed. This requires `sourceURL`.
|
|
285
|
+
`transformImage()` itself still needs `sharp`.
|
|
286
|
+
- imgproxy receives the IPX modifiers translated to its options (`w`, `h`,
|
|
287
|
+
`s`, `fit`, `enlarge`, `q`, `f`, `b`, `pos`, `blur`, `sharpen`, `rotate`);
|
|
288
|
+
modifiers without an equivalent are dropped. A standalone IPX gets them as-is.
|
|
289
|
+
- imgproxy blocks loopback and private source addresses by default. If
|
|
290
|
+
`sourceURL` points at one, set `IMGPROXY_ALLOW_LOOPBACK_SOURCE_ADDRESSES` /
|
|
291
|
+
`IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES`. A standalone IPX needs the source
|
|
292
|
+
host in `--domains`.
|
|
196
293
|
|
|
197
294
|
## Resumable uploads (tus)
|
|
198
295
|
|
|
@@ -323,9 +420,78 @@ export default defineNitroPlugin(() => {
|
|
|
323
420
|
|
|
324
421
|
> `createS3Provider` requires the optional [`aws4fetch`](https://github.com/mhart/aws4fetch) peer dependency (`npm i aws4fetch`). Pass a custom `client` to use a different transport or to unit-test without network.
|
|
325
422
|
|
|
423
|
+
## Drizzle (metadata in your database)
|
|
424
|
+
|
|
425
|
+
`createDrizzleProvider` keeps file metadata in a table of your own
|
|
426
|
+
[Drizzle](https://orm.drizzle.team) database and the bytes in a blob store —
|
|
427
|
+
S3/R2 via `createS3Client()`, or any mounted Nitro storage. It uses only
|
|
428
|
+
Drizzle's core query builder, so it works with every dialect and driver on
|
|
429
|
+
Drizzle 0.36+ and v1.
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
// server/db/schema.ts
|
|
433
|
+
import { pgTable, text, jsonb, timestamp, index } from 'drizzle-orm/pg-core'
|
|
434
|
+
|
|
435
|
+
export const filerFiles = pgTable('filer_files', {
|
|
436
|
+
id: text('id').primaryKey(),
|
|
437
|
+
groupId: text('group_id').notNull(),
|
|
438
|
+
metadata: jsonb('metadata'),
|
|
439
|
+
createdAt: timestamp('created_at'), // optional
|
|
440
|
+
updatedAt: timestamp('updated_at'), // optional
|
|
441
|
+
}, t => [index('filer_files_group_id_idx').on(t.groupId)])
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
// server/plugins/file-provider.ts (with `filer: { provider: 'custom' }`)
|
|
446
|
+
import { db } from '../utils/db'
|
|
447
|
+
import { filerFiles } from '../db/schema'
|
|
448
|
+
|
|
449
|
+
export default defineNitroPlugin(() => {
|
|
450
|
+
const { s3 } = useRuntimeConfig()
|
|
451
|
+
setFileStorageProvider(createDrizzleProvider({
|
|
452
|
+
db,
|
|
453
|
+
table: filerFiles,
|
|
454
|
+
blobs: createS3Client({
|
|
455
|
+
accessKeyId: s3.accessKeyId,
|
|
456
|
+
secretAccessKey: s3.secretAccessKey,
|
|
457
|
+
endpoint: s3.endpoint,
|
|
458
|
+
bucket: s3.bucket,
|
|
459
|
+
}),
|
|
460
|
+
// or keep bytes on disk in the module's fs storage: blobs: 'documents' (= `filer.storageName`)
|
|
461
|
+
// columns: { id: 'id', groupId: 'groupId', metadata: 'metadata', createdAt: 'createdAt', updatedAt: 'updatedAt' },
|
|
462
|
+
}))
|
|
463
|
+
})
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
- `columns` maps to the table's schema property names, so an existing table can be used. `createdAt`/`updatedAt` are filled in when the table has them.
|
|
467
|
+
- With a Postgres `jsonb` metadata column, `findByMeta()` (`@>`) and `update()` (`||` merge) run in the database — add a GIN index on `metadata` for large tables. Other column types and dialects filter and merge in JS.
|
|
468
|
+
- Bytes are stored at `<groupId>/data/<id>`, the same layout as the S3 and unstorage providers, so moving metadata into a database keeps existing files readable.
|
|
469
|
+
- Requires the optional `drizzle-orm` peer dependency.
|
|
470
|
+
|
|
471
|
+
### Migrating from the unstorage provider
|
|
472
|
+
|
|
473
|
+
Point `blobs` at the existing mount (`blobs: 'documents'`) so the bytes stay
|
|
474
|
+
where they are, then copy the metadata sidecars into the table once —
|
|
475
|
+
`importUnstorageMetadata` keeps ids and timestamps and skips rows that already
|
|
476
|
+
exist, so re-running it is safe:
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
// server/tasks/filer/import.ts (run with `nuxi task run filer:import`)
|
|
480
|
+
import { db } from '../../utils/db'
|
|
481
|
+
import { filerFiles } from '../../db/schema'
|
|
482
|
+
|
|
483
|
+
export default defineTask({
|
|
484
|
+
meta: { description: 'Copy nuxt-filer metadata into the database' },
|
|
485
|
+
async run() {
|
|
486
|
+
const result = await importUnstorageMetadata({ from: 'documents', db, table: filerFiles })
|
|
487
|
+
return { result } // { imported, skipped }
|
|
488
|
+
},
|
|
489
|
+
})
|
|
490
|
+
```
|
|
491
|
+
|
|
326
492
|
## Custom Provider
|
|
327
493
|
|
|
328
|
-
For advanced use cases (
|
|
494
|
+
For advanced use cases (other databases, external file sync), implement the `FileStorageProvider` interface and register it in a Nitro plugin:
|
|
329
495
|
|
|
330
496
|
```ts
|
|
331
497
|
// nuxt.config.ts
|
|
@@ -372,6 +538,9 @@ export default defineNitroPlugin(() => {
|
|
|
372
538
|
interface FileStorageProvider {
|
|
373
539
|
create(groupId: string, data: Buffer | Uint8Array, meta?: FileMeta): Promise<{ id: string }>
|
|
374
540
|
get(groupId: string, id: string): Promise<StoredFile | null>
|
|
541
|
+
// Optional: metadata without the bytes. Used by sendStoredFile and the IPX
|
|
542
|
+
// route to answer 304s without reading the file; falls back to get().
|
|
543
|
+
head?(groupId: string, id: string): Promise<StoredFile | null>
|
|
375
544
|
getData(groupId: string, id: string): Promise<Buffer | null>
|
|
376
545
|
getMeta(id: string): Promise<FileMeta | null>
|
|
377
546
|
list(groupId: string): Promise<StoredFile[]>
|
package/dist/module.d.mts
CHANGED
|
@@ -12,6 +12,25 @@ interface FilerImageOptions {
|
|
|
12
12
|
route?: string;
|
|
13
13
|
/** Name to register the @nuxt/image provider under. Default: `filer`. */
|
|
14
14
|
providerName?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Where images are processed. `'local'` (default) runs IPX/sharp in this
|
|
17
|
+
* server. `'imgproxy'` or `'ipx'` (a standalone `ipx serve`) offload it: the
|
|
18
|
+
* image route redirects to the service, and upload-time transforms are
|
|
19
|
+
* fetched from it — so `sharp`/`ipx` aren't needed here.
|
|
20
|
+
*/
|
|
21
|
+
service?: 'local' | 'imgproxy' | 'ipx';
|
|
22
|
+
/** Base URL of the image service. Runtime override: `NUXT_FILER_IMAGE_BASE_URL`. */
|
|
23
|
+
baseURL?: string;
|
|
24
|
+
/** imgproxy signing key (hex). Prefer `NUXT_FILER_IMAGE_KEY`. Unsigned URLs when unset. */
|
|
25
|
+
key?: string;
|
|
26
|
+
/** imgproxy signing salt (hex). Prefer `NUXT_FILER_IMAGE_SALT`. */
|
|
27
|
+
salt?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Origin the service fetches originals from (e.g. `http://app:3000` on a
|
|
30
|
+
* private network). Defaults to the request origin; required for
|
|
31
|
+
* upload-time transforms. Runtime override: `NUXT_FILER_IMAGE_SOURCE_URL`.
|
|
32
|
+
*/
|
|
33
|
+
sourceURL?: string;
|
|
15
34
|
}
|
|
16
35
|
interface FilerTusOptions {
|
|
17
36
|
/** Enable the tus endpoint + composable. Defaults to `true` when the `tus` option is an object. */
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -21,6 +21,18 @@ const module = defineNuxtModule({
|
|
|
21
21
|
name: "useFileStorage",
|
|
22
22
|
from: resolver.resolve("./runtime/server/utils/storage")
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
name: "readUploadedFile",
|
|
26
|
+
from: resolver.resolve("./runtime/server/utils/upload")
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "readUploadedFiles",
|
|
30
|
+
from: resolver.resolve("./runtime/server/utils/upload")
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "generateThumbnail",
|
|
34
|
+
from: resolver.resolve("./runtime/server/utils/thumbnail")
|
|
35
|
+
},
|
|
24
36
|
{
|
|
25
37
|
name: "transformImage",
|
|
26
38
|
from: resolver.resolve("./runtime/server/utils/image")
|
|
@@ -43,6 +55,18 @@ const module = defineNuxtModule({
|
|
|
43
55
|
{
|
|
44
56
|
name: "createS3Provider",
|
|
45
57
|
from: resolver.resolve("./runtime/server/providers/s3")
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "createS3Client",
|
|
61
|
+
from: resolver.resolve("./runtime/server/providers/s3")
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "createDrizzleProvider",
|
|
65
|
+
from: resolver.resolve("./runtime/server/providers/drizzle")
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "importUnstorageMetadata",
|
|
69
|
+
from: resolver.resolve("./runtime/server/providers/drizzle")
|
|
46
70
|
}
|
|
47
71
|
]);
|
|
48
72
|
const typesSpecifier = "nuxt-filer/runtime/types";
|
|
@@ -92,12 +116,29 @@ const module = defineNuxtModule({
|
|
|
92
116
|
const imageEnabled = options.image !== false && (imageOpt.enabled ?? true) !== false;
|
|
93
117
|
const ipxRoute = (imageOpt.route ?? "/_filer-ipx").replace(/\/+$/, "");
|
|
94
118
|
const providerName = imageOpt.providerName ?? "filer";
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
119
|
+
const imageService = imageEnabled ? imageOpt.service ?? "local" : "local";
|
|
120
|
+
if (imageService !== "local") {
|
|
121
|
+
const runtimeFiler = nuxt.options.runtimeConfig.filer ?? {};
|
|
122
|
+
runtimeFiler.image = defu(runtimeFiler.image, {
|
|
123
|
+
baseURL: imageOpt.baseURL ?? "",
|
|
124
|
+
key: imageOpt.key ?? "",
|
|
125
|
+
salt: imageOpt.salt ?? "",
|
|
126
|
+
sourceURL: imageOpt.sourceURL ?? ""
|
|
127
|
+
});
|
|
128
|
+
nuxt.options.runtimeConfig.filer = runtimeFiler;
|
|
97
129
|
addServerHandler({
|
|
98
130
|
route: `${ipxRoute}/**`,
|
|
99
|
-
handler: resolver.resolve("./runtime/server/handlers/
|
|
131
|
+
handler: resolver.resolve("./runtime/server/handlers/image-service")
|
|
100
132
|
});
|
|
133
|
+
}
|
|
134
|
+
const shouldRegisterImage = imageEnabled && (imageOpt.enabled === "force" || hasNuxtModule("@nuxt/image"));
|
|
135
|
+
if (shouldRegisterImage) {
|
|
136
|
+
if (imageService === "local") {
|
|
137
|
+
addServerHandler({
|
|
138
|
+
route: `${ipxRoute}/**`,
|
|
139
|
+
handler: resolver.resolve("./runtime/server/handlers/ipx")
|
|
140
|
+
});
|
|
141
|
+
}
|
|
101
142
|
const optionsWithImage = nuxt.options;
|
|
102
143
|
const imageConfig = optionsWithImage.image ?? {};
|
|
103
144
|
const providers = imageConfig.providers ?? {};
|
|
@@ -123,18 +164,21 @@ const module = defineNuxtModule({
|
|
|
123
164
|
`export const storageName = ${JSON.stringify(options.storageName)};`,
|
|
124
165
|
`export const storagePath = ${JSON.stringify(options.storagePath)};`
|
|
125
166
|
].join("\n");
|
|
126
|
-
nitroConfig.virtual["#nuxt-filer-image"] =
|
|
167
|
+
nitroConfig.virtual["#nuxt-filer-image"] = [
|
|
168
|
+
`export const ipxRoute = ${JSON.stringify(ipxRoute)};`,
|
|
169
|
+
`export const imageService = ${JSON.stringify(imageService)};`
|
|
170
|
+
].join("\n");
|
|
127
171
|
nitroConfig.virtual["#nuxt-filer-tus"] = [
|
|
128
172
|
`export const tusRoute = ${JSON.stringify(tusRoute)};`,
|
|
129
173
|
`export const tusStagingDir = ${JSON.stringify(tusStagingDir)};`,
|
|
130
174
|
`export const tusMaxSize = ${JSON.stringify(tusOpt.maxSize ?? 0)};`,
|
|
131
175
|
`export const tusExpiration = ${JSON.stringify(tusOpt.expiration ?? 0)};`
|
|
132
176
|
].join("\n");
|
|
177
|
+
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
178
|
+
nitroConfig.plugins.push(
|
|
179
|
+
resolver.resolve("./runtime/server/plugins/default-storage")
|
|
180
|
+
);
|
|
133
181
|
if (options.provider === "unstorage") {
|
|
134
|
-
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
135
|
-
nitroConfig.plugins.push(
|
|
136
|
-
resolver.resolve("./runtime/server/plugins/default-storage")
|
|
137
|
-
);
|
|
138
182
|
nitroConfig.plugins.push(
|
|
139
183
|
resolver.resolve("./runtime/server/plugins/default-provider")
|
|
140
184
|
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image route for an external service (imgproxy / standalone IPX). Same URL
|
|
3
|
+
* shape as the local IPX route — `<route>/<modifiers>/<groupId>/<id>` — so the
|
|
4
|
+
* `@nuxt/image` provider is unchanged:
|
|
5
|
+
*
|
|
6
|
+
* - `_` (no modifiers) serves the stored original. This is what the service
|
|
7
|
+
* fetches as its source.
|
|
8
|
+
* - anything else redirects to the service, so signing keys stay server-side
|
|
9
|
+
* and this server never loads sharp/IPX.
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void | Buffer<ArrayBufferLike> | null>>;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineEventHandler, createError, getRequestURL, sendRedirect, setResponseHeader } from "h3";
|
|
2
|
+
import { ipxRoute } from "#nuxt-filer-image";
|
|
3
|
+
import { sendStoredFile } from "../utils/send.js";
|
|
4
|
+
import { imageServiceURL, parseModifiers, sourceURL } from "../utils/image-service.js";
|
|
5
|
+
import { useImageService } from "../utils/image-service-runtime.js";
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
const path = event.path.slice(ipxRoute.length).split("?")[0].replace(/^\/+/, "");
|
|
8
|
+
const [modifiers = "", ...segments] = path.split("/");
|
|
9
|
+
const parts = segments.filter(Boolean).map(decodeURIComponent);
|
|
10
|
+
if (parts.length < 2) {
|
|
11
|
+
throw createError({ statusCode: 404, statusMessage: "Image not found" });
|
|
12
|
+
}
|
|
13
|
+
const id = parts.pop();
|
|
14
|
+
const groupId = parts.join("/");
|
|
15
|
+
if (modifiers === "_") return sendStoredFile(event, groupId, id);
|
|
16
|
+
const config = useImageService();
|
|
17
|
+
const origin = config.sourceURL ?? getRequestURL(event).origin;
|
|
18
|
+
const url = await imageServiceURL(config, parseModifiers(modifiers), sourceURL(config, origin, groupId, id));
|
|
19
|
+
setResponseHeader(event, "cache-control", "public, max-age=86400");
|
|
20
|
+
return sendRedirect(event, url, 302);
|
|
21
|
+
});
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
defineEventHandler,
|
|
3
|
+
sendWebResponse,
|
|
4
|
+
toWebRequest,
|
|
5
|
+
useBase
|
|
6
|
+
} from "h3";
|
|
7
|
+
import * as ipxModule from "ipx";
|
|
3
8
|
import { ipxRoute } from "#nuxt-filer-image";
|
|
4
|
-
import { useFileStorageProvider } from "../provider.js";
|
|
9
|
+
import { headStoredFile, useFileStorageProvider } from "../provider.js";
|
|
5
10
|
function parseId(id) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
const trimmed = id.replace(/^\/+/, "");
|
|
12
|
+
const lastSlash = trimmed.lastIndexOf("/");
|
|
13
|
+
if (lastSlash <= 0 || lastSlash === trimmed.length - 1) return null;
|
|
14
|
+
return [trimmed.slice(0, lastSlash), trimmed.slice(lastSlash + 1)];
|
|
9
15
|
}
|
|
10
16
|
const filerStorage = {
|
|
11
17
|
name: "nuxt-filer",
|
|
@@ -13,12 +19,13 @@ const filerStorage = {
|
|
|
13
19
|
const parsed = parseId(id);
|
|
14
20
|
if (!parsed) return void 0;
|
|
15
21
|
const [groupId, fileId] = parsed;
|
|
16
|
-
const
|
|
17
|
-
const file = await provider.get(groupId, fileId);
|
|
22
|
+
const file = await headStoredFile(useFileStorageProvider(), groupId, fileId);
|
|
18
23
|
if (!file) return void 0;
|
|
19
24
|
const mtime = file.updatedAt ?? file.createdAt ?? /* @__PURE__ */ new Date();
|
|
20
25
|
return {
|
|
21
|
-
|
|
26
|
+
// HTTP dates have second precision; without truncating, the
|
|
27
|
+
// `if-modified-since` echo is always "older" than mtime and never 304s.
|
|
28
|
+
mtime: new Date(Math.floor(mtime.getTime() / 1e3) * 1e3),
|
|
22
29
|
maxAge: 60 * 60 * 24 * 365
|
|
23
30
|
};
|
|
24
31
|
},
|
|
@@ -31,13 +38,23 @@ const filerStorage = {
|
|
|
31
38
|
return data;
|
|
32
39
|
}
|
|
33
40
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
function createHandler() {
|
|
42
|
+
const ipx = ipxModule.createIPX({ storage: filerStorage });
|
|
43
|
+
const ipx4 = ipxModule;
|
|
44
|
+
if (ipx4.createIPXFetchHandler && ipx4.parseIPXURL) {
|
|
45
|
+
const { parseIPXURL } = ipx4;
|
|
46
|
+
const fetchHandler = ipx4.createIPXFetchHandler(ipx, {
|
|
47
|
+
parseURL(url) {
|
|
48
|
+
const parsed = new URL(url);
|
|
49
|
+
parsed.pathname = parsed.pathname.slice(ipxRoute.length) || "/";
|
|
50
|
+
return parseIPXURL(parsed.href);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return defineEventHandler(
|
|
54
|
+
async (event) => sendWebResponse(event, await fetchHandler(toWebRequest(event)))
|
|
55
|
+
);
|
|
40
56
|
}
|
|
41
|
-
return
|
|
57
|
+
return useBase(ipxRoute, ipxModule.createIPXH3Handler(ipx));
|
|
42
58
|
}
|
|
43
|
-
|
|
59
|
+
let _handler = null;
|
|
60
|
+
export default defineEventHandler((event) => (_handler ??= createHandler())(event));
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Mount the default filesystem-backed storage for nuxt-filer. We mount
|
|
3
3
|
* via a Nitro plugin rather than `nitroConfig.storage` so that our
|
|
4
4
|
* custom fs driver is bundled with the plugin and there is no runtime
|
|
5
|
-
* module resolution against the package's `dist/`.
|
|
5
|
+
* module resolution against the package's `dist/`. A mount the app already
|
|
6
|
+
* configured under the same name (`nitro.storage`) is left alone.
|
|
6
7
|
*/
|
|
7
8
|
declare const _default: import("nitropack/types").NitroAppPlugin;
|
|
8
9
|
export default _default;
|
|
@@ -3,5 +3,7 @@ import { storageName, storagePath } from "#nuxt-filer-options";
|
|
|
3
3
|
import fsDriver from "../drivers/fs.js";
|
|
4
4
|
export default defineNitroPlugin(() => {
|
|
5
5
|
const storage = useStorage();
|
|
6
|
+
const mounted = storage.getMount(storageName).base.replace(/:$/, "");
|
|
7
|
+
if (mounted === storageName.replace(/[:/]+/g, ":").replace(/:$/, "")) return;
|
|
6
8
|
storage.mount(storageName, fsDriver({ base: storagePath }));
|
|
7
9
|
});
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
import type { FileStorageProvider } from '../../runtime/types.js';
|
|
1
|
+
import type { FileStorageProvider, StoredFile } from '../../runtime/types.js';
|
|
2
2
|
export declare function setFileStorageProvider(provider: FileStorageProvider): void;
|
|
3
3
|
export declare function useFileStorageProvider(): FileStorageProvider;
|
|
4
|
+
/**
|
|
5
|
+
* A file's metadata without its bytes when the provider supports `head()`;
|
|
6
|
+
* otherwise `get()`, whose `data` callers can reuse instead of reading again.
|
|
7
|
+
*/
|
|
8
|
+
export declare function headStoredFile(provider: FileStorageProvider, groupId: string, id: string): Promise<StoredFile | null>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Table } from 'drizzle-orm';
|
|
2
|
+
import type { FileStorageProvider } from '../../../runtime/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Where the drizzle provider keeps file bytes — the database only holds
|
|
5
|
+
* metadata. The {@link S3Client} from `createS3Client()` satisfies this, and a
|
|
6
|
+
* string is shorthand for a Nitro storage mount name.
|
|
7
|
+
*/
|
|
8
|
+
export interface BlobStore {
|
|
9
|
+
put(key: string, body: Buffer | Uint8Array, contentType?: string): Promise<void>;
|
|
10
|
+
get(key: string): Promise<Buffer | null>;
|
|
11
|
+
delete(key: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The slice of a Drizzle database the provider calls. Kept loose on purpose:
|
|
15
|
+
* Drizzle's per-dialect builders (pg/mysql/sqlite, v0.x and v1) don't share a
|
|
16
|
+
* common base type, so any `drizzle(...)` instance is accepted here.
|
|
17
|
+
*/
|
|
18
|
+
export interface DrizzleDatabase {
|
|
19
|
+
select: (...args: any[]) => any;
|
|
20
|
+
insert: (table: any) => any;
|
|
21
|
+
update: (table: any) => any;
|
|
22
|
+
delete: (table: any) => any;
|
|
23
|
+
}
|
|
24
|
+
export interface DrizzleProviderOptions {
|
|
25
|
+
/** Your Drizzle database instance (any dialect, any async or sync driver). */
|
|
26
|
+
db: DrizzleDatabase;
|
|
27
|
+
/** The table holding file metadata. */
|
|
28
|
+
table: Table;
|
|
29
|
+
/**
|
|
30
|
+
* Where file bytes are stored: a {@link BlobStore} (e.g. `createS3Client()`)
|
|
31
|
+
* or the name of a mounted Nitro storage.
|
|
32
|
+
*/
|
|
33
|
+
blobs: BlobStore | string;
|
|
34
|
+
/** Schema property names of the columns used. Unset timestamps are optional. */
|
|
35
|
+
columns?: {
|
|
36
|
+
/** Default: `'id'`. */
|
|
37
|
+
id?: string;
|
|
38
|
+
/** Default: `'groupId'`. */
|
|
39
|
+
groupId?: string;
|
|
40
|
+
/** JSON column. Default: `'metadata'`. */
|
|
41
|
+
metadata?: string;
|
|
42
|
+
/** Set on insert when the table has it. Default: `'createdAt'`. */
|
|
43
|
+
createdAt?: string;
|
|
44
|
+
/** Set on insert/update when the table has it. Default: `'updatedAt'`. */
|
|
45
|
+
updatedAt?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Drizzle-backed {@link FileStorageProvider}: metadata lives in a database
|
|
50
|
+
* table, bytes in a {@link BlobStore}. Uses only Drizzle's core query builder,
|
|
51
|
+
* which is unchanged between v0.x and v1.
|
|
52
|
+
*
|
|
53
|
+
* When the metadata column is Postgres `jsonb`, `findByMeta` and `update` run
|
|
54
|
+
* in the database (`@>` containment / `||` merge); other dialects filter and
|
|
55
|
+
* merge in JS.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createDrizzleProvider(options: DrizzleProviderOptions): FileStorageProvider;
|
|
58
|
+
export interface ImportUnstorageMetadataOptions {
|
|
59
|
+
/** Nitro storage mount the unstorage provider wrote to (e.g. `'documents'`). */
|
|
60
|
+
from: string;
|
|
61
|
+
db: DrizzleDatabase;
|
|
62
|
+
table: Table;
|
|
63
|
+
columns?: DrizzleProviderOptions['columns'];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One-off migration from the unstorage provider: copies its JSON metadata
|
|
67
|
+
* sidecars (`<groupId>:meta:<id>`) into the Drizzle table, keeping ids and
|
|
68
|
+
* timestamps. Bytes stay where they are — point `createDrizzleProvider`'s
|
|
69
|
+
* `blobs` at the same mount. Rows that already exist are skipped, so it is
|
|
70
|
+
* safe to re-run.
|
|
71
|
+
*/
|
|
72
|
+
export declare function importUnstorageMetadata(options: ImportUnstorageMetadataOptions): Promise<{
|
|
73
|
+
imported: number;
|
|
74
|
+
skipped: number;
|
|
75
|
+
}>;
|