nuxt-media-library 0.0.57 → 0.0.58
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 +19 -8
- package/dist/module.d.mts +13 -3
- package/dist/module.d.ts +13 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +11 -19
- package/dist/runtime/components/MLMedia/Field.d.vue.ts +6 -3
- package/dist/runtime/components/MLMedia/Field.vue +3 -0
- package/dist/runtime/components/MLMedia/Field.vue.d.ts +6 -3
- package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +6 -3
- package/dist/runtime/components/MLMedia/Gallery.vue +3 -0
- package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +6 -3
- package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +4 -1
- package/dist/runtime/components/MLMedia/GallerySection.vue +3 -0
- package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +4 -1
- package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/UploadRow.vue +10 -0
- package/dist/runtime/composables/useMLMediaUpload.d.ts +28 -8
- package/dist/runtime/composables/useMLMediaUpload.js +2 -1
- package/dist/runtime/composables/useMLUploadQueue.d.ts +5 -3
- package/dist/runtime/composables/useMLUploadQueue.js +54 -5
- package/dist/runtime/utils/backends.d.ts +63 -0
- package/dist/runtime/utils/backends.js +72 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -237,9 +237,11 @@ merges the layers, so every widget on the site can use any backend by name.
|
|
|
237
237
|
export default defineNuxtConfig({
|
|
238
238
|
modules: ['nuxt-media-library'],
|
|
239
239
|
mediaLibrary: {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
backends: {
|
|
241
|
+
videos: {
|
|
242
|
+
upload: { start: '/api/videos/uploads' }, // refresh defaults to `${start}/:sessionId/grant`
|
|
243
|
+
claim: '/api/videos/uploads/:sessionId/attach', // py_client on_attach
|
|
244
|
+
},
|
|
243
245
|
},
|
|
244
246
|
},
|
|
245
247
|
})
|
|
@@ -257,17 +259,26 @@ export default defineNuxtConfig({
|
|
|
257
259
|
```vue
|
|
258
260
|
<MLMediaGallery
|
|
259
261
|
v-model="lesson.mediaIds"
|
|
260
|
-
|
|
262
|
+
backend="videos"
|
|
261
263
|
profile="lesson-video"
|
|
262
264
|
:service-body="{ context: { lessonId } }"
|
|
263
265
|
@uploaded="(assetId, role, sessionId) => attach(assetId, sessionId)"
|
|
264
266
|
/>
|
|
265
267
|
```
|
|
266
268
|
|
|
267
|
-
- `
|
|
268
|
-
`useMLMediaUpload`): a
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
- `backend` on `MLMediaGallery`, `MLMediaField` and the file picker (and
|
|
270
|
+
`useMLMediaUpload`): a name in `mediaLibrary.backends`. `service` is its
|
|
271
|
+
older name and still takes `{ start }` for a one-off; `backend` wins.
|
|
272
|
+
Without either, widgets upload through Nuxt's own route as before; an
|
|
273
|
+
unknown name fails that upload with a message naming the missing backend.
|
|
274
|
+
- Each backend has a `kind`: `media-library` (the default) or `media-kit`.
|
|
275
|
+
This version uploads only to `media-library` backends; a `media-kit` one
|
|
276
|
+
fails the upload with a clear message.
|
|
277
|
+
- With `claim`, the upload queue attaches files whose page was left before
|
|
278
|
+
the widget attached them (`POST { assetId, profile }`; 409 counts as
|
|
279
|
+
attached). The tray shows a refusal with "Add again".
|
|
280
|
+
- `mediaLibrary.serviceUploads.backends` still works: each entry becomes a
|
|
281
|
+
`media-library` backend with that `upload` and no `claim`.
|
|
271
282
|
- The start request carries the file (`mimeType`, `sizeBytes`, `filename`),
|
|
272
283
|
`profile` (each role's, in a gallery with roles) and `service-body`. The
|
|
273
284
|
service's profile decides kind, visibility and processing, so
|
package/dist/module.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
import { ServiceUploadBackend } from '../dist/runtime/utils/uploader/serviceBackends.js';
|
|
3
|
+
import { MediaBackends } from '../dist/runtime/utils/backends.js';
|
|
3
4
|
export * from '../dist/runtime/types/resourcePolicy.js';
|
|
4
5
|
export * from '../dist/runtime/types/uploader.js';
|
|
5
6
|
export * from '../dist/runtime/types/preview.js';
|
|
@@ -43,9 +44,8 @@ interface ModuleOptions {
|
|
|
43
44
|
* public API root with `/v1`, or a proxy route); with `proxy` it defaults
|
|
44
45
|
* to the pass-through route.
|
|
45
46
|
*
|
|
46
|
-
* `backends
|
|
47
|
-
*
|
|
48
|
-
* merges them across layers. Paths only: each layer serves its routes.
|
|
47
|
+
* `backends`: older form of `mediaLibrary.backends` (start/refresh only).
|
|
48
|
+
* Each entry becomes a `media-library` backend with that `upload`.
|
|
49
49
|
*/
|
|
50
50
|
serviceUploads?: {
|
|
51
51
|
proxy?: boolean;
|
|
@@ -62,6 +62,16 @@ interface ModuleOptions {
|
|
|
62
62
|
* pass-through are still registered.
|
|
63
63
|
*/
|
|
64
64
|
manageProxy?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The backend registry: named places widgets upload to and read from
|
|
67
|
+
* (`<MLMediaGallery backend="videos" />`). Each entry has a `kind`
|
|
68
|
+
* (`media-library`, the default, or `media-kit`) and its routes: `upload`
|
|
69
|
+
* (`{ start, refresh? }`, py_client `create_upload_router`), `claim`
|
|
70
|
+
* (attach a finished upload, `/…/:sessionId/attach`). Each service's Nuxt
|
|
71
|
+
* layer registers its own; Nuxt merges them. Paths only: each layer serves
|
|
72
|
+
* its routes.
|
|
73
|
+
*/
|
|
74
|
+
backends?: MediaBackends;
|
|
65
75
|
}
|
|
66
76
|
declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
|
|
67
77
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
package/dist/module.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
import { ServiceUploadBackend } from '../dist/runtime/utils/uploader/serviceBackends.js';
|
|
3
|
+
import { MediaBackends } from '../dist/runtime/utils/backends.js';
|
|
3
4
|
export * from '../dist/runtime/types/resourcePolicy.js';
|
|
4
5
|
export * from '../dist/runtime/types/uploader.js';
|
|
5
6
|
export * from '../dist/runtime/types/preview.js';
|
|
@@ -43,9 +44,8 @@ interface ModuleOptions {
|
|
|
43
44
|
* public API root with `/v1`, or a proxy route); with `proxy` it defaults
|
|
44
45
|
* to the pass-through route.
|
|
45
46
|
*
|
|
46
|
-
* `backends
|
|
47
|
-
*
|
|
48
|
-
* merges them across layers. Paths only: each layer serves its routes.
|
|
47
|
+
* `backends`: older form of `mediaLibrary.backends` (start/refresh only).
|
|
48
|
+
* Each entry becomes a `media-library` backend with that `upload`.
|
|
49
49
|
*/
|
|
50
50
|
serviceUploads?: {
|
|
51
51
|
proxy?: boolean;
|
|
@@ -62,6 +62,16 @@ interface ModuleOptions {
|
|
|
62
62
|
* pass-through are still registered.
|
|
63
63
|
*/
|
|
64
64
|
manageProxy?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The backend registry: named places widgets upload to and read from
|
|
67
|
+
* (`<MLMediaGallery backend="videos" />`). Each entry has a `kind`
|
|
68
|
+
* (`media-library`, the default, or `media-kit`) and its routes: `upload`
|
|
69
|
+
* (`{ start, refresh? }`, py_client `create_upload_router`), `claim`
|
|
70
|
+
* (attach a finished upload, `/…/:sessionId/attach`). Each service's Nuxt
|
|
71
|
+
* layer registers its own; Nuxt merges them. Paths only: each layer serves
|
|
72
|
+
* its routes.
|
|
73
|
+
*/
|
|
74
|
+
backends?: MediaBackends;
|
|
65
75
|
}
|
|
66
76
|
declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
|
|
67
77
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { defineNuxtModule, createResolver, addImportsDir, addComponentsDir, addServerHandler, useLogger, addPlugin } from '@nuxt/kit';
|
|
4
|
+
import { normalizeBackends } from '../dist/runtime/utils/backends.js';
|
|
4
5
|
export * from '../dist/runtime/types/resourcePolicy.js';
|
|
5
6
|
export * from '../dist/runtime/types/uploader.js';
|
|
6
7
|
export * from '../dist/runtime/types/preview.js';
|
|
@@ -22,23 +23,6 @@ function browserBaseURL(existingPublic, publicBaseURL) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
|
|
25
|
-
function serviceBackends(backends, logger) {
|
|
26
|
-
const out = {};
|
|
27
|
-
for (const [name, backend] of Object.entries(backends ?? {})) {
|
|
28
|
-
if (!backend || typeof backend.start !== "string" || !backend.start) {
|
|
29
|
-
logger.warn(`mediaLibrary.serviceUploads.backends.${name} has no start path; ignored.`);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (backend.refresh && !backend.refresh.includes(":sessionId")) {
|
|
33
|
-
logger.warn(`mediaLibrary.serviceUploads.backends.${name}.refresh needs a :sessionId placeholder; using the default.`);
|
|
34
|
-
}
|
|
35
|
-
out[name] = {
|
|
36
|
-
start: backend.start,
|
|
37
|
-
...backend.refresh?.includes(":sessionId") ? { refresh: backend.refresh } : {}
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
26
|
function appCatchAlls(layers) {
|
|
43
27
|
const found = [];
|
|
44
28
|
for (const { config } of layers) {
|
|
@@ -171,6 +155,10 @@ const module$1 = defineNuxtModule({
|
|
|
171
155
|
if (existingPublic.proxyUploads === false && !existingPublic.baseURL && !options.publicBaseURL) {
|
|
172
156
|
logger.warn("proxyUploads is false but no public media library address is set: set mediaLibrary.publicBaseURL. baseURL is server-side only.");
|
|
173
157
|
}
|
|
158
|
+
const backends = {
|
|
159
|
+
...normalizeBackends(options.backends, options.serviceUploads?.backends, (m) => logger.warn(m)),
|
|
160
|
+
...normalizeBackends(existingPublic.backends, existingPublic.serviceUploadBackends, (m) => logger.warn(m))
|
|
161
|
+
};
|
|
174
162
|
nuxt.options.runtimeConfig.public.mediaLibrary = {
|
|
175
163
|
...existingPublic,
|
|
176
164
|
baseURL: browserBaseURL(existingPublic.baseURL, options.publicBaseURL),
|
|
@@ -186,8 +174,12 @@ const module$1 = defineNuxtModule({
|
|
|
186
174
|
headers: publicHeaders.headers,
|
|
187
175
|
// Where useMLServiceUpload sends parts (NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BASE_URL).
|
|
188
176
|
serviceUploadBaseURL: existingPublic.serviceUploadBaseURL || options.serviceUploads?.baseURL || (options.serviceUploads?.proxy ? SERVICE_UPLOADS_ROUTE : ""),
|
|
189
|
-
//
|
|
190
|
-
|
|
177
|
+
// The backend registry (NUXT_PUBLIC_MEDIA_LIBRARY_BACKENDS): names widgets pass as `backend`.
|
|
178
|
+
backends,
|
|
179
|
+
// Older form, kept for code that reads it: each backend's upload routes.
|
|
180
|
+
serviceUploadBackends: Object.fromEntries(
|
|
181
|
+
Object.entries(backends).flatMap(([name, b]) => b.upload ? [[name, b.upload]] : [])
|
|
182
|
+
)
|
|
191
183
|
};
|
|
192
184
|
addPlugin({
|
|
193
185
|
src: resolver.resolve("./runtime/plugins/config")
|
|
@@ -18,10 +18,12 @@ type __VLS_Props = {
|
|
|
18
18
|
/** Upload profile name for new files. */
|
|
19
19
|
profile?: string | null;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* The backend registered under `mediaLibrary.backends` that uploads go to
|
|
22
|
+
* (e.g. "videos"). Its `claim` route attaches files whose page was left
|
|
23
|
+
* before they finished. Wins over `service`.
|
|
24
24
|
*/
|
|
25
|
+
backend?: string | null;
|
|
26
|
+
/** Older name for `backend`; also takes an inline `{ start }`. See useMLMediaUpload. */
|
|
25
27
|
service?: MediaUploadOptions['service'];
|
|
26
28
|
/** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
|
|
27
29
|
serviceBody?: Record<string, unknown> | null;
|
|
@@ -65,6 +67,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
65
67
|
};
|
|
66
68
|
collectionName: string | null;
|
|
67
69
|
profile: string | null;
|
|
70
|
+
backend: string | null;
|
|
68
71
|
service: string | import("../../index.js").MediaServiceUpload | null;
|
|
69
72
|
serviceBody: Record<string, unknown> | null;
|
|
70
73
|
hint: string;
|
|
@@ -16,6 +16,7 @@ const props = defineProps({
|
|
|
16
16
|
processingConfig: { type: Object, required: false, default: void 0 },
|
|
17
17
|
collectionName: { type: [String, null], required: false, default: null },
|
|
18
18
|
profile: { type: [String, null], required: false, default: null },
|
|
19
|
+
backend: { type: [String, null], required: false, default: null },
|
|
19
20
|
service: { type: null, required: false, default: void 0 },
|
|
20
21
|
serviceBody: { type: [Object, null], required: false, default: null },
|
|
21
22
|
picker: { type: Boolean, required: false, default: true },
|
|
@@ -34,6 +35,7 @@ const field = useMLMediaField(model, {
|
|
|
34
35
|
processingConfig: props.processingConfig,
|
|
35
36
|
collectionName: props.collectionName,
|
|
36
37
|
profile: props.profile,
|
|
38
|
+
backend: props.backend,
|
|
37
39
|
service: props.service,
|
|
38
40
|
serviceBody: props.serviceBody,
|
|
39
41
|
label: props.label || void 0,
|
|
@@ -77,6 +79,7 @@ const uploadOptions = computed(() => ({
|
|
|
77
79
|
processingConfig: props.processingConfig,
|
|
78
80
|
collectionName: props.collectionName,
|
|
79
81
|
profile: props.profile,
|
|
82
|
+
backend: props.backend,
|
|
80
83
|
service: props.service,
|
|
81
84
|
serviceBody: props.serviceBody
|
|
82
85
|
}));
|
|
@@ -18,10 +18,12 @@ type __VLS_Props = {
|
|
|
18
18
|
/** Upload profile name for new files. */
|
|
19
19
|
profile?: string | null;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* The backend registered under `mediaLibrary.backends` that uploads go to
|
|
22
|
+
* (e.g. "videos"). Its `claim` route attaches files whose page was left
|
|
23
|
+
* before they finished. Wins over `service`.
|
|
24
24
|
*/
|
|
25
|
+
backend?: string | null;
|
|
26
|
+
/** Older name for `backend`; also takes an inline `{ start }`. See useMLMediaUpload. */
|
|
25
27
|
service?: MediaUploadOptions['service'];
|
|
26
28
|
/** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
|
|
27
29
|
serviceBody?: Record<string, unknown> | null;
|
|
@@ -65,6 +67,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
65
67
|
};
|
|
66
68
|
collectionName: string | null;
|
|
67
69
|
profile: string | null;
|
|
70
|
+
backend: string | null;
|
|
68
71
|
service: string | import("../../index.js").MediaServiceUpload | null;
|
|
69
72
|
serviceBody: Record<string, unknown> | null;
|
|
70
73
|
hint: string;
|
|
@@ -35,10 +35,12 @@ type __VLS_Props = {
|
|
|
35
35
|
/** Upload profile for the single list (with `profiles`, each role has its own). */
|
|
36
36
|
profile?: string | null;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* The backend registered under `mediaLibrary.backends` that uploads go to
|
|
39
|
+
* (e.g. "videos"). Its `claim` route attaches files whose page was left
|
|
40
|
+
* before they finished. Each role's profile is sent with the start request. Wins over `service`.
|
|
41
41
|
*/
|
|
42
|
+
backend?: string | null;
|
|
43
|
+
/** Older name for `backend`; also takes an inline `{ start }`. */
|
|
42
44
|
service?: MediaUploadOptions['service'];
|
|
43
45
|
/** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
|
|
44
46
|
serviceBody?: Record<string, unknown> | null;
|
|
@@ -76,6 +78,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
76
78
|
};
|
|
77
79
|
collectionName: string | null;
|
|
78
80
|
profile: string | null;
|
|
81
|
+
backend: string | null;
|
|
79
82
|
service: string | import("../../index.js").MediaServiceUpload | null;
|
|
80
83
|
serviceBody: Record<string, unknown> | null;
|
|
81
84
|
hint: string;
|
|
@@ -19,6 +19,7 @@ const props = defineProps({
|
|
|
19
19
|
processingConfig: { type: Object, required: false, default: void 0 },
|
|
20
20
|
collectionName: { type: [String, null], required: false, default: null },
|
|
21
21
|
profile: { type: [String, null], required: false, default: null },
|
|
22
|
+
backend: { type: [String, null], required: false, default: null },
|
|
22
23
|
service: { type: null, required: false, default: void 0 },
|
|
23
24
|
serviceBody: { type: [Object, null], required: false, default: null },
|
|
24
25
|
picker: { type: Boolean, required: false, default: true },
|
|
@@ -275,6 +276,7 @@ function chipCount(key, max) {
|
|
|
275
276
|
:profile="defaultRole?.profile ?? null"
|
|
276
277
|
:profile-for="profileFor"
|
|
277
278
|
:on-route-files="routeFiles"
|
|
279
|
+
:backend="backend"
|
|
278
280
|
:service="service"
|
|
279
281
|
:service-body="serviceBody"
|
|
280
282
|
:disabled="disabled"
|
|
@@ -353,6 +355,7 @@ function chipCount(key, max) {
|
|
|
353
355
|
:processing-config="processingConfig"
|
|
354
356
|
:collection-name="collectionName"
|
|
355
357
|
:profile="profile"
|
|
358
|
+
:backend="backend"
|
|
356
359
|
:service="service"
|
|
357
360
|
:service-body="serviceBody"
|
|
358
361
|
:picker="picker"
|
|
@@ -35,10 +35,12 @@ type __VLS_Props = {
|
|
|
35
35
|
/** Upload profile for the single list (with `profiles`, each role has its own). */
|
|
36
36
|
profile?: string | null;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* The backend registered under `mediaLibrary.backends` that uploads go to
|
|
39
|
+
* (e.g. "videos"). Its `claim` route attaches files whose page was left
|
|
40
|
+
* before they finished. Each role's profile is sent with the start request. Wins over `service`.
|
|
41
41
|
*/
|
|
42
|
+
backend?: string | null;
|
|
43
|
+
/** Older name for `backend`; also takes an inline `{ start }`. */
|
|
42
44
|
service?: MediaUploadOptions['service'];
|
|
43
45
|
/** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
|
|
44
46
|
serviceBody?: Record<string, unknown> | null;
|
|
@@ -76,6 +78,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
76
78
|
};
|
|
77
79
|
collectionName: string | null;
|
|
78
80
|
profile: string | null;
|
|
81
|
+
backend: string | null;
|
|
79
82
|
service: string | import("../../index.js").MediaServiceUpload | null;
|
|
80
83
|
serviceBody: Record<string, unknown> | null;
|
|
81
84
|
hint: string;
|
|
@@ -26,7 +26,9 @@ type __VLS_Props = {
|
|
|
26
26
|
onOpen?: (assetId: string) => void;
|
|
27
27
|
/** Upload profile name for new files in this list. */
|
|
28
28
|
profile?: string | null;
|
|
29
|
-
/**
|
|
29
|
+
/** Registered backend uploads go to (`mediaLibrary.backends`). Wins over `service`. */
|
|
30
|
+
backend?: string | null;
|
|
31
|
+
/** Older name for `backend`; also takes an inline `{ start }`. See useMLMediaUpload. */
|
|
30
32
|
service?: MediaUploadOptions['service'];
|
|
31
33
|
/** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
|
|
32
34
|
serviceBody?: Record<string, unknown> | null;
|
|
@@ -89,6 +91,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
89
91
|
};
|
|
90
92
|
collectionName: string | null;
|
|
91
93
|
profile: string | null;
|
|
94
|
+
backend: string | null;
|
|
92
95
|
service: string | import("../../index.js").MediaServiceUpload | null;
|
|
93
96
|
serviceBody: Record<string, unknown> | null;
|
|
94
97
|
profileFor: (file: File) => string | null | undefined;
|
|
@@ -24,6 +24,7 @@ const props = defineProps({
|
|
|
24
24
|
editor: { type: Boolean, required: false, default: true },
|
|
25
25
|
onOpen: { type: Function, required: false, default: void 0 },
|
|
26
26
|
profile: { type: [String, null], required: false, default: null },
|
|
27
|
+
backend: { type: [String, null], required: false, default: null },
|
|
27
28
|
service: { type: null, required: false, default: void 0 },
|
|
28
29
|
serviceBody: { type: [Object, null], required: false, default: null },
|
|
29
30
|
removeLabel: { type: String, required: false, default: void 0 },
|
|
@@ -45,6 +46,7 @@ const gallery = useMLMediaGallery(model, {
|
|
|
45
46
|
processingConfig: props.processingConfig,
|
|
46
47
|
collectionName: props.collectionName,
|
|
47
48
|
profile: props.profile,
|
|
49
|
+
backend: props.backend,
|
|
48
50
|
service: props.service,
|
|
49
51
|
serviceBody: props.serviceBody,
|
|
50
52
|
label: props.recordName ? `${props.title} of ${props.recordName}` : props.title,
|
|
@@ -130,6 +132,7 @@ async function pickExisting() {
|
|
|
130
132
|
collectionName: props.collectionName,
|
|
131
133
|
profile: props.profile,
|
|
132
134
|
profileFor: props.profileFor,
|
|
135
|
+
backend: props.backend,
|
|
133
136
|
service: props.service,
|
|
134
137
|
serviceBody: props.serviceBody
|
|
135
138
|
},
|
|
@@ -26,7 +26,9 @@ type __VLS_Props = {
|
|
|
26
26
|
onOpen?: (assetId: string) => void;
|
|
27
27
|
/** Upload profile name for new files in this list. */
|
|
28
28
|
profile?: string | null;
|
|
29
|
-
/**
|
|
29
|
+
/** Registered backend uploads go to (`mediaLibrary.backends`). Wins over `service`. */
|
|
30
|
+
backend?: string | null;
|
|
31
|
+
/** Older name for `backend`; also takes an inline `{ start }`. See useMLMediaUpload. */
|
|
30
32
|
service?: MediaUploadOptions['service'];
|
|
31
33
|
/** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
|
|
32
34
|
serviceBody?: Record<string, unknown> | null;
|
|
@@ -89,6 +91,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
89
91
|
};
|
|
90
92
|
collectionName: string | null;
|
|
91
93
|
profile: string | null;
|
|
94
|
+
backend: string | null;
|
|
92
95
|
service: string | import("../../index.js").MediaServiceUpload | null;
|
|
93
96
|
serviceBody: Record<string, unknown> | null;
|
|
94
97
|
profileFor: (file: File) => string | null | undefined;
|
|
@@ -42,12 +42,12 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
42
42
|
progress: number | null;
|
|
43
43
|
name: string;
|
|
44
44
|
kind: string | null;
|
|
45
|
+
duration: number | null;
|
|
45
46
|
disabled: boolean;
|
|
46
47
|
selected: boolean;
|
|
47
48
|
phase: ProgressVeilPhase | null;
|
|
48
49
|
missingAlt: boolean;
|
|
49
50
|
onOpen: () => void;
|
|
50
|
-
duration: number | null;
|
|
51
51
|
selectable: boolean;
|
|
52
52
|
selecting: boolean;
|
|
53
53
|
phaseDetail: string;
|
|
@@ -42,12 +42,12 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
42
42
|
progress: number | null;
|
|
43
43
|
name: string;
|
|
44
44
|
kind: string | null;
|
|
45
|
+
duration: number | null;
|
|
45
46
|
disabled: boolean;
|
|
46
47
|
selected: boolean;
|
|
47
48
|
phase: ProgressVeilPhase | null;
|
|
48
49
|
missingAlt: boolean;
|
|
49
50
|
onOpen: () => void;
|
|
50
|
-
duration: number | null;
|
|
51
51
|
selectable: boolean;
|
|
52
52
|
selecting: boolean;
|
|
53
53
|
phaseDetail: string;
|
|
@@ -56,6 +56,7 @@ const subtitle = computed(() => {
|
|
|
56
56
|
return ["Processing", at, x.step && stepLabel(x.step), elapsed.value].filter(Boolean).join(" \xB7 ");
|
|
57
57
|
}
|
|
58
58
|
case "ready":
|
|
59
|
+
if (x.orphaned && x.claimError) return `Not added${where.value ? ` to ${where.value}` : ""}: ${x.claimError}`;
|
|
59
60
|
if (x.orphaned) return `Uploaded, not added${where.value ? ` to ${where.value}` : ""}`;
|
|
60
61
|
return ["Ready to use", where.value].filter(Boolean).join(" \xB7 ");
|
|
61
62
|
case "failed":
|
|
@@ -191,6 +192,15 @@ async function retryStep() {
|
|
|
191
192
|
:loading="retrying"
|
|
192
193
|
@click="retryStep"
|
|
193
194
|
/>
|
|
195
|
+
<UButton
|
|
196
|
+
v-else-if="u.orphaned && u.claimError && u.assetId"
|
|
197
|
+
icon="i-lucide-rotate-ccw"
|
|
198
|
+
color="neutral"
|
|
199
|
+
variant="outline"
|
|
200
|
+
size="xs"
|
|
201
|
+
label="Add again"
|
|
202
|
+
@click="queue.claim(u)"
|
|
203
|
+
/>
|
|
194
204
|
<UButton
|
|
195
205
|
v-if="canShow"
|
|
196
206
|
:to="u.context.to"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChunkUploaderConfig } from '../types/uploader.js';
|
|
2
2
|
import type { ServiceUploadChoice } from '../utils/uploader/serviceBackends.js';
|
|
3
3
|
export type { MediaServiceUpload, ServiceUploadBackend, ServiceUploadChoice } from '../utils/uploader/serviceBackends.js';
|
|
4
|
+
export type { MediaBackend, MediaBackendKind, MediaBackends } from '../utils/backends.js';
|
|
4
5
|
export type MediaUploadPhase = 'uploading' | 'processing' | 'ready' | 'failed' | 'canceled';
|
|
5
6
|
/** What a policy-less upload session takes. See docs/resource-policy/retirement.md. */
|
|
6
7
|
export interface MediaUploadOptions {
|
|
@@ -23,10 +24,18 @@ export interface MediaUploadOptions {
|
|
|
23
24
|
/** Overrides the uploader config the module provides. */
|
|
24
25
|
chunkUploader?: ChunkUploaderConfig;
|
|
25
26
|
/**
|
|
27
|
+
* The backend registered under `mediaLibrary.backends` that uploads go to
|
|
28
|
+
* (e.g. `'videos'`). Its `upload.start` starts them (py_client
|
|
29
|
+
* `create_upload_router`); without one they go through Nuxt's own route.
|
|
30
|
+
* Its `claim` route attaches files whose widget went away first.
|
|
31
|
+
* Wins over `service`.
|
|
32
|
+
*/
|
|
33
|
+
backend?: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Older name for `backend`, which also takes an inline `{ start, … }`.
|
|
26
36
|
* Start uploads at a backend service (py_client `create_upload_router`)
|
|
27
|
-
* instead of Nuxt's own upload route: the name of a backend
|
|
28
|
-
*
|
|
29
|
-
* `{ start, … }`. The service creates the session and its profile decides
|
|
37
|
+
* instead of Nuxt's own upload route: the name of a registered backend
|
|
38
|
+
* (e.g. `'videos'`), or `{ start, … }`. The service creates the session and its profile decides
|
|
30
39
|
* kind, visibility and processing, so those options are not used here;
|
|
31
40
|
* `profile` is sent as the start request's `profile`. The file's parts go
|
|
32
41
|
* to `mediaLibrary.serviceUploads` (pass-through route or public URL).
|
|
@@ -69,11 +78,12 @@ export interface MediaUpload {
|
|
|
69
78
|
readonly created: Promise<string | null>;
|
|
70
79
|
/** Bytes. Kept for uploads restored after a reload, whose file is gone. */
|
|
71
80
|
size: number;
|
|
81
|
+
/** The registered backend it went to (`mediaLibrary.backends`), or null. */
|
|
82
|
+
backend: string | null;
|
|
72
83
|
/**
|
|
73
|
-
* Where the upload started: the backend name
|
|
74
|
-
* `
|
|
75
|
-
*
|
|
76
|
-
* and `assetId`, enough to attach the file at that service.
|
|
84
|
+
* Where the upload started: the backend name (e.g. `'videos'`), the start
|
|
85
|
+
* URL for an inline `{ start }`, or null for Nuxt's own route. With
|
|
86
|
+
* `sessionId` and `assetId`, enough to attach the file at that service.
|
|
77
87
|
*/
|
|
78
88
|
service: string | null;
|
|
79
89
|
/** Upload profile sent with the session (after `profileFor`), or null. */
|
|
@@ -81,8 +91,14 @@ export interface MediaUpload {
|
|
|
81
91
|
context: MediaUploadContext | null;
|
|
82
92
|
/** The widget added the asset to its record. */
|
|
83
93
|
attached: boolean;
|
|
84
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* The widget went away (page change) before it could add the asset. With
|
|
96
|
+
* a backend `claim` route the queue attaches it there; otherwise the app
|
|
97
|
+
* does (onCreated).
|
|
98
|
+
*/
|
|
85
99
|
orphaned: boolean;
|
|
100
|
+
/** Why attaching an orphaned upload at its backend failed, or null. */
|
|
101
|
+
claimError: string | null;
|
|
86
102
|
/** Current processing step (server name), and its place in the pipeline. */
|
|
87
103
|
step: string | null;
|
|
88
104
|
stepIndex: number | null;
|
|
@@ -150,6 +166,7 @@ export declare function useMLMediaUpload(defaults?: MediaUploadOptions): {
|
|
|
150
166
|
readonly [Symbol.toStringTag]: string;
|
|
151
167
|
};
|
|
152
168
|
size: number;
|
|
169
|
+
backend: string | null;
|
|
153
170
|
service: string | null;
|
|
154
171
|
profile: string | null;
|
|
155
172
|
context: {
|
|
@@ -163,6 +180,7 @@ export declare function useMLMediaUpload(defaults?: MediaUploadOptions): {
|
|
|
163
180
|
} | null;
|
|
164
181
|
attached: boolean;
|
|
165
182
|
orphaned: boolean;
|
|
183
|
+
claimError: string | null;
|
|
166
184
|
step: string | null;
|
|
167
185
|
stepIndex: number | null;
|
|
168
186
|
stepTotal: number | null;
|
|
@@ -217,6 +235,7 @@ export declare function useMLMediaUpload(defaults?: MediaUploadOptions): {
|
|
|
217
235
|
readonly [Symbol.toStringTag]: string;
|
|
218
236
|
};
|
|
219
237
|
size: number;
|
|
238
|
+
backend: string | null;
|
|
220
239
|
service: string | null;
|
|
221
240
|
profile: string | null;
|
|
222
241
|
context: {
|
|
@@ -230,6 +249,7 @@ export declare function useMLMediaUpload(defaults?: MediaUploadOptions): {
|
|
|
230
249
|
} | null;
|
|
231
250
|
attached: boolean;
|
|
232
251
|
orphaned: boolean;
|
|
252
|
+
claimError: string | null;
|
|
233
253
|
step: string | null;
|
|
234
254
|
stepIndex: number | null;
|
|
235
255
|
stepTotal: number | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCurrentScope, onScopeDispose, ref } from "vue";
|
|
2
2
|
import { useRoute, useRuntimeConfig } from "#imports";
|
|
3
|
+
import { legacyBackends } from "../utils/backends.js";
|
|
3
4
|
import { useMLChunkUploader } from "./useMLChunkUploader.js";
|
|
4
5
|
import { useMLUploadQueue } from "./useMLUploadQueue.js";
|
|
5
6
|
let owners = 0;
|
|
@@ -8,7 +9,7 @@ export function useMLMediaUpload(defaults = {}) {
|
|
|
8
9
|
const publicConfig = useRuntimeConfig().public?.mediaLibrary;
|
|
9
10
|
const service = {
|
|
10
11
|
baseURL: publicConfig?.serviceUploadBaseURL,
|
|
11
|
-
backends: publicConfig?.serviceUploadBackends
|
|
12
|
+
backends: publicConfig?.backends ?? legacyBackends(publicConfig?.serviceUploadBackends)
|
|
12
13
|
};
|
|
13
14
|
const queue = useMLUploadQueue();
|
|
14
15
|
const route = useRoute();
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { type Ref } from 'vue';
|
|
2
2
|
import type { ChunkUploaderConfig } from '../types/uploader.js';
|
|
3
|
-
import { type
|
|
3
|
+
import { type MediaBackends } from '../utils/backends.js';
|
|
4
4
|
import type { MediaUpload, MediaUploadContext, MediaUploadOptions } from './useMLMediaUpload.js';
|
|
5
5
|
interface StartMeta {
|
|
6
6
|
owner: string;
|
|
7
7
|
config: ChunkUploaderConfig;
|
|
8
|
-
/** Public
|
|
8
|
+
/** Public upload config, read in setup by the caller: where service-upload parts go, and the backend registry. */
|
|
9
9
|
service?: {
|
|
10
10
|
baseURL?: string;
|
|
11
|
-
backends?:
|
|
11
|
+
backends?: MediaBackends;
|
|
12
12
|
};
|
|
13
13
|
context?: MediaUploadContext | null;
|
|
14
14
|
}
|
|
@@ -25,6 +25,8 @@ export declare function useMLUploadQueue(): {
|
|
|
25
25
|
cancelUploads: () => void;
|
|
26
26
|
retryStep: (upload: MediaUpload) => Promise<void>;
|
|
27
27
|
onCreated: (fn: (upload: MediaUpload) => void) => () => boolean;
|
|
28
|
+
/** Try attaching an orphaned upload at its backend's claim route again. */
|
|
29
|
+
claim: (upload: MediaUpload) => Promise<void>;
|
|
28
30
|
/** Let the next unload through (the user confirmed it). */
|
|
29
31
|
allowUnload: () => void;
|
|
30
32
|
};
|
|
@@ -2,7 +2,7 @@ import { computed, reactive, ref, watch } from "vue";
|
|
|
2
2
|
import { $fetch } from "ofetch";
|
|
3
3
|
import { ChunkUploader } from "../utils/uploader/uploader.js";
|
|
4
4
|
import { createServiceUpload } from "../utils/uploader/serviceUpload.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveUploadTarget } from "../utils/backends.js";
|
|
6
6
|
import { kindForFile, mimeForFile } from "../utils/fileAccept.js";
|
|
7
7
|
import { stepLabel, usableAfter } from "../utils/processingSteps.js";
|
|
8
8
|
import { useMLProcessingStatus } from "./useMLProcessingStatus.js";
|
|
@@ -14,6 +14,8 @@ function createQueue() {
|
|
|
14
14
|
const owners = /* @__PURE__ */ new Map();
|
|
15
15
|
const cleanups = /* @__PURE__ */ new Map();
|
|
16
16
|
const createdHandlers = /* @__PURE__ */ new Set();
|
|
17
|
+
const claimUrls = /* @__PURE__ */ new Map();
|
|
18
|
+
const claiming = /* @__PURE__ */ new Set();
|
|
17
19
|
const trays = ref(0);
|
|
18
20
|
let allowUnload = false;
|
|
19
21
|
const processing = import.meta.client ? useMLProcessingStatus() : null;
|
|
@@ -26,6 +28,7 @@ function createQueue() {
|
|
|
26
28
|
file,
|
|
27
29
|
kind,
|
|
28
30
|
size: file.size,
|
|
31
|
+
backend: null,
|
|
29
32
|
service: null,
|
|
30
33
|
profile: null,
|
|
31
34
|
localPreview,
|
|
@@ -37,6 +40,7 @@ function createQueue() {
|
|
|
37
40
|
context,
|
|
38
41
|
attached: false,
|
|
39
42
|
orphaned: false,
|
|
43
|
+
claimError: null,
|
|
40
44
|
step: null,
|
|
41
45
|
stepIndex: null,
|
|
42
46
|
stepTotal: null,
|
|
@@ -170,10 +174,18 @@ function createQueue() {
|
|
|
170
174
|
const localPreview = isVisual && typeof URL !== "undefined" ? URL.createObjectURL(file) : null;
|
|
171
175
|
const { upload, resolveCreated } = create(file, kind, meta.context ?? null, localPreview);
|
|
172
176
|
upload.profile = opts.profile ?? null;
|
|
173
|
-
upload.service = typeof opts.service === "string" ? opts.service : opts.service?.start ?? null;
|
|
177
|
+
upload.service = opts.backend || (typeof opts.service === "string" ? opts.service : opts.service?.start ?? null);
|
|
174
178
|
const cfg = meta.config.proxyUploads ? { ...meta.config, baseURL: "" } : meta.config;
|
|
175
179
|
let uploader = null;
|
|
176
180
|
let serviceRun = null;
|
|
181
|
+
let target = null;
|
|
182
|
+
let targetError = null;
|
|
183
|
+
try {
|
|
184
|
+
target = resolveUploadTarget(opts.backend, opts.service, meta.service?.backends, opts.serviceBody);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
targetError = err;
|
|
187
|
+
}
|
|
188
|
+
upload.backend = target?.backend ?? null;
|
|
177
189
|
const onProgress = (p) => {
|
|
178
190
|
upload.progress = p.percent;
|
|
179
191
|
};
|
|
@@ -210,9 +222,11 @@ function createQueue() {
|
|
|
210
222
|
upload.error = null;
|
|
211
223
|
upload.sessionId = null;
|
|
212
224
|
try {
|
|
213
|
-
|
|
225
|
+
if (targetError) throw targetError;
|
|
226
|
+
const service = target?.service ?? null;
|
|
214
227
|
const { assetId, sessionId } = service ? await uploadViaService(service) : await uploadViaNuxt();
|
|
215
228
|
upload.sessionId = sessionId;
|
|
229
|
+
claimUrls.set(upload.key, sessionId && target?.claimUrl ? target.claimUrl(sessionId) : null);
|
|
216
230
|
if (!assetId) {
|
|
217
231
|
upload.phase = "failed";
|
|
218
232
|
upload.error = "The upload finished but no file was created";
|
|
@@ -222,6 +236,7 @@ function createQueue() {
|
|
|
222
236
|
upload.assetId = assetId;
|
|
223
237
|
resolveCreated(assetId);
|
|
224
238
|
for (const fn of createdHandlers) fn(upload);
|
|
239
|
+
if (upload.orphaned) void claim(upload);
|
|
225
240
|
followProcessing(upload, assetId);
|
|
226
241
|
} catch (err) {
|
|
227
242
|
if (upload.phase === "canceled") return;
|
|
@@ -255,9 +270,33 @@ function createQueue() {
|
|
|
255
270
|
owners.get(upload.key)?.delete(owner);
|
|
256
271
|
if (unmounted && !upload.attached && (upload.phase === "uploading" || upload.phase === "processing")) {
|
|
257
272
|
upload.orphaned = true;
|
|
273
|
+
if (upload.assetId) void claim(upload);
|
|
274
|
+
persist();
|
|
258
275
|
}
|
|
259
276
|
pruneOrphan(upload);
|
|
260
277
|
}
|
|
278
|
+
async function claim(upload) {
|
|
279
|
+
const url = claimUrls.get(upload.key);
|
|
280
|
+
if (!url || upload.attached || claiming.has(upload.key) || !upload.assetId) return;
|
|
281
|
+
claiming.add(upload.key);
|
|
282
|
+
try {
|
|
283
|
+
await $fetch(url, { method: "POST", body: { assetId: upload.assetId, profile: upload.profile } });
|
|
284
|
+
upload.attached = true;
|
|
285
|
+
upload.orphaned = false;
|
|
286
|
+
upload.claimError = null;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (err?.statusCode === 409 || err?.status === 409) {
|
|
289
|
+
upload.attached = true;
|
|
290
|
+
upload.orphaned = false;
|
|
291
|
+
upload.claimError = null;
|
|
292
|
+
} else {
|
|
293
|
+
upload.claimError = err?.data?.detail ?? err?.data?.message ?? err?.statusMessage ?? err?.message ?? "Could not add the file";
|
|
294
|
+
}
|
|
295
|
+
} finally {
|
|
296
|
+
claiming.delete(upload.key);
|
|
297
|
+
persist();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
261
300
|
function pruneOrphan(upload) {
|
|
262
301
|
const held = owners.get(upload.key)?.size ?? 0;
|
|
263
302
|
const finished = upload.phase === "ready" || upload.phase === "canceled";
|
|
@@ -268,6 +307,7 @@ function createQueue() {
|
|
|
268
307
|
cleanups.get(upload.key)?.();
|
|
269
308
|
cleanups.delete(upload.key);
|
|
270
309
|
owners.delete(upload.key);
|
|
310
|
+
claimUrls.delete(upload.key);
|
|
271
311
|
items.value = items.value.filter((u) => u.key !== upload.key);
|
|
272
312
|
persist();
|
|
273
313
|
}
|
|
@@ -290,7 +330,7 @@ function createQueue() {
|
|
|
290
330
|
}
|
|
291
331
|
function persist() {
|
|
292
332
|
if (!import.meta.client) return;
|
|
293
|
-
const stored = items.value.filter((u) => u.phase === "processing" && u.
|
|
333
|
+
const stored = items.value.filter((u) => u.assetId && (u.phase === "processing" || u.orphaned && !u.attached && claimUrls.get(u.key))).map((u) => ({ assetId: u.assetId, sessionId: u.sessionId, backend: u.backend, claimUrl: claimUrls.get(u.key) ?? null, service: u.service, profile: u.profile, name: u.file.name, kind: u.kind, size: u.size, context: u.context }));
|
|
294
334
|
try {
|
|
295
335
|
if (stored.length) sessionStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
|
|
296
336
|
else sessionStorage.removeItem(STORAGE_KEY);
|
|
@@ -312,13 +352,20 @@ function createQueue() {
|
|
|
312
352
|
upload.size = s.size;
|
|
313
353
|
upload.assetId = s.assetId;
|
|
314
354
|
upload.sessionId = s.sessionId ?? null;
|
|
355
|
+
upload.backend = s.backend ?? null;
|
|
315
356
|
upload.service = s.service ?? null;
|
|
316
357
|
upload.profile = s.profile ?? null;
|
|
317
|
-
upload.attached = true;
|
|
318
358
|
resolveCreated(s.assetId);
|
|
319
359
|
owners.set(upload.key, /* @__PURE__ */ new Set());
|
|
360
|
+
claimUrls.set(upload.key, s.claimUrl ?? null);
|
|
320
361
|
items.value = [...items.value, upload];
|
|
321
362
|
followProcessing(upload, s.assetId);
|
|
363
|
+
if (s.claimUrl) {
|
|
364
|
+
upload.orphaned = true;
|
|
365
|
+
void claim(upload);
|
|
366
|
+
} else {
|
|
367
|
+
upload.attached = true;
|
|
368
|
+
}
|
|
322
369
|
}
|
|
323
370
|
}
|
|
324
371
|
if (import.meta.client) {
|
|
@@ -341,6 +388,8 @@ function createQueue() {
|
|
|
341
388
|
cancelUploads,
|
|
342
389
|
retryStep,
|
|
343
390
|
onCreated,
|
|
391
|
+
/** Try attaching an orphaned upload at its backend's claim route again. */
|
|
392
|
+
claim,
|
|
344
393
|
/** Let the next unload through (the user confirmed it). */
|
|
345
394
|
allowUnload: () => {
|
|
346
395
|
allowUnload = true;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type MediaServiceUpload, type ServiceUploadBackend, type ServiceUploadChoice } from './uploader/serviceBackends.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which system a backend is. `media-library`: the current media library,
|
|
4
|
+
* reached through the module's /api/media-library proxies and media grants.
|
|
5
|
+
* `media-kit`: an app's own media-kit routes (not supported by this version's
|
|
6
|
+
* widgets yet; registering one is allowed, using it fails clearly).
|
|
7
|
+
*/
|
|
8
|
+
export type MediaBackendKind = 'media-library' | 'media-kit';
|
|
9
|
+
/**
|
|
10
|
+
* One entry of `mediaLibrary.backends`: a named place widgets read from and
|
|
11
|
+
* upload to (`<MLMediaGallery backend="videos" />`). Paths only: each layer
|
|
12
|
+
* serves its own routes. Paths with `:sessionId` / `:id` get them filled in.
|
|
13
|
+
*/
|
|
14
|
+
export interface MediaBackend {
|
|
15
|
+
/** Default `media-library`. */
|
|
16
|
+
kind?: MediaBackendKind;
|
|
17
|
+
/**
|
|
18
|
+
* Where uploads start. Without it, a `media-library` backend uploads
|
|
19
|
+
* through Nuxt's own route (`/api/media-library/manage/uploads`).
|
|
20
|
+
* `start`: py_client `create_upload_router`'s prefix. `refresh`: its grant
|
|
21
|
+
* refresh, default `<start>/:sessionId/grant`.
|
|
22
|
+
*/
|
|
23
|
+
upload?: ServiceUploadBackend;
|
|
24
|
+
/**
|
|
25
|
+
* Attach a finished upload: `POST` with `{ assetId, profile }`, e.g.
|
|
26
|
+
* `/api/videos/uploads/:sessionId/attach` (py_client
|
|
27
|
+
* `create_upload_router(on_attach=…)`). When set, the upload queue attaches
|
|
28
|
+
* files whose widget went away before it could (page change), so the app
|
|
29
|
+
* needs no plugin for them.
|
|
30
|
+
*/
|
|
31
|
+
claim?: string;
|
|
32
|
+
/** Planned (media-kit): list, resolve and asset routes. Not used yet; `media-library` backends use the module's proxies. */
|
|
33
|
+
list?: string;
|
|
34
|
+
resolve?: string;
|
|
35
|
+
asset?: string;
|
|
36
|
+
}
|
|
37
|
+
export type MediaBackends = Record<string, MediaBackend>;
|
|
38
|
+
/**
|
|
39
|
+
* The registry as public config: `backends`, plus the older
|
|
40
|
+
* `serviceUploads.backends` as `media-library` backends with only `upload`
|
|
41
|
+
* (an entry in `backends` wins over one of the same name). Invalid entries
|
|
42
|
+
* are dropped with a warning.
|
|
43
|
+
*/
|
|
44
|
+
export declare function normalizeBackends(backends: MediaBackends | undefined, legacy: Record<string, ServiceUploadBackend> | undefined, warn: (message: string) => void): MediaBackends;
|
|
45
|
+
/** The registry from only the older `serviceUploadBackends` public config (runtime overrides). */
|
|
46
|
+
export declare function legacyBackends(legacy: Record<string, ServiceUploadBackend> | null | undefined): MediaBackends | undefined;
|
|
47
|
+
/** Where an upload goes, resolved from a widget's `backend` (or `service`). */
|
|
48
|
+
export interface ResolvedUploadTarget {
|
|
49
|
+
/** The registered name, or null (inline `{ start }`, or no backend). */
|
|
50
|
+
backend: string | null;
|
|
51
|
+
/** The service that starts the upload, or null for Nuxt's own route. */
|
|
52
|
+
service: MediaServiceUpload | null;
|
|
53
|
+
/** The claim URL for this session, when the backend has one. */
|
|
54
|
+
claimUrl: ((sessionId: string) => string) | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a widget's `backend` / `service` against the registry. `backend`
|
|
58
|
+
* wins; `service` is its older name and still takes an inline `{ start }`.
|
|
59
|
+
* `body` (e.g. `{ context }`) is merged into the start request.
|
|
60
|
+
*
|
|
61
|
+
* @throws Error for an unregistered name, or a `media-kit` backend.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveUploadTarget(backend: string | null | undefined, service: ServiceUploadChoice, registry: MediaBackends | null | undefined, body?: Record<string, unknown> | null): ResolvedUploadTarget;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { resolveServiceUpload } from "./uploader/serviceBackends.js";
|
|
2
|
+
const KINDS = ["media-library", "media-kit"];
|
|
3
|
+
export function normalizeBackends(backends, legacy, warn) {
|
|
4
|
+
const out = {};
|
|
5
|
+
const upload = (where, u) => {
|
|
6
|
+
if (u === void 0) return void 0;
|
|
7
|
+
if (!u || typeof u.start !== "string" || !u.start) {
|
|
8
|
+
warn(`${where} has no start path; ignored.`);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
if (u.refresh && !u.refresh.includes(":sessionId")) {
|
|
12
|
+
warn(`${where}.refresh needs a :sessionId placeholder; using the default.`);
|
|
13
|
+
}
|
|
14
|
+
return { start: u.start, ...u.refresh?.includes(":sessionId") ? { refresh: u.refresh } : {} };
|
|
15
|
+
};
|
|
16
|
+
for (const [name, b] of Object.entries(legacy ?? {})) {
|
|
17
|
+
const u = upload(`mediaLibrary.serviceUploads.backends.${name}`, b);
|
|
18
|
+
if (u) out[name] = { kind: "media-library", upload: u };
|
|
19
|
+
}
|
|
20
|
+
for (const [name, b] of Object.entries(backends ?? {})) {
|
|
21
|
+
const where = `mediaLibrary.backends.${name}`;
|
|
22
|
+
if (!b || typeof b !== "object") {
|
|
23
|
+
warn(`${where} is not an object; ignored.`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const kind = b.kind ?? "media-library";
|
|
27
|
+
if (!KINDS.includes(kind)) {
|
|
28
|
+
warn(`${where}.kind must be one of ${KINDS.join(", ")}; ignored.`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const entry = { kind };
|
|
32
|
+
const u = upload(`${where}.upload`, b.upload);
|
|
33
|
+
if (u === null) continue;
|
|
34
|
+
if (u) entry.upload = u;
|
|
35
|
+
if (b.claim) {
|
|
36
|
+
if (b.claim.includes(":sessionId")) entry.claim = b.claim;
|
|
37
|
+
else warn(`${where}.claim needs a :sessionId placeholder; ignored.`);
|
|
38
|
+
}
|
|
39
|
+
for (const key of ["list", "resolve", "asset"]) {
|
|
40
|
+
if (typeof b[key] === "string" && b[key]) entry[key] = b[key];
|
|
41
|
+
}
|
|
42
|
+
out[name] = entry;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export function legacyBackends(legacy) {
|
|
47
|
+
return legacy ? normalizeBackends(void 0, legacy, () => {
|
|
48
|
+
}) : void 0;
|
|
49
|
+
}
|
|
50
|
+
function fill(path, sessionId) {
|
|
51
|
+
return path.replace(/:sessionId\b|\{sessionId\}/g, encodeURIComponent(sessionId));
|
|
52
|
+
}
|
|
53
|
+
export function resolveUploadTarget(backend, service, registry, body) {
|
|
54
|
+
const choice = backend || service;
|
|
55
|
+
if (!choice) return { backend: null, service: null, claimUrl: null };
|
|
56
|
+
if (typeof choice !== "string") {
|
|
57
|
+
return { backend: null, service: resolveServiceUpload(choice, null, body), claimUrl: null };
|
|
58
|
+
}
|
|
59
|
+
const entry = registry?.[choice];
|
|
60
|
+
if (!entry) {
|
|
61
|
+
throw new Error(`No backend '${choice}': add it under mediaLibrary.backends in nuxt.config.`);
|
|
62
|
+
}
|
|
63
|
+
if (entry.kind === "media-kit") {
|
|
64
|
+
throw new Error(`Backend '${choice}' is a media-kit backend; this version of nuxt-media-library cannot upload to media-kit yet.`);
|
|
65
|
+
}
|
|
66
|
+
const claim = entry.claim;
|
|
67
|
+
return {
|
|
68
|
+
backend: choice,
|
|
69
|
+
service: entry.upload ? resolveServiceUpload(choice, { [choice]: entry.upload }, body) : null,
|
|
70
|
+
claimUrl: claim ? (sessionId) => fill(claim, sessionId) : null
|
|
71
|
+
};
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-media-library",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.58",
|
|
4
4
|
"description": "Nuxt module for media library management — chunked uploads, asset previews, and resource policies",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"dev": "yarn dev:prepare && nuxi dev playground",
|
|
39
39
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
40
40
|
"build": "nuxt build playground",
|
|
41
|
+
"docs:dev": "nuxt-module-build prepare && nuxi dev docs",
|
|
42
|
+
"docs:generate": "nuxt-module-build prepare && nuxi generate docs",
|
|
41
43
|
"admin:dev": "yarn dev:prepare && yarn --cwd admin-playground dev",
|
|
42
44
|
"prepack": "nuxt-module-build build",
|
|
43
45
|
"lint": "eslint .",
|
|
@@ -74,6 +76,7 @@
|
|
|
74
76
|
"dashjs": "^5.1.1",
|
|
75
77
|
"eslint": "^9.39.2",
|
|
76
78
|
"hls.js": "^1.6.16",
|
|
79
|
+
"msw": "2.15.0",
|
|
77
80
|
"nuxt": "^4.2.2",
|
|
78
81
|
"sirv": "^3.0.1",
|
|
79
82
|
"typescript": "~5.9.3",
|