nuxt-upload-kit 0.1.22 → 0.1.24

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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-upload-kit",
3
3
  "configKey": "uploadKit",
4
- "version": "0.1.22",
4
+ "version": "0.1.24",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -71,6 +71,10 @@ export declare const useUploadKit: <TUploadResult = any>(_options?: UploadOption
71
71
  readonly meta: {
72
72
  readonly [x: string]: Readonly<unknown>;
73
73
  };
74
+ readonly thumbnail?: {
75
+ readonly url: string;
76
+ readonly storageKey: string;
77
+ } | undefined;
74
78
  } | {
75
79
  readonly source: Exclude<import("./types.js").FileSource, "local">;
76
80
  readonly data: null;
@@ -93,6 +97,10 @@ export declare const useUploadKit: <TUploadResult = any>(_options?: UploadOption
93
97
  readonly meta: {
94
98
  readonly [x: string]: Readonly<unknown>;
95
99
  };
100
+ readonly thumbnail?: {
101
+ readonly url: string;
102
+ readonly storageKey: string;
103
+ } | undefined;
96
104
  })[], readonly ({
97
105
  readonly source: "local";
98
106
  readonly data: {
@@ -164,6 +172,10 @@ export declare const useUploadKit: <TUploadResult = any>(_options?: UploadOption
164
172
  readonly meta: {
165
173
  readonly [x: string]: Readonly<unknown>;
166
174
  };
175
+ readonly thumbnail?: {
176
+ readonly url: string;
177
+ readonly storageKey: string;
178
+ } | undefined;
167
179
  } | {
168
180
  readonly source: Exclude<import("./types.js").FileSource, "local">;
169
181
  readonly data: null;
@@ -186,6 +198,10 @@ export declare const useUploadKit: <TUploadResult = any>(_options?: UploadOption
186
198
  readonly meta: {
187
199
  readonly [x: string]: Readonly<unknown>;
188
200
  };
201
+ readonly thumbnail?: {
202
+ readonly url: string;
203
+ readonly storageKey: string;
204
+ } | undefined;
189
205
  })[]>>;
190
206
  totalProgress: import("vue").ComputedRef<number>;
191
207
  isReady: Readonly<import("vue").Ref<boolean, boolean>>;
@@ -2,7 +2,7 @@ import mitt from "mitt";
2
2
  import { computed, onBeforeUnmount, readonly, ref } from "vue";
3
3
  import { ValidatorAllowedFileTypes, ValidatorMaxFileSize, ValidatorMaxFiles } from "./validators/index.js";
4
4
  import { PluginThumbnailGenerator, PluginImageCompressor } from "./plugins/index.js";
5
- import { createPluginContext, createFileError, getExtension, setupInitialFiles } from "./utils.js";
5
+ import { createPluginContext, createFileError, getExtension, setupInitialFiles, dataUrlToBlob, deriveThumbnailKey } from "./utils.js";
6
6
  import { createPluginRunner } from "./plugin-runner.js";
7
7
  import { createFileOperations } from "./file-operations.js";
8
8
  const defaultOptions = {
@@ -65,6 +65,7 @@ export const useUploadKit = (_options = {}) => {
65
65
  })
66
66
  );
67
67
  }
68
+ const shouldUploadThumbnails = options.thumbnails !== false && options.thumbnails !== void 0 && typeof options.thumbnails === "object" && options.thumbnails.upload === true;
68
69
  const { getPluginEmitFn, runPluginStage } = createPluginRunner({ options, files, emitter });
69
70
  const uploadHolder = { fn: async () => {
70
71
  } };
@@ -205,7 +206,22 @@ export const useUploadKit = (_options = {}) => {
205
206
  const currentFile = files.value.find((f) => f.id === processedFile.id);
206
207
  const preview = currentFile?.preview || remoteUrl;
207
208
  const storageKey = extractStorageKey(uploadResult);
208
- updateFile(processedFile.id, { status: "complete", uploadResult, remoteUrl, preview, storageKey });
209
+ let thumbnail;
210
+ if (shouldUploadThumbnails && currentFile?.preview?.startsWith("data:")) {
211
+ try {
212
+ const thumbnailBlob = dataUrlToBlob(currentFile.preview);
213
+ const thumbnailKey = deriveThumbnailKey(processedFile.id);
214
+ const thumbnailResult = await storagePlugin.upload(thumbnailBlob, thumbnailKey, {
215
+ contentType: thumbnailBlob.type
216
+ });
217
+ thumbnail = { url: thumbnailResult.url, storageKey: thumbnailResult.storageKey };
218
+ } catch (err) {
219
+ if (import.meta.dev) {
220
+ console.warn(`[useUploadKit] Thumbnail upload failed for "${processedFile.name}":`, err);
221
+ }
222
+ }
223
+ }
224
+ updateFile(processedFile.id, { status: "complete", uploadResult, remoteUrl, preview, storageKey, thumbnail });
209
225
  };
210
226
  const upload = async () => {
211
227
  const filesToUpload = files.value.filter((f) => f.status === "waiting");
@@ -12,8 +12,11 @@ export interface AzureDataLakeOptions {
12
12
  * - File SAS (sr=b): Called per file for granular access control
13
13
  *
14
14
  * @param storageKey - The intended storage path for the file
15
+ * @param operation - The storage operation being performed. Use this to:
16
+ * - Generate SAS tokens with appropriate permissions per operation
17
+ * - Apply different server-side path resolution (e.g., prepend org prefix only for "upload")
15
18
  */
16
- getSASUrl?: (storageKey: string) => Promise<string>;
19
+ getSASUrl?: (storageKey: string, operation: "upload" | "delete" | "read") => Promise<string>;
17
20
  /**
18
21
  * Optional subdirectory path within the container
19
22
  * @example "uploads/images"
@@ -44,7 +44,7 @@ export const PluginAzureDataLake = defineStorageAdapter((options) => {
44
44
  return true;
45
45
  }
46
46
  };
47
- const getSasUrlForFile = async (storageKey) => {
47
+ const getSasUrlForFile = async (storageKey, operation) => {
48
48
  if (options.sasURL) {
49
49
  detectedMode ??= detectSasMode(options.sasURL);
50
50
  return options.sasURL;
@@ -52,16 +52,16 @@ export const PluginAzureDataLake = defineStorageAdapter((options) => {
52
52
  if (!options.getSASUrl) {
53
53
  throw new Error("Either sasURL or getSASUrl must be provided");
54
54
  }
55
- if (detectedMode === "file") return options.getSASUrl(storageKey);
55
+ if (detectedMode === "file") return options.getSASUrl(storageKey, operation);
56
56
  if (!detectedMode) {
57
- const url = await options.getSASUrl(storageKey);
57
+ const url = await options.getSASUrl(storageKey, operation);
58
58
  detectedMode = detectSasMode(url);
59
59
  sasURL.value = url;
60
60
  if (import.meta.dev) console.debug(`[Azure Storage] Auto-detected SAS mode: ${detectedMode}`);
61
61
  if (detectedMode === "file") return url;
62
62
  }
63
63
  if (isTokenExpired(sasURL.value)) {
64
- refreshPromise ??= options.getSASUrl(storageKey).then((url) => {
64
+ refreshPromise ??= options.getSASUrl(storageKey, operation).then((url) => {
65
65
  refreshPromise = null;
66
66
  sasURL.value = url;
67
67
  return url;
@@ -88,8 +88,8 @@ export const PluginAzureDataLake = defineStorageAdapter((options) => {
88
88
  }
89
89
  return dir.getFileClient(filename);
90
90
  };
91
- const getFileClient = async (storageKey) => {
92
- const sasUrl = await getSasUrlForFile(storageKey);
91
+ const getFileClient = async (storageKey, operation) => {
92
+ const sasUrl = await getSasUrlForFile(storageKey, operation);
93
93
  if (detectedMode === "file") {
94
94
  return new DataLakeFileClient(sasUrl);
95
95
  }
@@ -97,6 +97,26 @@ export const PluginAzureDataLake = defineStorageAdapter((options) => {
97
97
  };
98
98
  return {
99
99
  id: "azure-datalake-storage",
100
+ async upload(data, storageKey, uploadOptions) {
101
+ const requestKey = buildFullStorageKey(storageKey, true);
102
+ const fileClient = await getFileClient(requestKey, "upload");
103
+ const contentType = uploadOptions?.contentType || "application/octet-stream";
104
+ await fileClient.upload(data, {
105
+ pathHttpHeaders: {
106
+ ...options.pathHttpHeaders,
107
+ contentType
108
+ },
109
+ onProgress: uploadOptions?.onProgress ? ({ loadedBytes }) => {
110
+ const percentage = Math.round(loadedBytes / data.size * 100);
111
+ uploadOptions.onProgress(percentage);
112
+ } : void 0
113
+ });
114
+ const actualStorageKey = getBlobPathFromUrl(fileClient.url) || requestKey;
115
+ return {
116
+ url: fileClient.url,
117
+ storageKey: actualStorageKey
118
+ };
119
+ },
100
120
  hooks: {
101
121
  /**
102
122
  * Upload file to Azure Blob Storage
@@ -106,7 +126,7 @@ export const PluginAzureDataLake = defineStorageAdapter((options) => {
106
126
  throw new Error("Cannot upload remote file - no local data available");
107
127
  }
108
128
  const requestKey = buildFullStorageKey(file.id, true);
109
- const fileClient = await getFileClient(requestKey);
129
+ const fileClient = await getFileClient(requestKey, "upload");
110
130
  await fileClient.upload(file.data, {
111
131
  metadata: {
112
132
  ...options.metadata,
@@ -134,7 +154,7 @@ export const PluginAzureDataLake = defineStorageAdapter((options) => {
134
154
  * Expects the full storageKey (e.g., "basePath/subdir/filename.jpg").
135
155
  */
136
156
  async getRemoteFile(storageKey, _context) {
137
- const fileClient = await getFileClient(storageKey);
157
+ const fileClient = await getFileClient(storageKey, "read");
138
158
  const properties = await fileClient.getProperties();
139
159
  return {
140
160
  size: properties.contentLength || 0,
@@ -158,7 +178,7 @@ export const PluginAzureDataLake = defineStorageAdapter((options) => {
158
178
  }
159
179
  return;
160
180
  }
161
- const fileClient = await getFileClient(storageKey);
181
+ const fileClient = await getFileClient(storageKey, "delete");
162
182
  await fileClient.deleteIfExists();
163
183
  }
164
184
  }
@@ -56,6 +56,12 @@ export const PluginFirebaseStorage = defineStorageAdapter((options) => {
56
56
  };
57
57
  return {
58
58
  id: "firebase-storage",
59
+ async upload(data, storageKey, uploadOptions) {
60
+ const fullKey = buildFullStorageKey(storageKey);
61
+ const contentType = uploadOptions?.contentType || "application/octet-stream";
62
+ return uploadToFirebase(fullKey, data, contentType, storageKey, uploadOptions?.onProgress || (() => {
63
+ }));
64
+ },
59
65
  hooks: {
60
66
  /**
61
67
  * Upload file to Firebase Storage
@@ -33,6 +33,23 @@ export const PluginS3 = defineStorageAdapter((options) => {
33
33
  }
34
34
  return {
35
35
  id: "s3-storage",
36
+ async upload(data, storageKey, uploadOptions) {
37
+ return withRetry(async () => {
38
+ const fullKey = buildFullStorageKey(storageKey);
39
+ const contentType = uploadOptions?.contentType || "application/octet-stream";
40
+ const { uploadUrl, publicUrl } = await options.getPresignedUploadUrl(fullKey, contentType, {
41
+ fileName: storageKey,
42
+ fileSize: data.size
43
+ });
44
+ const etag = await uploadWithProgress(uploadUrl, data, contentType, uploadOptions?.onProgress || (() => {
45
+ }));
46
+ return {
47
+ url: publicUrl,
48
+ storageKey: fullKey,
49
+ etag
50
+ };
51
+ }, `Standalone upload "${storageKey}"`);
52
+ },
36
53
  hooks: {
37
54
  /**
38
55
  * Upload file to S3 using presigned URL
@@ -81,6 +81,14 @@ export interface BaseUploadFile<TUploadResult = any> {
81
81
  * Plugins can add data here (e.g., { extension: 'jpg', originalWidth: 4000 })
82
82
  */
83
83
  meta: Record<string, unknown>;
84
+ /**
85
+ * Thumbnail upload result (populated when `thumbnails.upload` is enabled).
86
+ * Contains the URL and storage key for the uploaded thumbnail.
87
+ */
88
+ thumbnail?: {
89
+ url: string;
90
+ storageKey: string;
91
+ };
84
92
  }
85
93
  /**
86
94
  * Local upload file - originates from user's device
@@ -272,6 +280,26 @@ export interface ThumbnailOptions {
272
280
  width?: number;
273
281
  height?: number;
274
282
  quality?: number;
283
+ /**
284
+ * Upload generated thumbnails to storage alongside main files.
285
+ * Requires a storage adapter with standalone upload support.
286
+ *
287
+ * When enabled, after each file uploads, its thumbnail preview
288
+ * is converted to a Blob and uploaded using `storage.upload()`.
289
+ * The result is stored on `file.thumbnail`.
290
+ *
291
+ * @default false
292
+ */
293
+ upload?: boolean;
294
+ }
295
+ /**
296
+ * Options for standalone upload (bypassing the useUploadKit pipeline)
297
+ */
298
+ export interface StandaloneUploadOptions {
299
+ /** MIME type of the data being uploaded. Defaults to 'application/octet-stream'. */
300
+ contentType?: string;
301
+ /** Progress callback (0-100) */
302
+ onProgress?: (percentage: number) => void;
275
303
  }
276
304
  export interface ImageCompressionOptions {
277
305
  maxWidth?: number;
@@ -405,6 +433,39 @@ export interface StoragePlugin<TUploadResult = any, TPluginEvents extends Record
405
433
  hooks: StoragePluginHooks<TUploadResult, TPluginEvents>;
406
434
  options?: UploadOptions;
407
435
  events?: TPluginEvents;
436
+ /**
437
+ * Upload a raw Blob or File directly to storage, bypassing the useUploadKit pipeline.
438
+ *
439
+ * This is a low-level primitive for uploading arbitrary data without going through
440
+ * validation, preprocessing, or processing stages. Useful for:
441
+ * - Uploading edited/cropped images
442
+ * - Uploading thumbnails separately
443
+ * - Any scenario where you have a Blob and just need it in storage
444
+ *
445
+ * The `storageKey` is treated like a filename — the adapter prepends any configured
446
+ * path prefix (e.g., `options.path`), and the server-side SAS/presigned URL handler
447
+ * may further resolve it (e.g., prepend organization ID).
448
+ *
449
+ * @param data - The Blob or File to upload
450
+ * @param storageKey - Filename or relative path for the file in storage
451
+ * @param options - Optional content type and progress callback
452
+ * @returns The upload result with `url` and resolved `storageKey`
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * const storage = PluginAzureDataLake({ getSASUrl: ... })
457
+ *
458
+ * // Upload an edited image
459
+ * const result = await storage.upload(croppedBlob, 'edited_photo.jpg', {
460
+ * contentType: 'image/jpeg',
461
+ * })
462
+ * console.log(result.storageKey) // Full resolved path in storage
463
+ * ```
464
+ */
465
+ upload: (data: Blob | File, storageKey: string, options?: StandaloneUploadOptions) => Promise<TUploadResult & {
466
+ url: string;
467
+ storageKey: string;
468
+ }>;
408
469
  }
409
470
  /**
410
471
  * Base plugin interface (for internal use - supports both types)
@@ -19,6 +19,16 @@ export declare function calculateThumbnailDimensions(originalWidth: number, orig
19
19
  width: number;
20
20
  height: number;
21
21
  };
22
+ /**
23
+ * Convert a base64-encoded data URL (e.g., from canvas.toDataURL) to a Blob.
24
+ * Only supports base64-encoded data URLs (`;base64,` format).
25
+ */
26
+ export declare function dataUrlToBlob(dataUrl: string): Blob;
27
+ /**
28
+ * Derive a thumbnail storage key from a file ID by appending '_thumb' before the extension
29
+ * @example "1738345678901-abc123.jpg" → "1738345678901-abc123_thumb.jpg"
30
+ */
31
+ export declare function deriveThumbnailKey(fileId: string): string;
22
32
  /**
23
33
  * Cleanup object URLs to prevent memory leaks
24
34
  * @param urlMap Map of file IDs to object URLs
@@ -37,6 +37,24 @@ export function calculateThumbnailDimensions(originalWidth, originalHeight, maxW
37
37
  }
38
38
  return { width, height };
39
39
  }
40
+ export function dataUrlToBlob(dataUrl) {
41
+ if (!dataUrl.includes(";base64,")) {
42
+ throw new Error("dataUrlToBlob only supports base64-encoded data URLs");
43
+ }
44
+ const [header, base64] = dataUrl.split(",");
45
+ const mimeType = header.match(/:(.*?);/)?.[1] ?? "image/jpeg";
46
+ const binary = atob(base64);
47
+ const bytes = new Uint8Array(binary.length);
48
+ for (let i = 0; i < binary.length; i++) {
49
+ bytes[i] = binary.charCodeAt(i);
50
+ }
51
+ return new Blob([bytes], { type: mimeType });
52
+ }
53
+ export function deriveThumbnailKey(fileId) {
54
+ const lastDot = fileId.lastIndexOf(".");
55
+ if (lastDot === -1) return `${fileId}_thumb`;
56
+ return `${fileId.slice(0, lastDot)}_thumb${fileId.slice(lastDot)}`;
57
+ }
40
58
  export function cleanupObjectURLs(urlMap, fileId) {
41
59
  if (fileId) {
42
60
  const url = urlMap.get(fileId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-upload-kit",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "A powerful, plugin-based file upload manager for Nuxt applications",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/genu/nuxt-upload-kit.git",
@@ -50,35 +50,35 @@
50
50
  "docs:dev": "nuxt dev docs --extends docus"
51
51
  },
52
52
  "dependencies": {
53
- "mitt": "^3.0.1"
53
+ "mitt": "3.0.1"
54
54
  },
55
55
  "devDependencies": {
56
- "@aws-sdk/client-s3": "3.982.0",
57
- "@aws-sdk/lib-storage": "3.982.0",
58
- "@azure/storage-file-datalake": "^12.29.0",
56
+ "@aws-sdk/client-s3": "3.988.0",
57
+ "@aws-sdk/lib-storage": "3.988.0",
58
+ "@azure/storage-file-datalake": "12.29.0",
59
59
  "@ffmpeg/ffmpeg": "0.12.15",
60
60
  "@ffmpeg/util": "0.12.2",
61
- "@nuxt/devtools": "^3.1.1",
62
- "@nuxt/eslint-config": "^1.13.0",
63
- "@nuxt/kit": "^4.3.0",
64
- "@nuxt/module-builder": "^1.0.2",
65
- "@nuxt/schema": "^4.3.0",
66
- "@nuxt/test-utils": "^3.23.0",
61
+ "@nuxt/devtools": "3.2.1",
62
+ "@nuxt/eslint-config": "1.15.1",
63
+ "@nuxt/kit": "4.3.1",
64
+ "@nuxt/module-builder": "1.0.2",
65
+ "@nuxt/schema": "4.3.1",
66
+ "@nuxt/test-utils": "4.0.0",
67
67
  "@types/node": "latest",
68
- "@vitejs/plugin-vue": "^6.0.4",
69
- "@vitest/coverage-v8": "^4.0.18",
70
- "eslint": "^9.39.2",
68
+ "@vitejs/plugin-vue": "6.0.4",
69
+ "@vitest/coverage-v8": "4.0.18",
70
+ "eslint": "10.0.0",
71
71
  "eslint-config-prettier": "10.1.8",
72
72
  "eslint-plugin-prettier": "5.5.5",
73
- "firebase": "^12.8.0",
74
- "happy-dom": "20.5.0",
75
- "nuxt": "^4.3.0",
76
- "prettier": "^3.8.1",
77
- "typescript": "~5.9.3",
78
- "unbuild": "^3.6.1",
79
- "vitest": "^4.0.18",
80
- "vue": "^3.5.27",
81
- "vue-tsc": "^3.2.4"
73
+ "firebase": "12.9.0",
74
+ "happy-dom": "20.6.1",
75
+ "nuxt": "4.3.1",
76
+ "prettier": "3.8.1",
77
+ "typescript": "5.9.3",
78
+ "unbuild": "3.6.1",
79
+ "vitest": "4.0.18",
80
+ "vue": "3.5.28",
81
+ "vue-tsc": "3.2.4"
82
82
  },
83
83
  "peerDependencies": {
84
84
  "@azure/storage-file-datalake": "^12.0.0",
@@ -100,5 +100,5 @@
100
100
  "optional": true
101
101
  }
102
102
  },
103
- "packageManager": "pnpm@10.28.2"
103
+ "packageManager": "pnpm@10.29.3"
104
104
  }