@bernierllc/file-uploader 0.3.5 → 0.6.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/dist/errors.d.ts +11 -0
- package/dist/errors.js +20 -0
- package/dist/file-uploader.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/package.json +7 -2
- package/.eslintrc.cjs +0 -33
- package/CHANGELOG.md +0 -97
- package/__tests__/file-uploader.test.ts +0 -517
- package/jest.config.cjs +0 -33
- package/src/file-uploader.ts +0 -198
- package/src/index.ts +0 -16
- package/src/types.ts +0 -73
- package/tsconfig.json +0 -24
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type FileUploaderErrorCode = 'FILE_UPLOADER_ERROR' | 'INVALID_INPUT' | 'OPERATION_FAILED';
|
|
2
|
+
export interface FileUploaderErrorOptions {
|
|
3
|
+
cause?: Error;
|
|
4
|
+
code?: FileUploaderErrorCode;
|
|
5
|
+
context?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export declare class FileUploaderError extends Error {
|
|
8
|
+
readonly code: FileUploaderErrorCode;
|
|
9
|
+
readonly context?: Record<string, unknown>;
|
|
10
|
+
constructor(message: string, options?: FileUploaderErrorOptions);
|
|
11
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code only within the scope of the project it was delivered for.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
export class FileUploaderError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
context;
|
|
11
|
+
constructor(message, options) {
|
|
12
|
+
super(message, options?.cause ? { cause: options.cause } : undefined);
|
|
13
|
+
this.name = 'FileUploaderError';
|
|
14
|
+
this.code = options?.code ?? 'FILE_UPLOADER_ERROR';
|
|
15
|
+
if (options?.context !== undefined) {
|
|
16
|
+
this.context = options.context;
|
|
17
|
+
}
|
|
18
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/file-uploader.js
CHANGED
|
@@ -13,8 +13,9 @@ const logger = createLogger();
|
|
|
13
13
|
* Multipart file upload handler
|
|
14
14
|
*/
|
|
15
15
|
export class FileUploader {
|
|
16
|
+
config;
|
|
17
|
+
progressMap = new Map();
|
|
16
18
|
constructor(config = {}) {
|
|
17
|
-
this.progressMap = new Map();
|
|
18
19
|
this.config = {
|
|
19
20
|
maxFileSize: config.maxFileSize ?? 10 * 1024 * 1024, // 10MB default
|
|
20
21
|
maxFiles: config.maxFiles ?? 10,
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -6,3 +6,5 @@ The client may use and modify this code *only within the scope of the project it
|
|
|
6
6
|
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
7
|
*/
|
|
8
8
|
export { FileUploader } from './file-uploader.js';
|
|
9
|
+
// Error types
|
|
10
|
+
export { FileUploaderError } from './errors.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bernierllc/file-uploader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Multipart file upload handler with validation and progress tracking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/**/*",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
14
19
|
"keywords": [
|
|
15
20
|
"upload",
|
|
16
21
|
"multipart",
|
|
@@ -23,7 +28,7 @@
|
|
|
23
28
|
"dependencies": {
|
|
24
29
|
"busboy": "^1.6.0",
|
|
25
30
|
"uuid": "^9.0.0",
|
|
26
|
-
"@bernierllc/logger": "1.
|
|
31
|
+
"@bernierllc/logger": "1.7.0"
|
|
27
32
|
},
|
|
28
33
|
"devDependencies": {
|
|
29
34
|
"@types/busboy": "^1.5.0",
|
package/.eslintrc.cjs
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
module.exports = {
|
|
10
|
-
parser: '@typescript-eslint/parser',
|
|
11
|
-
extends: [
|
|
12
|
-
'eslint:recommended',
|
|
13
|
-
'plugin:@typescript-eslint/recommended'
|
|
14
|
-
],
|
|
15
|
-
plugins: ['@typescript-eslint'],
|
|
16
|
-
parserOptions: {
|
|
17
|
-
ecmaVersion: 2020,
|
|
18
|
-
sourceType: 'module'
|
|
19
|
-
},
|
|
20
|
-
env: {
|
|
21
|
-
node: true,
|
|
22
|
-
es6: true,
|
|
23
|
-
jest: true
|
|
24
|
-
},
|
|
25
|
-
rules: {
|
|
26
|
-
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
27
|
-
'@typescript-eslint/no-explicit-any': 'error',
|
|
28
|
-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
29
|
-
'@typescript-eslint/no-non-null-assertion': 'warn'
|
|
30
|
-
},
|
|
31
|
-
ignorePatterns: ['dist/', 'node_modules/', '*.cjs']
|
|
32
|
-
};
|
|
33
|
-
|
package/CHANGELOG.md
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# Change Log
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
-
|
|
6
|
-
## [0.3.5](https://github.com/bernierllc/tools/compare/@bernierllc/file-uploader@0.3.4...@bernierllc/file-uploader@0.3.5) (2026-05-19)
|
|
7
|
-
|
|
8
|
-
**Note:** Version bump only for package @bernierllc/file-uploader
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## [0.3.4](https://github.com/bernierllc/tools/compare/@bernierllc/file-uploader@0.3.3...@bernierllc/file-uploader@0.3.4) (2026-05-19)
|
|
15
|
-
|
|
16
|
-
**Note:** Version bump only for package @bernierllc/file-uploader
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## [0.3.3](https://github.com/bernierllc/tools/compare/@bernierllc/file-uploader@0.3.2...@bernierllc/file-uploader@0.3.3) (2026-03-30)
|
|
23
|
-
|
|
24
|
-
**Note:** Version bump only for package @bernierllc/file-uploader
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## [0.3.2](https://github.com/bernierllc/tools/compare/@bernierllc/file-uploader@0.3.1...@bernierllc/file-uploader@0.3.2) (2026-03-09)
|
|
31
|
-
|
|
32
|
-
**Note:** Version bump only for package @bernierllc/file-uploader
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
## [0.3.1](https://github.com/bernierllc/tools/compare/@bernierllc/file-uploader@0.3.0...@bernierllc/file-uploader@0.3.1) (2026-03-03)
|
|
39
|
-
|
|
40
|
-
**Note:** Version bump only for package @bernierllc/file-uploader
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# 0.3.0 (2025-12-25)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
### Bug Fixes
|
|
50
|
-
|
|
51
|
-
* **ci:** add lerna version step before publish ([3d20300](https://github.com/bernierllc/tools/commit/3d203002143bf353fffafe4f8a78a99009567347))
|
|
52
|
-
* **ci:** fix release workflow + convert internal deps to workspace:* ([01c078b](https://github.com/bernierllc/tools/commit/01c078b49d6025f7eef750f79207a1c71c8d85dc))
|
|
53
|
-
* update neverhub-adapter deps to workspace:* and publish 0.1.2 ([f0e3d04](https://github.com/bernierllc/tools/commit/f0e3d04d8d4f094e3bb899ddf81e93243d16e2c2))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
### Features
|
|
57
|
-
|
|
58
|
-
* add 4 new core packages ([3726d4c](https://github.com/bernierllc/tools/commit/3726d4cc31d60c7c6d65edb526e7e4835287d426))
|
|
59
|
-
* enhance core and suite packages (batch 11) ([1f7ef67](https://github.com/bernierllc/tools/commit/1f7ef674796994d39e9117d01cb26b55cf9f25ae))
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# 0.2.0 (2025-12-25)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
### Bug Fixes
|
|
69
|
-
|
|
70
|
-
* **ci:** fix release workflow + convert internal deps to workspace:* ([01c078b](https://github.com/bernierllc/tools/commit/01c078b49d6025f7eef750f79207a1c71c8d85dc))
|
|
71
|
-
* update neverhub-adapter deps to workspace:* and publish 0.1.2 ([f0e3d04](https://github.com/bernierllc/tools/commit/f0e3d04d8d4f094e3bb899ddf81e93243d16e2c2))
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
### Features
|
|
75
|
-
|
|
76
|
-
* add 4 new core packages ([3726d4c](https://github.com/bernierllc/tools/commit/3726d4cc31d60c7c6d65edb526e7e4835287d426))
|
|
77
|
-
* enhance core and suite packages (batch 11) ([1f7ef67](https://github.com/bernierllc/tools/commit/1f7ef674796994d39e9117d01cb26b55cf9f25ae))
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# @bernierllc/file-uploader
|
|
84
|
-
|
|
85
|
-
## 0.1.2
|
|
86
|
-
|
|
87
|
-
### Patch Changes
|
|
88
|
-
|
|
89
|
-
- Updated dependencies [[`73c1d1e`](https://github.com/bernierllc/tools/commit/73c1d1e46114cf2ccfd5b74b83743361247f4502)]:
|
|
90
|
-
- @bernierllc/logger@1.1.0
|
|
91
|
-
|
|
92
|
-
## 0.1.1
|
|
93
|
-
|
|
94
|
-
### Patch Changes
|
|
95
|
-
|
|
96
|
-
- Updated dependencies [[`24899fa`](https://github.com/bernierllc/tools/commit/24899fa4eaf80ae329f7bd74483cf97a132f5d4c)]:
|
|
97
|
-
- @bernierllc/logger@1.0.4
|
|
@@ -1,517 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { FileUploader } from '../src';
|
|
10
|
-
import { IncomingMessage } from 'http';
|
|
11
|
-
import { Readable } from 'stream';
|
|
12
|
-
import type { FileInfo } from '../src/types';
|
|
13
|
-
|
|
14
|
-
// Mock @bernierllc/logger
|
|
15
|
-
jest.mock('@bernierllc/logger', () => ({
|
|
16
|
-
createLogger: jest.fn(() => ({
|
|
17
|
-
debug: jest.fn(),
|
|
18
|
-
info: jest.fn(),
|
|
19
|
-
warn: jest.fn(),
|
|
20
|
-
error: jest.fn()
|
|
21
|
-
}))
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Create a mock multipart request
|
|
26
|
-
*/
|
|
27
|
-
function createMockRequest(parts: { fieldname: string; filename: string; mimeType: string; data: Buffer }[]): IncomingMessage {
|
|
28
|
-
const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW';
|
|
29
|
-
|
|
30
|
-
let body = '';
|
|
31
|
-
for (const part of parts) {
|
|
32
|
-
body += `--${boundary}\r\n`;
|
|
33
|
-
body += `Content-Disposition: form-data; name="${part.fieldname}"; filename="${part.filename}"\r\n`;
|
|
34
|
-
body += `Content-Type: ${part.mimeType}\r\n\r\n`;
|
|
35
|
-
body += part.data.toString('binary');
|
|
36
|
-
body += '\r\n';
|
|
37
|
-
}
|
|
38
|
-
body += `--${boundary}--\r\n`;
|
|
39
|
-
|
|
40
|
-
const stream = new Readable();
|
|
41
|
-
stream.push(Buffer.from(body, 'binary'));
|
|
42
|
-
stream.push(null);
|
|
43
|
-
|
|
44
|
-
const req = stream as IncomingMessage;
|
|
45
|
-
req.headers = {
|
|
46
|
-
'content-type': `multipart/form-data; boundary=${boundary}`
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
return req;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe('FileUploader', () => {
|
|
53
|
-
let uploader: FileUploader;
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
uploader = new FileUploader({
|
|
57
|
-
maxFileSize: 1024 * 1024, // 1MB
|
|
58
|
-
allowedMimeTypes: ['image/jpeg', 'image/png']
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('constructor', () => {
|
|
63
|
-
it('should create a file uploader with default config', () => {
|
|
64
|
-
const defaultUploader = new FileUploader();
|
|
65
|
-
expect(defaultUploader).toBeInstanceOf(FileUploader);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should create a file uploader with custom config', () => {
|
|
69
|
-
expect(uploader).toBeInstanceOf(FileUploader);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should accept destination as string', () => {
|
|
73
|
-
const uploaderWithDestination = new FileUploader({
|
|
74
|
-
destination: '/custom/path'
|
|
75
|
-
});
|
|
76
|
-
expect(uploaderWithDestination).toBeInstanceOf(FileUploader);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should accept destination as function', () => {
|
|
80
|
-
const uploaderWithDestination = new FileUploader({
|
|
81
|
-
destination: async (file: FileInfo) => `/custom/${file.filename}`
|
|
82
|
-
});
|
|
83
|
-
expect(uploaderWithDestination).toBeInstanceOf(FileUploader);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should accept onProgress callback', () => {
|
|
87
|
-
const onProgress = jest.fn();
|
|
88
|
-
const uploaderWithProgress = new FileUploader({
|
|
89
|
-
onProgress
|
|
90
|
-
});
|
|
91
|
-
expect(uploaderWithProgress).toBeInstanceOf(FileUploader);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe('validateFile', () => {
|
|
96
|
-
it('should validate file with allowed MIME type', () => {
|
|
97
|
-
const fileInfo: FileInfo = {
|
|
98
|
-
fieldname: 'file',
|
|
99
|
-
filename: 'test.jpg',
|
|
100
|
-
encoding: '7bit',
|
|
101
|
-
mimeType: 'image/jpeg',
|
|
102
|
-
size: 1000
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const result = uploader.validateFile(fileInfo);
|
|
106
|
-
expect(result.valid).toBe(true);
|
|
107
|
-
expect(result.error).toBeUndefined();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should reject invalid MIME types', () => {
|
|
111
|
-
const fileInfo: FileInfo = {
|
|
112
|
-
fieldname: 'file',
|
|
113
|
-
filename: 'test.exe',
|
|
114
|
-
encoding: '7bit',
|
|
115
|
-
mimeType: 'application/x-msdownload',
|
|
116
|
-
size: 1000
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const result = uploader.validateFile(fileInfo);
|
|
120
|
-
expect(result.valid).toBe(false);
|
|
121
|
-
expect(result.error).toContain('MIME type');
|
|
122
|
-
expect(result.error).toContain('application/x-msdownload');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should reject files exceeding size limit', () => {
|
|
126
|
-
const fileInfo: FileInfo = {
|
|
127
|
-
fieldname: 'file',
|
|
128
|
-
filename: 'test.jpg',
|
|
129
|
-
encoding: '7bit',
|
|
130
|
-
mimeType: 'image/jpeg',
|
|
131
|
-
size: 2 * 1024 * 1024 // 2MB
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const result = uploader.validateFile(fileInfo);
|
|
135
|
-
expect(result.valid).toBe(false);
|
|
136
|
-
expect(result.error).toContain('File size');
|
|
137
|
-
expect(result.error).toContain('exceeds maximum');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should accept files when no MIME type restrictions', () => {
|
|
141
|
-
const noMimeUploader = new FileUploader({
|
|
142
|
-
maxFileSize: 1024 * 1024
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const fileInfo: FileInfo = {
|
|
146
|
-
fieldname: 'file',
|
|
147
|
-
filename: 'test.exe',
|
|
148
|
-
encoding: '7bit',
|
|
149
|
-
mimeType: 'application/x-msdownload',
|
|
150
|
-
size: 1000
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const result = noMimeUploader.validateFile(fileInfo);
|
|
154
|
-
expect(result.valid).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should accept files at size limit boundary', () => {
|
|
158
|
-
const fileInfo: FileInfo = {
|
|
159
|
-
fieldname: 'file',
|
|
160
|
-
filename: 'test.jpg',
|
|
161
|
-
encoding: '7bit',
|
|
162
|
-
mimeType: 'image/jpeg',
|
|
163
|
-
size: 1024 * 1024 // Exactly 1MB
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const result = uploader.validateFile(fileInfo);
|
|
167
|
-
expect(result.valid).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe('handleMultipart', () => {
|
|
172
|
-
it('should handle single file upload', async () => {
|
|
173
|
-
const mockData = Buffer.from('test file content');
|
|
174
|
-
const req = createMockRequest([
|
|
175
|
-
{
|
|
176
|
-
fieldname: 'file',
|
|
177
|
-
filename: 'test.jpg',
|
|
178
|
-
mimeType: 'image/jpeg',
|
|
179
|
-
data: mockData
|
|
180
|
-
}
|
|
181
|
-
]);
|
|
182
|
-
|
|
183
|
-
const results = await uploader.handleMultipart(req);
|
|
184
|
-
|
|
185
|
-
expect(results).toHaveLength(1);
|
|
186
|
-
expect(results[0].filename).toBe('test.jpg');
|
|
187
|
-
expect(results[0].mimeType).toBe('image/jpeg');
|
|
188
|
-
expect(results[0].size).toBe(mockData.length);
|
|
189
|
-
expect(results[0].fileId).toBeDefined();
|
|
190
|
-
expect(results[0].destination).toContain('test.jpg');
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('should handle multiple file uploads', async () => {
|
|
194
|
-
const mockData1 = Buffer.from('file 1 content');
|
|
195
|
-
const mockData2 = Buffer.from('file 2 content');
|
|
196
|
-
|
|
197
|
-
const req = createMockRequest([
|
|
198
|
-
{
|
|
199
|
-
fieldname: 'file1',
|
|
200
|
-
filename: 'test1.jpg',
|
|
201
|
-
mimeType: 'image/jpeg',
|
|
202
|
-
data: mockData1
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
fieldname: 'file2',
|
|
206
|
-
filename: 'test2.png',
|
|
207
|
-
mimeType: 'image/png',
|
|
208
|
-
data: mockData2
|
|
209
|
-
}
|
|
210
|
-
]);
|
|
211
|
-
|
|
212
|
-
const results = await uploader.handleMultipart(req);
|
|
213
|
-
|
|
214
|
-
expect(results).toHaveLength(2);
|
|
215
|
-
expect(results[0].filename).toBe('test1.jpg');
|
|
216
|
-
expect(results[1].filename).toBe('test2.png');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should reject when file count exceeds limit', async () => {
|
|
220
|
-
const uploaderWithLimit = new FileUploader({
|
|
221
|
-
maxFiles: 1,
|
|
222
|
-
allowedMimeTypes: ['image/jpeg', 'image/png']
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const mockData1 = Buffer.from('file 1');
|
|
226
|
-
const mockData2 = Buffer.from('file 2');
|
|
227
|
-
|
|
228
|
-
const req = createMockRequest([
|
|
229
|
-
{
|
|
230
|
-
fieldname: 'file1',
|
|
231
|
-
filename: 'test1.jpg',
|
|
232
|
-
mimeType: 'image/jpeg',
|
|
233
|
-
data: mockData1
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
fieldname: 'file2',
|
|
237
|
-
filename: 'test2.png',
|
|
238
|
-
mimeType: 'image/png',
|
|
239
|
-
data: mockData2
|
|
240
|
-
}
|
|
241
|
-
]);
|
|
242
|
-
|
|
243
|
-
await expect(uploaderWithLimit.handleMultipart(req)).rejects.toThrow('Maximum file count exceeded');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('should reject files with invalid MIME type during upload', async () => {
|
|
247
|
-
const mockData = Buffer.from('malicious content');
|
|
248
|
-
const req = createMockRequest([
|
|
249
|
-
{
|
|
250
|
-
fieldname: 'file',
|
|
251
|
-
filename: 'malware.exe',
|
|
252
|
-
mimeType: 'application/x-msdownload',
|
|
253
|
-
data: mockData
|
|
254
|
-
}
|
|
255
|
-
]);
|
|
256
|
-
|
|
257
|
-
await expect(uploader.handleMultipart(req)).rejects.toThrow('MIME type');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('should reject files exceeding size during upload', async () => {
|
|
261
|
-
const mockData = Buffer.alloc(2 * 1024 * 1024); // 2MB
|
|
262
|
-
const req = createMockRequest([
|
|
263
|
-
{
|
|
264
|
-
fieldname: 'file',
|
|
265
|
-
filename: 'large.jpg',
|
|
266
|
-
mimeType: 'image/jpeg',
|
|
267
|
-
data: mockData
|
|
268
|
-
}
|
|
269
|
-
]);
|
|
270
|
-
|
|
271
|
-
await expect(uploader.handleMultipart(req)).rejects.toThrow('File size exceeded');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('should call onProgress callback during upload', async () => {
|
|
275
|
-
const onProgress = jest.fn();
|
|
276
|
-
const uploaderWithProgress = new FileUploader({
|
|
277
|
-
maxFileSize: 1024 * 1024,
|
|
278
|
-
allowedMimeTypes: ['image/jpeg'],
|
|
279
|
-
onProgress
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
const mockData = Buffer.from('test content for progress tracking');
|
|
283
|
-
const req = createMockRequest([
|
|
284
|
-
{
|
|
285
|
-
fieldname: 'file',
|
|
286
|
-
filename: 'test.jpg',
|
|
287
|
-
mimeType: 'image/jpeg',
|
|
288
|
-
data: mockData
|
|
289
|
-
}
|
|
290
|
-
]);
|
|
291
|
-
|
|
292
|
-
await uploaderWithProgress.handleMultipart(req);
|
|
293
|
-
|
|
294
|
-
expect(onProgress).toHaveBeenCalled();
|
|
295
|
-
const lastCall = onProgress.mock.calls[onProgress.mock.calls.length - 1][0];
|
|
296
|
-
expect(lastCall.percentage).toBe(100);
|
|
297
|
-
expect(lastCall.bytesUploaded).toBe(mockData.length);
|
|
298
|
-
expect(lastCall.totalBytes).toBe(mockData.length);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('should use custom destination string', async () => {
|
|
302
|
-
const uploaderWithDest = new FileUploader({
|
|
303
|
-
destination: '/custom/uploads',
|
|
304
|
-
allowedMimeTypes: ['image/jpeg']
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const mockData = Buffer.from('test');
|
|
308
|
-
const req = createMockRequest([
|
|
309
|
-
{
|
|
310
|
-
fieldname: 'file',
|
|
311
|
-
filename: 'test.jpg',
|
|
312
|
-
mimeType: 'image/jpeg',
|
|
313
|
-
data: mockData
|
|
314
|
-
}
|
|
315
|
-
]);
|
|
316
|
-
|
|
317
|
-
const results = await uploaderWithDest.handleMultipart(req);
|
|
318
|
-
expect(results[0].destination).toBe('/custom/uploads');
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('should use custom destination function', async () => {
|
|
322
|
-
const uploaderWithDest = new FileUploader({
|
|
323
|
-
destination: async (file: FileInfo) => `/custom/${file.filename}`,
|
|
324
|
-
allowedMimeTypes: ['image/jpeg']
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
const mockData = Buffer.from('test');
|
|
328
|
-
const req = createMockRequest([
|
|
329
|
-
{
|
|
330
|
-
fieldname: 'file',
|
|
331
|
-
filename: 'test.jpg',
|
|
332
|
-
mimeType: 'image/jpeg',
|
|
333
|
-
data: mockData
|
|
334
|
-
}
|
|
335
|
-
]);
|
|
336
|
-
|
|
337
|
-
const results = await uploaderWithDest.handleMultipart(req);
|
|
338
|
-
expect(results[0].destination).toBe('/custom/test.jpg');
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('should handle busboy error', async () => {
|
|
342
|
-
const req = new Readable() as IncomingMessage;
|
|
343
|
-
req.headers = {
|
|
344
|
-
'content-type': 'invalid-content-type'
|
|
345
|
-
};
|
|
346
|
-
req.push('invalid data');
|
|
347
|
-
req.push(null);
|
|
348
|
-
|
|
349
|
-
await expect(uploader.handleMultipart(req)).rejects.toThrow();
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('should calculate progress percentage correctly', async () => {
|
|
353
|
-
const progressUpdates: number[] = [];
|
|
354
|
-
const uploaderWithProgress = new FileUploader({
|
|
355
|
-
maxFileSize: 1024 * 1024,
|
|
356
|
-
allowedMimeTypes: ['image/jpeg'],
|
|
357
|
-
onProgress: (progress) => {
|
|
358
|
-
progressUpdates.push(progress.percentage);
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const mockData = Buffer.from('test content');
|
|
363
|
-
const req = createMockRequest([
|
|
364
|
-
{
|
|
365
|
-
fieldname: 'file',
|
|
366
|
-
filename: 'test.jpg',
|
|
367
|
-
mimeType: 'image/jpeg',
|
|
368
|
-
data: mockData
|
|
369
|
-
}
|
|
370
|
-
]);
|
|
371
|
-
|
|
372
|
-
await uploaderWithProgress.handleMultipart(req);
|
|
373
|
-
|
|
374
|
-
expect(progressUpdates.length).toBeGreaterThan(0);
|
|
375
|
-
expect(progressUpdates[progressUpdates.length - 1]).toBe(100);
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
describe('getProgress', () => {
|
|
380
|
-
it('should return null for unknown file ID', () => {
|
|
381
|
-
const progress = uploader.getProgress('unknown-id');
|
|
382
|
-
expect(progress).toBeNull();
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('should return progress during active upload', async () => {
|
|
386
|
-
const onProgress = jest.fn();
|
|
387
|
-
const uploaderWithProgress = new FileUploader({
|
|
388
|
-
maxFileSize: 1024 * 1024,
|
|
389
|
-
allowedMimeTypes: ['image/jpeg'],
|
|
390
|
-
onProgress
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
const mockData = Buffer.from('test');
|
|
394
|
-
const req = createMockRequest([
|
|
395
|
-
{
|
|
396
|
-
fieldname: 'file',
|
|
397
|
-
filename: 'test.jpg',
|
|
398
|
-
mimeType: 'image/jpeg',
|
|
399
|
-
data: mockData
|
|
400
|
-
}
|
|
401
|
-
]);
|
|
402
|
-
|
|
403
|
-
await uploaderWithProgress.handleMultipart(req);
|
|
404
|
-
|
|
405
|
-
// Progress should be cleared after upload completes
|
|
406
|
-
const fileId = onProgress.mock.calls[0][0].fileId;
|
|
407
|
-
const progress = uploaderWithProgress.getProgress(fileId);
|
|
408
|
-
expect(progress).toBeNull(); // Cleared after completion
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
describe('edge cases', () => {
|
|
413
|
-
it('should handle empty file', async () => {
|
|
414
|
-
const mockData = Buffer.alloc(0);
|
|
415
|
-
const req = createMockRequest([
|
|
416
|
-
{
|
|
417
|
-
fieldname: 'file',
|
|
418
|
-
filename: 'empty.jpg',
|
|
419
|
-
mimeType: 'image/jpeg',
|
|
420
|
-
data: mockData
|
|
421
|
-
}
|
|
422
|
-
]);
|
|
423
|
-
|
|
424
|
-
const results = await uploader.handleMultipart(req);
|
|
425
|
-
expect(results).toHaveLength(1);
|
|
426
|
-
expect(results[0].size).toBe(0);
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('should handle special characters in filename', async () => {
|
|
430
|
-
const mockData = Buffer.from('test');
|
|
431
|
-
const req = createMockRequest([
|
|
432
|
-
{
|
|
433
|
-
fieldname: 'file',
|
|
434
|
-
filename: 'test file (1) [copy].jpg',
|
|
435
|
-
mimeType: 'image/jpeg',
|
|
436
|
-
data: mockData
|
|
437
|
-
}
|
|
438
|
-
]);
|
|
439
|
-
|
|
440
|
-
const results = await uploader.handleMultipart(req);
|
|
441
|
-
expect(results).toHaveLength(1);
|
|
442
|
-
expect(results[0].filename).toBe('test file (1) [copy].jpg');
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it('should handle no files in request', async () => {
|
|
446
|
-
const req = createMockRequest([]);
|
|
447
|
-
|
|
448
|
-
const results = await uploader.handleMultipart(req);
|
|
449
|
-
expect(results).toHaveLength(0);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it('should handle mixed valid and invalid files', async () => {
|
|
453
|
-
const mockData1 = Buffer.from('valid file');
|
|
454
|
-
const mockData2 = Buffer.from('invalid file');
|
|
455
|
-
|
|
456
|
-
const req = createMockRequest([
|
|
457
|
-
{
|
|
458
|
-
fieldname: 'file1',
|
|
459
|
-
filename: 'test.jpg',
|
|
460
|
-
mimeType: 'image/jpeg',
|
|
461
|
-
data: mockData1
|
|
462
|
-
},
|
|
463
|
-
{
|
|
464
|
-
fieldname: 'file2',
|
|
465
|
-
filename: 'malware.exe',
|
|
466
|
-
mimeType: 'application/x-msdownload',
|
|
467
|
-
data: mockData2
|
|
468
|
-
}
|
|
469
|
-
]);
|
|
470
|
-
|
|
471
|
-
await expect(uploader.handleMultipart(req)).rejects.toThrow('MIME type');
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
it('should handle destination function throwing non-Error', async () => {
|
|
475
|
-
const uploaderWithThrowingDest = new FileUploader({
|
|
476
|
-
destination: () => {
|
|
477
|
-
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
|
478
|
-
throw 'String error';
|
|
479
|
-
},
|
|
480
|
-
allowedMimeTypes: ['image/jpeg']
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
const mockData = Buffer.from('test');
|
|
484
|
-
const req = createMockRequest([
|
|
485
|
-
{
|
|
486
|
-
fieldname: 'file',
|
|
487
|
-
filename: 'test.jpg',
|
|
488
|
-
mimeType: 'image/jpeg',
|
|
489
|
-
data: mockData
|
|
490
|
-
}
|
|
491
|
-
]);
|
|
492
|
-
|
|
493
|
-
await expect(uploaderWithThrowingDest.handleMultipart(req)).rejects.toThrow();
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
it('should handle destination function throwing Error', async () => {
|
|
497
|
-
const uploaderWithErrorDest = new FileUploader({
|
|
498
|
-
destination: () => {
|
|
499
|
-
throw new Error('Custom destination error');
|
|
500
|
-
},
|
|
501
|
-
allowedMimeTypes: ['image/jpeg']
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
const mockData = Buffer.from('test');
|
|
505
|
-
const req = createMockRequest([
|
|
506
|
-
{
|
|
507
|
-
fieldname: 'file',
|
|
508
|
-
filename: 'test.jpg',
|
|
509
|
-
mimeType: 'image/jpeg',
|
|
510
|
-
data: mockData
|
|
511
|
-
}
|
|
512
|
-
]);
|
|
513
|
-
|
|
514
|
-
await expect(uploaderWithErrorDest.handleMultipart(req)).rejects.toThrow('Custom destination error');
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
});
|
package/jest.config.cjs
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
module.exports = {
|
|
10
|
-
preset: 'ts-jest',
|
|
11
|
-
testEnvironment: '<rootDir>/../../../jest-environment-node-fixed.cjs',
|
|
12
|
-
roots: ['<rootDir>/__tests__'],
|
|
13
|
-
testMatch: ['**/*.test.ts'],
|
|
14
|
-
moduleNameMapper: {
|
|
15
|
-
'^(\\.{1,2}/.*)\\.js$': '$1'
|
|
16
|
-
},
|
|
17
|
-
collectCoverageFrom: [
|
|
18
|
-
'src/**/*.ts',
|
|
19
|
-
'!src/**/*.d.ts',
|
|
20
|
-
'!src/index.ts'
|
|
21
|
-
],
|
|
22
|
-
coverageThreshold: {
|
|
23
|
-
global: {
|
|
24
|
-
branches: 90,
|
|
25
|
-
functions: 90,
|
|
26
|
-
lines: 90,
|
|
27
|
-
statements: 90
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
coverageDirectory: 'coverage',
|
|
31
|
-
verbose: true
|
|
32
|
-
};
|
|
33
|
-
|
package/src/file-uploader.ts
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { createLogger } from '@bernierllc/logger';
|
|
10
|
-
import Busboy from 'busboy';
|
|
11
|
-
import type { IncomingMessage } from 'http';
|
|
12
|
-
import { randomUUID } from 'crypto';
|
|
13
|
-
import type { UploadConfig, FileInfo, UploadProgress, UploadResult } from './types.js';
|
|
14
|
-
|
|
15
|
-
const logger = createLogger();
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Multipart file upload handler
|
|
19
|
-
*/
|
|
20
|
-
export class FileUploader {
|
|
21
|
-
private readonly config: Required<Pick<UploadConfig, 'maxFileSize' | 'maxFiles'>> & Partial<UploadConfig>;
|
|
22
|
-
private readonly progressMap = new Map<string, UploadProgress>();
|
|
23
|
-
|
|
24
|
-
constructor(config: UploadConfig = {}) {
|
|
25
|
-
this.config = {
|
|
26
|
-
maxFileSize: config.maxFileSize ?? 10 * 1024 * 1024, // 10MB default
|
|
27
|
-
maxFiles: config.maxFiles ?? 10,
|
|
28
|
-
allowedMimeTypes: config.allowedMimeTypes,
|
|
29
|
-
destination: config.destination,
|
|
30
|
-
onProgress: config.onProgress
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Handle multipart form upload
|
|
36
|
-
*/
|
|
37
|
-
async handleMultipart(req: IncomingMessage): Promise<UploadResult[]> {
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
const results: UploadResult[] = [];
|
|
40
|
-
let fileCount = 0;
|
|
41
|
-
const errors: Error[] = [];
|
|
42
|
-
|
|
43
|
-
const bb = Busboy({ headers: req.headers });
|
|
44
|
-
|
|
45
|
-
bb.on('file', (fieldname, file, info) => {
|
|
46
|
-
fileCount++;
|
|
47
|
-
|
|
48
|
-
if (fileCount > this.config.maxFiles) {
|
|
49
|
-
const error = new Error(`Maximum file count exceeded: ${this.config.maxFiles}`);
|
|
50
|
-
errors.push(error);
|
|
51
|
-
file.resume();
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const fileId = randomUUID();
|
|
56
|
-
const fileInfo: FileInfo = {
|
|
57
|
-
fieldname,
|
|
58
|
-
filename: info.filename,
|
|
59
|
-
encoding: info.encoding,
|
|
60
|
-
mimeType: info.mimeType,
|
|
61
|
-
size: 0
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Validate file
|
|
65
|
-
const validation = this.validateFile(fileInfo);
|
|
66
|
-
if (!validation.valid) {
|
|
67
|
-
const error = new Error(validation.error || 'File validation failed');
|
|
68
|
-
errors.push(error);
|
|
69
|
-
file.resume();
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Track progress
|
|
74
|
-
let bytesUploaded = 0;
|
|
75
|
-
const progress: UploadProgress = {
|
|
76
|
-
fileId,
|
|
77
|
-
bytesUploaded: 0,
|
|
78
|
-
totalBytes: 0,
|
|
79
|
-
percentage: 0
|
|
80
|
-
};
|
|
81
|
-
this.progressMap.set(fileId, progress);
|
|
82
|
-
|
|
83
|
-
// Collect file data
|
|
84
|
-
const chunks: Buffer[] = [];
|
|
85
|
-
|
|
86
|
-
file.on('data', (chunk: Buffer) => {
|
|
87
|
-
chunks.push(chunk);
|
|
88
|
-
bytesUploaded += chunk.length;
|
|
89
|
-
fileInfo.size = bytesUploaded;
|
|
90
|
-
|
|
91
|
-
// Update progress
|
|
92
|
-
progress.bytesUploaded = bytesUploaded;
|
|
93
|
-
progress.totalBytes = bytesUploaded; // Will be updated when complete
|
|
94
|
-
progress.percentage = this.config.maxFileSize
|
|
95
|
-
? Math.min(100, (bytesUploaded / this.config.maxFileSize) * 100)
|
|
96
|
-
: 0;
|
|
97
|
-
|
|
98
|
-
if (this.config.onProgress) {
|
|
99
|
-
this.config.onProgress(progress);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Check size limit
|
|
103
|
-
if (bytesUploaded > this.config.maxFileSize) {
|
|
104
|
-
const error = new Error(`File size exceeded: ${this.config.maxFileSize} bytes`);
|
|
105
|
-
errors.push(error);
|
|
106
|
-
file.resume();
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
file.on('end', async () => {
|
|
111
|
-
try {
|
|
112
|
-
const buffer = Buffer.concat(chunks as readonly Uint8Array[]);
|
|
113
|
-
fileInfo.size = buffer.length;
|
|
114
|
-
|
|
115
|
-
// Update final progress
|
|
116
|
-
progress.totalBytes = buffer.length;
|
|
117
|
-
progress.percentage = 100;
|
|
118
|
-
if (this.config.onProgress) {
|
|
119
|
-
this.config.onProgress(progress);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Determine destination
|
|
123
|
-
let destination: string;
|
|
124
|
-
if (typeof this.config.destination === 'function') {
|
|
125
|
-
destination = await this.config.destination(fileInfo);
|
|
126
|
-
} else {
|
|
127
|
-
destination = this.config.destination || `/uploads/${fileId}-${fileInfo.filename}`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const result: UploadResult = {
|
|
131
|
-
fileId,
|
|
132
|
-
filename: fileInfo.filename,
|
|
133
|
-
size: buffer.length,
|
|
134
|
-
mimeType: fileInfo.mimeType,
|
|
135
|
-
destination
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
results.push(result);
|
|
139
|
-
this.progressMap.delete(fileId);
|
|
140
|
-
|
|
141
|
-
logger.debug('File uploaded', { fileId, filename: fileInfo.filename, size: buffer.length });
|
|
142
|
-
} catch (error) {
|
|
143
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
144
|
-
errors.push(err);
|
|
145
|
-
logger.error('Error processing file', err);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
bb.on('finish', () => {
|
|
151
|
-
if (errors.length > 0) {
|
|
152
|
-
reject(new Error(`Upload failed: ${errors.map(e => e.message).join(', ')}`));
|
|
153
|
-
} else {
|
|
154
|
-
resolve(results);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
bb.on('error', (error) => {
|
|
159
|
-
reject(error);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
req.pipe(bb);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Validate file
|
|
168
|
-
*/
|
|
169
|
-
validateFile(file: FileInfo): { valid: boolean; error?: string } {
|
|
170
|
-
// Check MIME type
|
|
171
|
-
if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {
|
|
172
|
-
if (!this.config.allowedMimeTypes.includes(file.mimeType)) {
|
|
173
|
-
return {
|
|
174
|
-
valid: false,
|
|
175
|
-
error: `MIME type ${file.mimeType} not allowed. Allowed types: ${this.config.allowedMimeTypes.join(', ')}`
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Check file size (if already known)
|
|
181
|
-
if (file.size > this.config.maxFileSize) {
|
|
182
|
-
return {
|
|
183
|
-
valid: false,
|
|
184
|
-
error: `File size ${file.size} exceeds maximum ${this.config.maxFileSize} bytes`
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return { valid: true };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Get upload progress
|
|
193
|
-
*/
|
|
194
|
-
getProgress(fileId: string): UploadProgress | null {
|
|
195
|
-
return this.progressMap.get(fileId) || null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
package/src/index.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export { FileUploader } from './file-uploader.js';
|
|
10
|
-
export type {
|
|
11
|
-
UploadConfig,
|
|
12
|
-
FileInfo,
|
|
13
|
-
UploadProgress,
|
|
14
|
-
UploadResult
|
|
15
|
-
} from './types.js';
|
|
16
|
-
|
package/src/types.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* File upload configuration
|
|
12
|
-
*/
|
|
13
|
-
export interface UploadConfig {
|
|
14
|
-
/** Maximum file size in bytes */
|
|
15
|
-
maxFileSize?: number;
|
|
16
|
-
/** Allowed MIME types */
|
|
17
|
-
allowedMimeTypes?: string[];
|
|
18
|
-
/** Maximum number of files */
|
|
19
|
-
maxFiles?: number;
|
|
20
|
-
/** Destination path or function */
|
|
21
|
-
destination?: string | ((file: FileInfo) => Promise<string>);
|
|
22
|
-
/** Progress callback */
|
|
23
|
-
onProgress?: (progress: UploadProgress) => void;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* File information
|
|
28
|
-
*/
|
|
29
|
-
export interface FileInfo {
|
|
30
|
-
/** Form field name */
|
|
31
|
-
fieldname: string;
|
|
32
|
-
/** Original filename */
|
|
33
|
-
filename: string;
|
|
34
|
-
/** File encoding */
|
|
35
|
-
encoding: string;
|
|
36
|
-
/** MIME type */
|
|
37
|
-
mimeType: string;
|
|
38
|
-
/** File size in bytes */
|
|
39
|
-
size: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Upload progress information
|
|
44
|
-
*/
|
|
45
|
-
export interface UploadProgress {
|
|
46
|
-
/** File identifier */
|
|
47
|
-
fileId: string;
|
|
48
|
-
/** Bytes uploaded */
|
|
49
|
-
bytesUploaded: number;
|
|
50
|
-
/** Total bytes */
|
|
51
|
-
totalBytes: number;
|
|
52
|
-
/** Percentage complete */
|
|
53
|
-
percentage: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Upload result
|
|
58
|
-
*/
|
|
59
|
-
export interface UploadResult {
|
|
60
|
-
/** File identifier */
|
|
61
|
-
fileId: string;
|
|
62
|
-
/** Filename */
|
|
63
|
-
filename: string;
|
|
64
|
-
/** File size */
|
|
65
|
-
size: number;
|
|
66
|
-
/** MIME type */
|
|
67
|
-
mimeType: string;
|
|
68
|
-
/** Destination path */
|
|
69
|
-
destination: string;
|
|
70
|
-
/** Optional URL */
|
|
71
|
-
url?: string;
|
|
72
|
-
}
|
|
73
|
-
|
package/tsconfig.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2020"],
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"moduleResolution": "bundler",
|
|
14
|
-
"resolveJsonModule": true,
|
|
15
|
-
"noUnusedLocals": true,
|
|
16
|
-
"noUnusedParameters": true,
|
|
17
|
-
"noImplicitReturns": true,
|
|
18
|
-
"noFallthroughCasesInSwitch": true,
|
|
19
|
-
"allowSyntheticDefaultImports": true
|
|
20
|
-
},
|
|
21
|
-
"include": ["src/**/*"],
|
|
22
|
-
"exclude": ["node_modules", "dist", "__tests__"]
|
|
23
|
-
}
|
|
24
|
-
|