@kiyasov/platform-hono 1.6.1 → 2.0.0
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/.claude/settings.local.json +7 -1
- package/dist/cjs/src/adapters/hono-adapter.d.ts +1 -0
- package/dist/cjs/src/adapters/hono-adapter.js +14 -3
- package/dist/cjs/src/adapters/hono-adapter.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/Upload.d.ts +5 -7
- package/dist/cjs/src/drivers/graphQLUpload/Upload.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/fs-capacitor.d.ts +20 -8
- package/dist/cjs/src/drivers/graphQLUpload/fs-capacitor.js +111 -58
- package/dist/cjs/src/drivers/graphQLUpload/fs-capacitor.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/index.d.ts +9 -3
- package/dist/cjs/src/drivers/graphQLUpload/index.js +21 -3
- package/dist/cjs/src/drivers/graphQLUpload/index.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/processRequest.d.ts +8 -1
- package/dist/cjs/src/drivers/graphQLUpload/processRequest.js +43 -37
- package/dist/cjs/src/drivers/graphQLUpload/processRequest.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/storage/capacitor-storage.d.ts +15 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/capacitor-storage.js +47 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/capacitor-storage.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/index.d.ts +3 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/index.js +20 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/index.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/memory-storage.d.ts +13 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/memory-storage.js +31 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/memory-storage.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/storage.d.ts +17 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/storage.js +3 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/storage.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/file.d.ts +6 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/file.js +62 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/file.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/index.d.ts +2 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/index.js +19 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/index.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/validators.d.ts +18 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/validators.js +171 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/validators.js.map +1 -0
- package/dist/cjs/src/multer/index.d.ts +1 -0
- package/dist/cjs/src/multer/index.js +1 -0
- package/dist/cjs/src/multer/index.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/any-files-interceptor.d.ts +2 -2
- package/dist/cjs/src/multer/interceptors/any-files-interceptor.js +6 -23
- package/dist/cjs/src/multer/interceptors/any-files-interceptor.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/base-interceptor.d.ts +6 -0
- package/dist/cjs/src/multer/interceptors/base-interceptor.js +26 -0
- package/dist/cjs/src/multer/interceptors/base-interceptor.js.map +1 -0
- package/dist/cjs/src/multer/interceptors/file-fields-interceptor.d.ts +2 -2
- package/dist/cjs/src/multer/interceptors/file-fields-interceptor.js +7 -24
- package/dist/cjs/src/multer/interceptors/file-fields-interceptor.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/file-interceptor.d.ts +2 -2
- package/dist/cjs/src/multer/interceptors/file-interceptor.js +6 -23
- package/dist/cjs/src/multer/interceptors/file-interceptor.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/files-interceptor.d.ts +2 -2
- package/dist/cjs/src/multer/interceptors/files-interceptor.js +6 -23
- package/dist/cjs/src/multer/interceptors/files-interceptor.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/index.d.ts +1 -0
- package/dist/cjs/src/multer/interceptors/index.js +1 -0
- package/dist/cjs/src/multer/interceptors/index.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/any-files.d.ts +2 -8
- package/dist/cjs/src/multer/multipart/handlers/any-files.js +12 -25
- package/dist/cjs/src/multer/multipart/handlers/any-files.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/base-handler.d.ts +42 -0
- package/dist/cjs/src/multer/multipart/handlers/base-handler.js +106 -0
- package/dist/cjs/src/multer/multipart/handlers/base-handler.js.map +1 -0
- package/dist/cjs/src/multer/multipart/handlers/file-fields.d.ts +3 -10
- package/dist/cjs/src/multer/multipart/handlers/file-fields.js +19 -33
- package/dist/cjs/src/multer/multipart/handlers/file-fields.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/index.d.ts +6 -1
- package/dist/cjs/src/multer/multipart/handlers/index.js +13 -0
- package/dist/cjs/src/multer/multipart/handlers/index.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/multiple-files.d.ts +2 -8
- package/dist/cjs/src/multer/multipart/handlers/multiple-files.js +18 -36
- package/dist/cjs/src/multer/multipart/handlers/multiple-files.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/single-file.d.ts +2 -8
- package/dist/cjs/src/multer/multipart/handlers/single-file.js +11 -33
- package/dist/cjs/src/multer/multipart/handlers/single-file.js.map +1 -1
- package/dist/cjs/src/multer/multipart/index.d.ts +1 -1
- package/dist/cjs/src/multer/multipart/options.d.ts +10 -16
- package/dist/cjs/src/multer/multipart/options.js.map +1 -1
- package/dist/cjs/src/multer/multipart/request.js +14 -3
- package/dist/cjs/src/multer/multipart/request.js.map +1 -1
- package/dist/cjs/src/multer/storage/disk-storage.d.ts +2 -1
- package/dist/cjs/src/multer/storage/disk-storage.js +2 -1
- package/dist/cjs/src/multer/storage/disk-storage.js.map +1 -1
- package/dist/cjs/src/multer/storage/memory-storage.d.ts +2 -11
- package/dist/cjs/src/multer/storage/memory-storage.js +6 -4
- package/dist/cjs/src/multer/storage/memory-storage.js.map +1 -1
- package/dist/cjs/src/multer/storage/storage.d.ts +6 -5
- package/dist/cjs/src/multer/utils/file.d.ts +6 -0
- package/dist/cjs/src/multer/utils/file.js +62 -0
- package/dist/cjs/src/multer/utils/file.js.map +1 -0
- package/dist/cjs/src/multer/utils/index.d.ts +2 -0
- package/dist/cjs/src/multer/utils/index.js +19 -0
- package/dist/cjs/src/multer/utils/index.js.map +1 -0
- package/dist/cjs/src/multer/utils/validators.d.ts +18 -0
- package/dist/cjs/src/multer/utils/validators.js +171 -0
- package/dist/cjs/src/multer/utils/validators.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/adapters/hono-adapter.d.ts +1 -0
- package/dist/esm/src/adapters/hono-adapter.js +14 -3
- package/dist/esm/src/adapters/hono-adapter.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/Upload.d.ts +5 -7
- package/dist/esm/src/drivers/graphQLUpload/Upload.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/fs-capacitor.d.ts +20 -8
- package/dist/esm/src/drivers/graphQLUpload/fs-capacitor.js +112 -59
- package/dist/esm/src/drivers/graphQLUpload/fs-capacitor.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/index.d.ts +9 -3
- package/dist/esm/src/drivers/graphQLUpload/index.js +7 -3
- package/dist/esm/src/drivers/graphQLUpload/index.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/processRequest.d.ts +8 -1
- package/dist/esm/src/drivers/graphQLUpload/processRequest.js +42 -36
- package/dist/esm/src/drivers/graphQLUpload/processRequest.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/storage/capacitor-storage.d.ts +15 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/capacitor-storage.js +43 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/capacitor-storage.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/index.d.ts +3 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/index.js +4 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/index.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/memory-storage.d.ts +13 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/memory-storage.js +27 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/memory-storage.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/storage.d.ts +17 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/storage.js +2 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/storage.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/file.d.ts +6 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/file.js +54 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/file.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/index.d.ts +2 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/index.js +3 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/index.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/validators.d.ts +18 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/validators.js +167 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/validators.js.map +1 -0
- package/dist/esm/src/multer/index.d.ts +1 -0
- package/dist/esm/src/multer/index.js +1 -0
- package/dist/esm/src/multer/index.js.map +1 -1
- package/dist/esm/src/multer/interceptors/any-files-interceptor.d.ts +2 -2
- package/dist/esm/src/multer/interceptors/any-files-interceptor.js +6 -23
- package/dist/esm/src/multer/interceptors/any-files-interceptor.js.map +1 -1
- package/dist/esm/src/multer/interceptors/base-interceptor.d.ts +6 -0
- package/dist/esm/src/multer/interceptors/base-interceptor.js +23 -0
- package/dist/esm/src/multer/interceptors/base-interceptor.js.map +1 -0
- package/dist/esm/src/multer/interceptors/file-fields-interceptor.d.ts +2 -2
- package/dist/esm/src/multer/interceptors/file-fields-interceptor.js +7 -24
- package/dist/esm/src/multer/interceptors/file-fields-interceptor.js.map +1 -1
- package/dist/esm/src/multer/interceptors/file-interceptor.d.ts +2 -2
- package/dist/esm/src/multer/interceptors/file-interceptor.js +6 -23
- package/dist/esm/src/multer/interceptors/file-interceptor.js.map +1 -1
- package/dist/esm/src/multer/interceptors/files-interceptor.d.ts +2 -2
- package/dist/esm/src/multer/interceptors/files-interceptor.js +6 -23
- package/dist/esm/src/multer/interceptors/files-interceptor.js.map +1 -1
- package/dist/esm/src/multer/interceptors/index.d.ts +1 -0
- package/dist/esm/src/multer/interceptors/index.js +1 -0
- package/dist/esm/src/multer/interceptors/index.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/any-files.d.ts +2 -8
- package/dist/esm/src/multer/multipart/handlers/any-files.js +12 -25
- package/dist/esm/src/multer/multipart/handlers/any-files.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/base-handler.d.ts +42 -0
- package/dist/esm/src/multer/multipart/handlers/base-handler.js +102 -0
- package/dist/esm/src/multer/multipart/handlers/base-handler.js.map +1 -0
- package/dist/esm/src/multer/multipart/handlers/file-fields.d.ts +3 -10
- package/dist/esm/src/multer/multipart/handlers/file-fields.js +19 -33
- package/dist/esm/src/multer/multipart/handlers/file-fields.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/index.d.ts +6 -1
- package/dist/esm/src/multer/multipart/handlers/index.js +6 -1
- package/dist/esm/src/multer/multipart/handlers/index.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/multiple-files.d.ts +2 -8
- package/dist/esm/src/multer/multipart/handlers/multiple-files.js +18 -36
- package/dist/esm/src/multer/multipart/handlers/multiple-files.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/single-file.d.ts +2 -8
- package/dist/esm/src/multer/multipart/handlers/single-file.js +11 -33
- package/dist/esm/src/multer/multipart/handlers/single-file.js.map +1 -1
- package/dist/esm/src/multer/multipart/index.d.ts +1 -1
- package/dist/esm/src/multer/multipart/options.d.ts +10 -16
- package/dist/esm/src/multer/multipart/options.js.map +1 -1
- package/dist/esm/src/multer/multipart/request.js +14 -3
- package/dist/esm/src/multer/multipart/request.js.map +1 -1
- package/dist/esm/src/multer/storage/disk-storage.d.ts +2 -1
- package/dist/esm/src/multer/storage/disk-storage.js +2 -1
- package/dist/esm/src/multer/storage/disk-storage.js.map +1 -1
- package/dist/esm/src/multer/storage/memory-storage.d.ts +2 -11
- package/dist/esm/src/multer/storage/memory-storage.js +6 -4
- package/dist/esm/src/multer/storage/memory-storage.js.map +1 -1
- package/dist/esm/src/multer/storage/storage.d.ts +6 -5
- package/dist/esm/src/multer/utils/file.d.ts +6 -0
- package/dist/esm/src/multer/utils/file.js +54 -0
- package/dist/esm/src/multer/utils/file.js.map +1 -0
- package/dist/esm/src/multer/utils/index.d.ts +2 -0
- package/dist/esm/src/multer/utils/index.js +3 -0
- package/dist/esm/src/multer/utils/index.js.map +1 -0
- package/dist/esm/src/multer/utils/validators.d.ts +18 -0
- package/dist/esm/src/multer/utils/validators.js +167 -0
- package/dist/esm/src/multer/utils/validators.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +6 -4
- package/src/adapters/hono-adapter.ts +18 -3
- package/src/drivers/graphQLUpload/Upload.ts +21 -14
- package/src/drivers/graphQLUpload/fs-capacitor.ts +240 -116
- package/src/drivers/graphQLUpload/index.ts +37 -3
- package/src/drivers/graphQLUpload/processRequest.ts +92 -38
- package/src/drivers/graphQLUpload/storage/capacitor-storage.ts +86 -0
- package/src/drivers/graphQLUpload/storage/index.ts +3 -0
- package/src/drivers/graphQLUpload/storage/memory-storage.ts +62 -0
- package/src/drivers/graphQLUpload/storage/storage.ts +52 -0
- package/src/drivers/graphQLUpload/utils/file.ts +109 -0
- package/src/drivers/graphQLUpload/utils/index.ts +2 -0
- package/src/drivers/graphQLUpload/utils/validators.ts +219 -0
- package/src/multer/index.ts +1 -0
- package/src/multer/interceptors/any-files-interceptor.ts +12 -43
- package/src/multer/interceptors/base-interceptor.ts +54 -0
- package/src/multer/interceptors/file-fields-interceptor.ts +14 -48
- package/src/multer/interceptors/file-interceptor.ts +12 -44
- package/src/multer/interceptors/files-interceptor.ts +13 -45
- package/src/multer/interceptors/index.ts +1 -0
- package/src/multer/multipart/handlers/any-files.ts +14 -32
- package/src/multer/multipart/handlers/base-handler.ts +204 -0
- package/src/multer/multipart/handlers/file-fields.ts +29 -57
- package/src/multer/multipart/handlers/index.ts +11 -1
- package/src/multer/multipart/handlers/multiple-files.ts +23 -54
- package/src/multer/multipart/handlers/single-file.ts +14 -47
- package/src/multer/multipart/index.ts +1 -1
- package/src/multer/multipart/options.ts +26 -8
- package/src/multer/multipart/request.ts +19 -3
- package/src/multer/storage/disk-storage.ts +2 -1
- package/src/multer/storage/memory-storage.ts +13 -6
- package/src/multer/storage/storage.ts +12 -5
- package/src/multer/utils/file.ts +109 -0
- package/src/multer/utils/index.ts +2 -0
- package/src/multer/utils/validators.ts +219 -0
- package/test/README.md +247 -0
- package/test/graphql-upload.test.ts +509 -0
- package/test/helpers.ts +70 -0
- package/test/integration.test.ts +197 -0
- package/test/interceptors-e2e.test.ts +362 -0
- package/test/multipart-upload.test.ts +354 -0
- package/test/smoke.test.ts +227 -0
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { BadRequestException } from '@nestjs/common';
|
|
2
|
-
import { BodyData } from 'hono/utils/body';
|
|
3
2
|
|
|
4
3
|
import { StorageFile } from '../../storage/storage';
|
|
5
|
-
import { removeStorageFiles } from '../file';
|
|
6
|
-
import { filterUpload } from '../filter';
|
|
7
4
|
import { UploadOptions } from '../options';
|
|
8
|
-
import { THonoRequest
|
|
5
|
+
import { THonoRequest } from '../request';
|
|
6
|
+
import { FileHandler, FileFieldsResult } from './base-handler';
|
|
9
7
|
|
|
10
8
|
export interface UploadField {
|
|
11
9
|
name: string;
|
|
@@ -16,74 +14,48 @@ export type UploadFieldMapEntry = Required<Pick<UploadField, 'maxCount'>>;
|
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
16
|
* Converts an array of upload fields into a map for easy lookup.
|
|
19
|
-
* @param {UploadField[]} uploadFields - Array of upload field definitions.
|
|
20
|
-
* @returns {Map<string, UploadFieldMapEntry>} A map of field names to their max count settings.
|
|
21
17
|
*/
|
|
22
|
-
export const uploadFieldsToMap = (
|
|
18
|
+
export const uploadFieldsToMap = (
|
|
19
|
+
uploadFields: UploadField[],
|
|
20
|
+
): Map<string, UploadFieldMapEntry> =>
|
|
23
21
|
new Map(
|
|
24
22
|
uploadFields.map(({ name, ...opts }) => [name, { maxCount: 1, ...opts }]),
|
|
25
23
|
);
|
|
26
24
|
|
|
27
25
|
/**
|
|
28
26
|
* Handles multipart file fields by processing form-data parts.
|
|
29
|
-
* @param {THonoRequest} req - The incoming request object.
|
|
30
|
-
* @param {Map<string, UploadFieldMapEntry>} fieldsMap - A map of allowed upload fields.
|
|
31
|
-
* @param {UploadOptions} options - Upload options including storage handler.
|
|
32
|
-
* @returns {Promise<{ body: BodyData, files: Record<string, StorageFile[]>, remove: () => Promise<void> }>} The parsed body and files with a removal function.
|
|
33
27
|
*/
|
|
34
28
|
export const handleMultipartFileFields = async (
|
|
35
29
|
req: THonoRequest,
|
|
36
30
|
fieldsMap: Map<string, UploadFieldMapEntry>,
|
|
37
31
|
options: UploadOptions,
|
|
38
|
-
): Promise<{
|
|
39
|
-
|
|
40
|
-
files: Record<string, StorageFile[]>;
|
|
41
|
-
remove: () => Promise<void>;
|
|
42
|
-
}> => {
|
|
43
|
-
const parts = getParts(req, options);
|
|
44
|
-
const body: BodyData = {};
|
|
32
|
+
): Promise<FileFieldsResult> => {
|
|
33
|
+
const handler = new FileHandler(req, options);
|
|
45
34
|
const files: Record<string, StorageFile[]> = {};
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const removeFiles = async (error?: boolean): Promise<void> => {
|
|
53
|
-
const allFiles = Object.values(files).flat();
|
|
54
|
-
return removeStorageFiles(options.storage!, allFiles, error);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
for await (const [fieldName, part] of Object.entries(parts)) {
|
|
59
|
-
if (!(part instanceof File)) {
|
|
60
|
-
body[fieldName] = part;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const fieldOptions = fieldsMap.get(fieldName);
|
|
65
|
-
if (!fieldOptions) {
|
|
66
|
-
throw new BadRequestException(
|
|
67
|
-
`Field ${fieldName} doesn't accept files`,
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
files[fieldName] = files[fieldName] || [];
|
|
72
|
-
if (files[fieldName].length >= fieldOptions.maxCount) {
|
|
73
|
-
throw new BadRequestException(
|
|
74
|
-
`Field ${fieldName} accepts max ${fieldOptions.maxCount} files`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
36
|
+
await handler.process(async (fieldName, part) => {
|
|
37
|
+
const fieldOptions = fieldsMap.get(fieldName);
|
|
38
|
+
if (!fieldOptions) {
|
|
39
|
+
throw new BadRequestException(`Field ${fieldName} doesn't accept files`);
|
|
40
|
+
}
|
|
77
41
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
42
|
+
files[fieldName] = files[fieldName] || [];
|
|
43
|
+
handler.validateMaxCount(
|
|
44
|
+
fieldName,
|
|
45
|
+
files[fieldName].length,
|
|
46
|
+
fieldOptions.maxCount,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const storageFile = await handler.handleSingleFile(fieldName, part);
|
|
50
|
+
if (storageFile) {
|
|
51
|
+
files[fieldName].push(storageFile);
|
|
52
|
+
handler.addFile(fieldName, storageFile);
|
|
82
53
|
}
|
|
83
|
-
}
|
|
84
|
-
await removeFiles(true);
|
|
85
|
-
throw error;
|
|
86
|
-
}
|
|
54
|
+
});
|
|
87
55
|
|
|
88
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
body: handler.getBody(),
|
|
58
|
+
files,
|
|
59
|
+
remove: handler.createRemoveFunction(),
|
|
60
|
+
};
|
|
89
61
|
};
|
|
@@ -1 +1,11 @@
|
|
|
1
|
-
export { UploadField } from './file-fields';
|
|
1
|
+
export { UploadField, uploadFieldsToMap } from './file-fields';
|
|
2
|
+
export { handleMultipartSingleFile } from './single-file';
|
|
3
|
+
export { handleMultipartMultipleFiles } from './multiple-files';
|
|
4
|
+
export { handleMultipartFileFields } from './file-fields';
|
|
5
|
+
export { handleMultipartAnyFiles } from './any-files';
|
|
6
|
+
export {
|
|
7
|
+
FileHandler,
|
|
8
|
+
SingleFileResult,
|
|
9
|
+
MultipleFilesResult,
|
|
10
|
+
FileFieldsResult,
|
|
11
|
+
} from './base-handler';
|
|
@@ -1,70 +1,39 @@
|
|
|
1
1
|
import { BadRequestException } from '@nestjs/common';
|
|
2
|
-
import { BodyData } from 'hono/utils/body';
|
|
3
2
|
|
|
4
|
-
import { StorageFile } from '../../storage';
|
|
5
|
-
import { removeStorageFiles } from '../file';
|
|
6
|
-
import { filterUpload } from '../filter';
|
|
3
|
+
import { StorageFile } from '../../storage/storage';
|
|
7
4
|
import { UploadOptions } from '../options';
|
|
8
|
-
import { THonoRequest
|
|
5
|
+
import { THonoRequest } from '../request';
|
|
6
|
+
import { FileHandler, MultipleFilesResult } from './base-handler';
|
|
9
7
|
|
|
10
8
|
export const handleMultipartMultipleFiles = async (
|
|
11
9
|
req: THonoRequest,
|
|
12
10
|
fieldname: string,
|
|
13
11
|
maxCount: number,
|
|
14
12
|
options: UploadOptions,
|
|
15
|
-
) => {
|
|
16
|
-
const
|
|
17
|
-
const body: BodyData = {};
|
|
18
|
-
|
|
13
|
+
): Promise<MultipleFilesResult> => {
|
|
14
|
+
const handler = new FileHandler(req, options);
|
|
19
15
|
const files: StorageFile[] = [];
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (!(part instanceof File || Array.isArray(part))) {
|
|
28
|
-
body[partFieldName] = part;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const partArray = Array.isArray(part) ? part : [part];
|
|
33
|
-
|
|
34
|
-
for (const singlePart of partArray) {
|
|
35
|
-
if (!(singlePart instanceof File)) {
|
|
36
|
-
throw new BadRequestException(
|
|
37
|
-
`Field ${partFieldName} contains invalid file data`,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (partFieldName !== fieldname) {
|
|
42
|
-
throw new BadRequestException(
|
|
43
|
-
`Field ${partFieldName} doesn't accept files`,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (files.length >= maxCount) {
|
|
48
|
-
throw new BadRequestException(
|
|
49
|
-
`Field ${partFieldName} accepts max ${maxCount} files`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
17
|
+
await handler.process(async (fieldName, part) => {
|
|
18
|
+
if (!(part instanceof File)) {
|
|
19
|
+
throw new BadRequestException(
|
|
20
|
+
`Field ${fieldName} contains invalid file data`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
52
23
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
req,
|
|
56
|
-
partFieldName,
|
|
57
|
-
);
|
|
24
|
+
handler.validateFieldName(fieldName, fieldname);
|
|
25
|
+
handler.validateMaxCount(fieldName, files.length, maxCount);
|
|
58
26
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
27
|
+
const storageFile = await handler.handleSingleFile(fieldName, part);
|
|
28
|
+
if (storageFile) {
|
|
29
|
+
files.push(storageFile);
|
|
30
|
+
handler.addFile(fieldName, storageFile);
|
|
63
31
|
}
|
|
64
|
-
}
|
|
65
|
-
await removeFiles(error);
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
32
|
+
});
|
|
68
33
|
|
|
69
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
body: handler.getBody(),
|
|
36
|
+
files,
|
|
37
|
+
remove: handler.createRemoveFunction(),
|
|
38
|
+
};
|
|
70
39
|
};
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BodyData } from 'hono/utils/body';
|
|
3
|
-
|
|
4
|
-
import { StorageFile } from '../../storage';
|
|
5
|
-
import { filterUpload } from '../filter';
|
|
1
|
+
import { StorageFile } from '../../storage/storage';
|
|
6
2
|
import { UploadOptions } from '../options';
|
|
7
|
-
import { THonoRequest
|
|
3
|
+
import { THonoRequest } from '../request';
|
|
4
|
+
import { FileHandler, SingleFileResult } from './base-handler';
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* Handles a single file upload in a multipart request.
|
|
@@ -17,53 +14,23 @@ export const handleMultipartSingleFile = async (
|
|
|
17
14
|
req: THonoRequest,
|
|
18
15
|
fieldname: string,
|
|
19
16
|
options: UploadOptions,
|
|
20
|
-
) => {
|
|
21
|
-
const
|
|
22
|
-
const body: BodyData = {};
|
|
17
|
+
): Promise<SingleFileResult> => {
|
|
18
|
+
const handler = new FileHandler(req, options);
|
|
23
19
|
let file: StorageFile | undefined;
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*/
|
|
29
|
-
const removeFiles = async (error?: boolean) => {
|
|
30
|
-
if (!file) return;
|
|
31
|
-
await options.storage!.removeFile(file, error);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
for await (const [partFieldName, part] of Object.entries(parts)) {
|
|
36
|
-
if (!(part instanceof File)) {
|
|
37
|
-
body[partFieldName] = part;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (partFieldName !== fieldname) {
|
|
42
|
-
throw new BadRequestException(
|
|
43
|
-
`Field "${partFieldName}" doesn't accept file.`,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (file) {
|
|
48
|
-
throw new BadRequestException(
|
|
49
|
-
`Field "${fieldname}" accepts only one file.`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const _file = await options.storage!.handleFile(part, req, partFieldName);
|
|
21
|
+
await handler.process(async (fieldName, part) => {
|
|
22
|
+
handler.validateFieldName(fieldName, fieldname);
|
|
23
|
+
handler.validateSingleFile(file);
|
|
54
24
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
25
|
+
const storageFile = await handler.handleSingleFile(fieldName, part);
|
|
26
|
+
if (storageFile) {
|
|
27
|
+
file = storageFile;
|
|
58
28
|
}
|
|
59
|
-
}
|
|
60
|
-
await removeFiles(true);
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
29
|
+
});
|
|
63
30
|
|
|
64
31
|
return {
|
|
65
|
-
body,
|
|
32
|
+
body: handler.getBody(),
|
|
66
33
|
file,
|
|
67
|
-
remove: ()
|
|
34
|
+
remove: handler.createRemoveFunction(),
|
|
68
35
|
};
|
|
69
36
|
};
|
|
@@ -1,20 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { DiskStorage, MemoryStorage, Storage } from '../storage';
|
|
1
|
+
import { DiskStorage, MemoryStorage, Storage, StorageFile } from '../storage';
|
|
4
2
|
import { UploadFilterHandler } from './filter';
|
|
5
3
|
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Upload limits configuration
|
|
6
|
+
*/
|
|
7
|
+
export interface UploadLimits {
|
|
8
|
+
/** Maximum file size in bytes */
|
|
9
|
+
fileSize?: number;
|
|
10
|
+
/** Maximum number of files for a field */
|
|
11
|
+
files?: number;
|
|
12
|
+
/** Maximum number of fields (for file fields upload) */
|
|
13
|
+
fields?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* File upload options
|
|
18
|
+
*/
|
|
19
|
+
export type UploadOptions = {
|
|
20
|
+
/** Destination directory for disk storage */
|
|
7
21
|
dest?: string;
|
|
8
|
-
|
|
22
|
+
/** Storage implementation */
|
|
23
|
+
storage?: Storage<StorageFile>;
|
|
24
|
+
/** File filter function */
|
|
9
25
|
filter?: UploadFilterHandler;
|
|
26
|
+
/** Upload limits */
|
|
27
|
+
limits?: UploadLimits;
|
|
10
28
|
};
|
|
11
29
|
|
|
12
30
|
export const DEFAULT_UPLOAD_OPTIONS: Partial<UploadOptions> = {
|
|
13
31
|
storage: new MemoryStorage(),
|
|
14
32
|
};
|
|
15
33
|
|
|
16
|
-
export const transformUploadOptions = (opts?: UploadOptions) => {
|
|
17
|
-
if (opts == null) return DEFAULT_UPLOAD_OPTIONS;
|
|
34
|
+
export const transformUploadOptions = (opts?: UploadOptions): UploadOptions => {
|
|
35
|
+
if (opts == null) return DEFAULT_UPLOAD_OPTIONS as UploadOptions;
|
|
18
36
|
|
|
19
37
|
if (opts.dest != null) {
|
|
20
38
|
return {
|
|
@@ -26,5 +44,5 @@ export const transformUploadOptions = (opts?: UploadOptions) => {
|
|
|
26
44
|
};
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
return { ...DEFAULT_UPLOAD_OPTIONS, ...opts };
|
|
47
|
+
return { ...DEFAULT_UPLOAD_OPTIONS, ...opts } as UploadOptions;
|
|
30
48
|
};
|
|
@@ -36,10 +36,26 @@ export const getParts = (
|
|
|
36
36
|
): BodyData => {
|
|
37
37
|
const parts = req.body ?? {};
|
|
38
38
|
|
|
39
|
-
for (const [key,
|
|
40
|
-
|
|
39
|
+
for (const [key, value] of Object.entries(parts)) {
|
|
40
|
+
// Handle array of files
|
|
41
|
+
if (Array.isArray(value) && value.every((item) => item instanceof File)) {
|
|
41
42
|
const maxSize = options?.limits?.fileSize;
|
|
42
|
-
if (maxSize
|
|
43
|
+
if (maxSize) {
|
|
44
|
+
for (const file of value) {
|
|
45
|
+
if (file.size > maxSize) {
|
|
46
|
+
throw new BadRequestException(
|
|
47
|
+
`File "${key}" is too large. Maximum size is ${maxSize} bytes.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle single file
|
|
56
|
+
if (value instanceof File) {
|
|
57
|
+
const maxSize = options?.limits?.fileSize;
|
|
58
|
+
if (maxSize && value.size > maxSize) {
|
|
43
59
|
throw new BadRequestException(
|
|
44
60
|
`File "${key}" is too large. Maximum size is ${maxSize} bytes.`,
|
|
45
61
|
);
|
|
@@ -8,26 +8,33 @@ export interface MemoryStorageFile extends StorageFile {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export class MemoryStorage implements Storage<MemoryStorageFile> {
|
|
11
|
-
public async handleFile(
|
|
11
|
+
public async handleFile(
|
|
12
|
+
file: File,
|
|
13
|
+
_req: HonoRequest,
|
|
14
|
+
fieldName: string,
|
|
15
|
+
): Promise<MemoryStorageFile> {
|
|
12
16
|
const buffer = await file
|
|
13
17
|
.stream()
|
|
14
18
|
.pipeTo(new WritableStream())
|
|
15
19
|
.then(() => file.arrayBuffer())
|
|
16
|
-
.then((
|
|
20
|
+
.then((buf) => Buffer.from(buf));
|
|
17
21
|
|
|
18
22
|
return {
|
|
19
23
|
buffer,
|
|
20
24
|
size: buffer.length,
|
|
21
25
|
encoding: 'utf-8',
|
|
22
26
|
mimetype: file.type,
|
|
23
|
-
|
|
27
|
+
fieldName: fieldName,
|
|
24
28
|
originalFilename: file.name,
|
|
29
|
+
uploadedAt: new Date().toISOString(),
|
|
25
30
|
stream: () => file.stream(),
|
|
26
|
-
lastModified: file.lastModified,
|
|
27
31
|
};
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
public async removeFile(file:
|
|
31
|
-
|
|
34
|
+
public async removeFile(file: StorageFile): Promise<void> {
|
|
35
|
+
// Check if it's a MemoryStorageFile before deleting buffer
|
|
36
|
+
if ('buffer' in file) {
|
|
37
|
+
delete (file as MemoryStorageFile).buffer;
|
|
38
|
+
}
|
|
32
39
|
}
|
|
33
40
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { HonoRequest } from 'hono';
|
|
2
2
|
|
|
3
3
|
export interface StorageFile {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
mimetype: string;
|
|
4
|
+
/** Field name in the multipart form */
|
|
5
|
+
fieldName: string;
|
|
6
|
+
/** Original filename provided by client */
|
|
8
7
|
originalFilename: string;
|
|
8
|
+
/** MIME type of the file */
|
|
9
|
+
mimetype: string;
|
|
10
|
+
/** Encoding type (e.g., '7bit', '8bit', 'binary') */
|
|
11
|
+
encoding: string;
|
|
12
|
+
/** Size of the file in bytes */
|
|
13
|
+
size: number;
|
|
14
|
+
/** Timestamp when file was uploaded (ISO 8601) */
|
|
15
|
+
uploadedAt?: string;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
19
|
export interface Storage<T extends StorageFile = StorageFile, K = any> {
|
|
13
20
|
handleFile: (file: File, req: HonoRequest, fieldName: string) => Promise<T>;
|
|
14
|
-
removeFile: (file: T, force?: boolean) => Promise<void> | void;
|
|
21
|
+
removeFile: (file: T | StorageFile, force?: boolean) => Promise<void> | void;
|
|
15
22
|
options?: K;
|
|
16
23
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File utilities for upload handling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format bytes to human-readable size
|
|
7
|
+
* @param bytes - Size in bytes
|
|
8
|
+
* @returns Formatted size string (e.g., "1.5 MB")
|
|
9
|
+
*/
|
|
10
|
+
export function formatBytes(bytes: number): string {
|
|
11
|
+
if (bytes === 0) return '0 Bytes';
|
|
12
|
+
|
|
13
|
+
const k = 1024;
|
|
14
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
15
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
16
|
+
|
|
17
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate file size against limit
|
|
22
|
+
* @param file - File to validate
|
|
23
|
+
* @param maxSize - Maximum size in bytes
|
|
24
|
+
* @returns true if file size is within limit
|
|
25
|
+
* @throws Error if file exceeds limit
|
|
26
|
+
*/
|
|
27
|
+
export function validateFileSize(file: File, maxSize: number): void {
|
|
28
|
+
if (file.size > maxSize) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`File "${file.name}" (${formatBytes(file.size)}) exceeds maximum size of ${formatBytes(maxSize)}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get file extension from filename
|
|
37
|
+
* @param filename - Name of the file
|
|
38
|
+
* @returns File extension without dot, or empty string
|
|
39
|
+
*/
|
|
40
|
+
export function getFileExtension(filename: string): string {
|
|
41
|
+
const ext = filename.lastIndexOf('.');
|
|
42
|
+
return ext === -1 ? '' : filename.slice(ext + 1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if file is of expected type
|
|
47
|
+
* @param file - File to check
|
|
48
|
+
* @param allowedTypes - Array of allowed MIME types or extensions
|
|
49
|
+
* @returns true if file type is allowed
|
|
50
|
+
*/
|
|
51
|
+
export function isAllowedFileType(file: File, allowedTypes: string[]): boolean {
|
|
52
|
+
return allowedTypes.some((type) => {
|
|
53
|
+
if (type.startsWith('.')) {
|
|
54
|
+
// Extension check
|
|
55
|
+
return getFileExtension(file.name).toLowerCase() === type.slice(1);
|
|
56
|
+
}
|
|
57
|
+
// MIME type check (supports wildcards like 'image/*')
|
|
58
|
+
if (type.endsWith('/*')) {
|
|
59
|
+
const prefix = type.slice(0, -2);
|
|
60
|
+
return file.type.startsWith(prefix);
|
|
61
|
+
}
|
|
62
|
+
return file.type === type;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate a safe filename from user input
|
|
68
|
+
* @param filename - Original filename
|
|
69
|
+
* @returns Safe filename without special characters
|
|
70
|
+
*/
|
|
71
|
+
export function sanitizeFilename(filename: string): string {
|
|
72
|
+
return filename
|
|
73
|
+
.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
74
|
+
.replace(/_{2,}/g, '_')
|
|
75
|
+
.replace(/^\.+|\.+$/g, '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generate a unique filename if file already exists
|
|
80
|
+
* @param filename - Original filename
|
|
81
|
+
* @param existingFilenames - Set of existing filenames
|
|
82
|
+
* @returns Unique filename
|
|
83
|
+
*/
|
|
84
|
+
export function getUniqueFilename(
|
|
85
|
+
filename: string,
|
|
86
|
+
existingFilenames?: Set<string>,
|
|
87
|
+
): string {
|
|
88
|
+
if (!existingFilenames?.has(filename)) {
|
|
89
|
+
return filename;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const name = filename.lastIndexOf('.');
|
|
93
|
+
if (name === -1) {
|
|
94
|
+
return `${filename}_1`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const baseName = filename.slice(0, name);
|
|
98
|
+
const ext = filename.slice(name);
|
|
99
|
+
|
|
100
|
+
let counter = 1;
|
|
101
|
+
let newFilename = `${baseName}_${counter}${ext}`;
|
|
102
|
+
|
|
103
|
+
while (existingFilenames.has(newFilename)) {
|
|
104
|
+
counter++;
|
|
105
|
+
newFilename = `${baseName}_${counter}${ext}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return newFilename;
|
|
109
|
+
}
|