@asterflow/multipart 1.1.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/README.md +75 -0
- package/dist/cjs/index.cjs +437 -0
- package/dist/cjs/package.json +3 -0
- package/dist/mjs/index.js +416 -0
- package/dist/mjs/package.json +3 -0
- package/dist/types/controllers/MultipartParser.d.ts +18 -0
- package/dist/types/controllers/multipartExtension.d.ts +8 -0
- package/dist/types/controllers/validateMultipartFields.d.ts +3 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/types/asterflow.d.ts +61 -0
- package/dist/types/types/inferRequest.d.ts +18 -0
- package/dist/types/types/mime.d.ts +21 -0
- package/dist/types/types/multipart.d.ts +153 -0
- package/dist/types/utils/errors.d.ts +14 -0
- package/dist/types/utils/log.d.ts +2 -0
- package/dist/types/utils/stream.d.ts +8 -0
- package/package.json +50 -0
- package/tsconfig.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @asterflow/multipart
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
> Parses `multipart/form-data` requests before your handler runs, with optional per-route field rules that are checked at runtime and enforced in the handler's types.
|
|
14
|
+
|
|
15
|
+
## 📦 Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun install @asterflow/multipart
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Register the plugin on an AsterFlow app:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { AsterFlow } from 'asterflow'
|
|
25
|
+
import { multipartPlugin } from '@asterflow/multipart'
|
|
26
|
+
|
|
27
|
+
const app = new AsterFlow()
|
|
28
|
+
.use(multipartPlugin, { limits: { fileSize: 10 * 1024 * 1024 } })
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### ✨ Features
|
|
32
|
+
|
|
33
|
+
- **Automatic parsing** - any `multipart/form-data` request is parsed with `busboy` before it reaches your route.
|
|
34
|
+
- **Per-route criteria** - calling `.multipart({...})` on `Method.create(...)` (or inside a `Router.builder(...).method(...)` chain) validates fields at runtime and narrows `getFile`/`getFiles` in that handler's types - a `required` field types as always-present, a declared `mimeTypes` list narrows `.mimeType`.
|
|
35
|
+
- **Request extensions** - `request.body`, `request.files`, `request.getFile()`, `request.getFiles()`, `request.hasFiles()`, `request.getFilesByType()`, `request.saveAll()` and `request.cleanupMultipart()` are attached directly onto `request`, no wrapper object.
|
|
36
|
+
- **Streaming storage** - files stream into memory or to disk (`fileHandling.keepInMemory`), never buffered twice.
|
|
37
|
+
- **Automatic cleanup** - temp files written to disk are removed after the response is sent.
|
|
38
|
+
- **Standardized errors** - limit, MIME/extension and required-field failures all reject with the same `{ error, code, message }` shape before your handler runs.
|
|
39
|
+
|
|
40
|
+
## ❓ How to Use
|
|
41
|
+
|
|
42
|
+
Declare a route's fields with `.multipart(schema)` - the handler only runs once the request passes that schema, and `getFile`/`getFiles` are typed to match it:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Method } from '@asterflow/router'
|
|
46
|
+
|
|
47
|
+
export default Method.create(Method.POST, { path: '/avatar' })
|
|
48
|
+
.multipart({
|
|
49
|
+
avatar: { mimeTypes: ['image/png', 'image/jpeg'], maxSize: 5 * 1024 * 1024, required: true }
|
|
50
|
+
})
|
|
51
|
+
.handler(({ request, response }) => {
|
|
52
|
+
const avatar = request.getFile('avatar') // always present, mimeType narrowed
|
|
53
|
+
return response.success({ filename: avatar.filename, size: avatar.size })
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Without a declared schema, the same methods are still there on `request`, just optional and unnarrowed:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
export default Method.create(Method.POST, { path: '/upload' }).handler(({ request, response }) => {
|
|
61
|
+
if (!request.hasFiles?.()) return response.badRequest({ error: 'NO_FILES' })
|
|
62
|
+
return response.success({ files: request.files, fields: request.body })
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 🔗 Related Packages
|
|
67
|
+
|
|
68
|
+
- [@asterflow/plugin](https://www.npmjs.com/package/@asterflow/plugin) - built as a plugin with `Plugin.create()`
|
|
69
|
+
- [@asterflow/router](https://www.npmjs.com/package/@asterflow/router) - adds `.multipart(...)` to `Method` and `RouteMethodBuilder` via declaration merging
|
|
70
|
+
- [@asterflow/request](https://www.npmjs.com/package/@asterflow/request) - extends `AsterRequest` via `.extend()` to attach parsed multipart data
|
|
71
|
+
- [@asterflow/response](https://www.npmjs.com/package/@asterflow/response) - returns `AsterResponse` errors for malformed or rejected multipart requests
|
|
72
|
+
|
|
73
|
+
## 📄 License
|
|
74
|
+
|
|
75
|
+
This project is licensed under the [MIT License](../../LICENSE).
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var W = Object.create;
|
|
3
|
+
var L = Object.defineProperty;
|
|
4
|
+
var ee = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var te = Object.getOwnPropertyNames;
|
|
6
|
+
var ie = Object.getPrototypeOf, re = Object.prototype.hasOwnProperty;
|
|
7
|
+
var ne = (t, e) => {
|
|
8
|
+
for (var i in e)
|
|
9
|
+
L(t, i, { get: e[i], enumerable: !0 });
|
|
10
|
+
}, O = (t, e, i, r) => {
|
|
11
|
+
if (e && typeof e == "object" || typeof e == "function")
|
|
12
|
+
for (let s of te(e))
|
|
13
|
+
!re.call(t, s) && s !== i && L(t, s, { get: () => e[s], enumerable: !(r = ee(e, s)) || r.enumerable });
|
|
14
|
+
return t;
|
|
15
|
+
};
|
|
16
|
+
var oe = (t, e, i) => (i = t != null ? W(ie(t)) : {}, O(
|
|
17
|
+
e || !t || !t.__esModule ? L(i, "default", { value: t, enumerable: !0 }) : i,
|
|
18
|
+
t
|
|
19
|
+
)), se = (t) => O(L({}, "__esModule", { value: !0 }), t);
|
|
20
|
+
// plugins/multipart/src/index.ts
|
|
21
|
+
var me = {};
|
|
22
|
+
ne(me, {
|
|
23
|
+
DEFAULT_CONFIG: () => b,
|
|
24
|
+
ErrorCodes: () => n,
|
|
25
|
+
MultipartError: () => o,
|
|
26
|
+
MultipartParser: () => v,
|
|
27
|
+
default: () => fe,
|
|
28
|
+
errorResponse: () => A,
|
|
29
|
+
multipartPlugin: () => K,
|
|
30
|
+
parseMultipart: () => k,
|
|
31
|
+
resolveConfig: () => R,
|
|
32
|
+
validateFields: () => D
|
|
33
|
+
});
|
|
34
|
+
module.exports = se(me);
|
|
35
|
+
var G = require("@asterflow/plugin"), P = require("@asterflow/router"), J = require("fs/promises"), Q = require("os");
|
|
36
|
+
// plugins/multipart/package.json
|
|
37
|
+
var z = "1.1.0";
|
|
38
|
+
// plugins/multipart/src/controllers/multipartExtension.ts
|
|
39
|
+
var x = require("@asterflow/router"), $ = !1;
|
|
40
|
+
function B() {
|
|
41
|
+
return $ || ($ = !0, x.Method.prototype.multipart = function(t) {
|
|
42
|
+
return this.extend({}, { multipart: t });
|
|
43
|
+
}, x.RouteMethodBuilder.prototype.multipart = function(t) {
|
|
44
|
+
return this.extend({}, { multipart: t });
|
|
45
|
+
}), !0;
|
|
46
|
+
}
|
|
47
|
+
// plugins/multipart/src/types/multipart.ts
|
|
48
|
+
var b = {
|
|
49
|
+
limits: {
|
|
50
|
+
fieldNameSize: 100,
|
|
51
|
+
fieldSize: 1048576,
|
|
52
|
+
fields: 1 / 0,
|
|
53
|
+
fileSize: 10485760,
|
|
54
|
+
files: 10,
|
|
55
|
+
parts: 1 / 0
|
|
56
|
+
},
|
|
57
|
+
fileHandling: {
|
|
58
|
+
keepInMemory: !0
|
|
59
|
+
},
|
|
60
|
+
validation: {
|
|
61
|
+
allowedMimeTypes: [],
|
|
62
|
+
allowedExtensions: []
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
function R(t, e) {
|
|
66
|
+
return {
|
|
67
|
+
limits: {
|
|
68
|
+
...b.limits,
|
|
69
|
+
...t.limits
|
|
70
|
+
},
|
|
71
|
+
fileHandling: {
|
|
72
|
+
keepInMemory: !0,
|
|
73
|
+
tempDir: e,
|
|
74
|
+
...t.fileHandling
|
|
75
|
+
},
|
|
76
|
+
validation: {
|
|
77
|
+
...b.validation,
|
|
78
|
+
...t.validation
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
var n = {
|
|
83
|
+
LIMIT_FILE_SIZE: "LIMIT_FILE_SIZE",
|
|
84
|
+
LIMIT_FILE_COUNT: "LIMIT_FILE_COUNT",
|
|
85
|
+
LIMIT_FIELD_SIZE: "LIMIT_FIELD_SIZE",
|
|
86
|
+
LIMIT_FIELD_COUNT: "LIMIT_FIELD_COUNT",
|
|
87
|
+
LIMIT_PARTS: "LIMIT_PARTS",
|
|
88
|
+
INVALID_MIME_TYPE: "INVALID_MIME_TYPE",
|
|
89
|
+
INVALID_EXTENSION: "INVALID_EXTENSION",
|
|
90
|
+
VALIDATION_FAILED: "VALIDATION_FAILED",
|
|
91
|
+
FIELD_REQUIRED: "FIELD_REQUIRED",
|
|
92
|
+
FIELD_TOO_MANY_FILES: "FIELD_TOO_MANY_FILES",
|
|
93
|
+
PARSE_ERROR: "PARSE_ERROR"
|
|
94
|
+
}, o = class extends Error {
|
|
95
|
+
code;
|
|
96
|
+
details;
|
|
97
|
+
constructor(e, i, r) {
|
|
98
|
+
super(e), this.name = "MultipartError", this.code = i, this.details = r;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// plugins/multipart/src/controllers/validateMultipartFields.ts
|
|
102
|
+
function D(t, e) {
|
|
103
|
+
for (let [i, r] of Object.entries(e)) {
|
|
104
|
+
let s = t.files.filter((u) => u.fieldName === i), c = r.multiple ?? !1;
|
|
105
|
+
if ((r.required ?? !1) && s.length === 0)
|
|
106
|
+
throw new o(`Field "${i}" is required`, n.FIELD_REQUIRED, { field: i });
|
|
107
|
+
if (!c && s.length > 1)
|
|
108
|
+
throw new o(
|
|
109
|
+
`Field "${i}" accepts only a single file`,
|
|
110
|
+
n.FIELD_TOO_MANY_FILES,
|
|
111
|
+
{ field: i, count: s.length }
|
|
112
|
+
);
|
|
113
|
+
for (let u of s) {
|
|
114
|
+
if (r.mimeTypes && !r.mimeTypes.includes(u.mimeType))
|
|
115
|
+
throw new o(
|
|
116
|
+
`Field "${i}" has unsupported MIME type: ${u.mimeType}`,
|
|
117
|
+
n.INVALID_MIME_TYPE,
|
|
118
|
+
{ field: i, mimeType: u.mimeType }
|
|
119
|
+
);
|
|
120
|
+
if (r.extensions && !r.extensions.map((d) => d.toLowerCase()).includes(u.extension.toLowerCase()))
|
|
121
|
+
throw new o(
|
|
122
|
+
`Field "${i}" has unsupported extension: ${u.extension}`,
|
|
123
|
+
n.INVALID_EXTENSION,
|
|
124
|
+
{ field: i, extension: u.extension }
|
|
125
|
+
);
|
|
126
|
+
if (r.maxSize && u.size > r.maxSize)
|
|
127
|
+
throw new o(
|
|
128
|
+
`Field "${i}" exceeds max size of ${r.maxSize} bytes`,
|
|
129
|
+
n.LIMIT_FILE_SIZE,
|
|
130
|
+
{ field: i, size: u.size, maxSize: r.maxSize }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// plugins/multipart/src/controllers/MultipartParser.ts
|
|
136
|
+
var C = oe(require("busboy"), 1), H = require("crypto"), w = require("fs"), y = require("fs/promises"), j = require("os"), M = require("path"), Y = require("stream"), T = require("stream/promises");
|
|
137
|
+
// plugins/multipart/src/utils/stream.ts
|
|
138
|
+
var q = require("stream");
|
|
139
|
+
function U(t) {
|
|
140
|
+
return typeof t == "object" && t !== null && typeof t.pipe == "function";
|
|
141
|
+
}
|
|
142
|
+
function ue(t) {
|
|
143
|
+
return typeof t == "object" && t !== null && typeof t.headers?.forEach == "function" && "body" in t;
|
|
144
|
+
}
|
|
145
|
+
function V(t) {
|
|
146
|
+
if (U(t)) return t;
|
|
147
|
+
let e = t?.raw;
|
|
148
|
+
if (U(e)) return e;
|
|
149
|
+
if (ue(t)) {
|
|
150
|
+
let i = t.body;
|
|
151
|
+
return i ? q.Readable.fromWeb(i) : null;
|
|
152
|
+
}
|
|
153
|
+
throw new o(
|
|
154
|
+
"Unsupported request type for multipart parsing",
|
|
155
|
+
n.PARSE_ERROR
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
// plugins/multipart/src/controllers/MultipartParser.ts
|
|
159
|
+
var pe = C.default ?? C, _ = class {
|
|
160
|
+
fieldName;
|
|
161
|
+
filename;
|
|
162
|
+
encoding;
|
|
163
|
+
mimeType;
|
|
164
|
+
size;
|
|
165
|
+
extension;
|
|
166
|
+
buffer;
|
|
167
|
+
tempPath;
|
|
168
|
+
constructor(e) {
|
|
169
|
+
this.fieldName = e.fieldName, this.filename = e.filename, this.encoding = e.encoding, this.mimeType = e.mimeType, this.size = e.size, this.extension = e.extension, this.buffer = e.buffer, this.tempPath = e.tempPath;
|
|
170
|
+
}
|
|
171
|
+
async toBuffer() {
|
|
172
|
+
if (this.buffer) return this.buffer;
|
|
173
|
+
if (this.tempPath) return (0, y.readFile)(this.tempPath);
|
|
174
|
+
throw new Error(`No data available for file "${this.filename}"`);
|
|
175
|
+
}
|
|
176
|
+
async save(e) {
|
|
177
|
+
if (await (0, y.mkdir)((0, M.dirname)(e), { recursive: !0 }), this.buffer) {
|
|
178
|
+
await (0, y.writeFile)(e, this.buffer);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (this.tempPath) {
|
|
182
|
+
await (0, T.pipeline)((0, w.createReadStream)(this.tempPath), (0, w.createWriteStream)(e));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`No data available for file "${this.filename}"`);
|
|
186
|
+
}
|
|
187
|
+
stream() {
|
|
188
|
+
if (this.buffer) return Y.Readable.from(this.buffer);
|
|
189
|
+
if (this.tempPath) return (0, w.createReadStream)(this.tempPath);
|
|
190
|
+
throw new Error(`No data available for file "${this.filename}"`);
|
|
191
|
+
}
|
|
192
|
+
}, v = class {
|
|
193
|
+
config;
|
|
194
|
+
events;
|
|
195
|
+
constructor(e = {}, i = {}) {
|
|
196
|
+
this.config = R(e, e.fileHandling?.tempDir ?? (0, j.tmpdir)()), this.events = i;
|
|
197
|
+
}
|
|
198
|
+
async parse(e) {
|
|
199
|
+
let i = Date.now(), r = V(e.raw);
|
|
200
|
+
return r ? new Promise((s, c) => {
|
|
201
|
+
let p = !1, u = 0, f = 0, d = {}, I = [], F = [], a;
|
|
202
|
+
try {
|
|
203
|
+
a = pe({ headers: e.getHeaders(), limits: this.config.limits });
|
|
204
|
+
} catch (l) {
|
|
205
|
+
c(new o(
|
|
206
|
+
l instanceof Error ? l.message : "Failed to initialize the multipart parser",
|
|
207
|
+
n.PARSE_ERROR
|
|
208
|
+
));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let m = (l) => {
|
|
212
|
+
if (p) return;
|
|
213
|
+
p = !0;
|
|
214
|
+
let g = l instanceof o ? l : new o(
|
|
215
|
+
l instanceof Error ? l.message : "Unknown multipart parsing error",
|
|
216
|
+
n.PARSE_ERROR
|
|
217
|
+
);
|
|
218
|
+
this.events.onError?.(g), r.unpipe(a), queueMicrotask(() => a.destroy()), c(g);
|
|
219
|
+
};
|
|
220
|
+
a.on("field", (l, g, S) => {
|
|
221
|
+
if (p) return;
|
|
222
|
+
if (S.valueTruncated) {
|
|
223
|
+
m(new o(`Field "${l}" exceeds the configured size limit`, n.LIMIT_FIELD_SIZE));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.events.onField?.(l, g);
|
|
227
|
+
let h = d[l];
|
|
228
|
+
d[l] = h === void 0 ? g : Array.isArray(h) ? [...h, g] : [h, g], f++;
|
|
229
|
+
}), a.on("file", (l, g, S) => {
|
|
230
|
+
if (p) {
|
|
231
|
+
g.resume();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
let h = this.consumeFile(l, g, S, m, (E) => {
|
|
235
|
+
u += E.length, this.events.onProgress?.(u);
|
|
236
|
+
}).then((E) => {
|
|
237
|
+
p || !E || (I.push(E), this.events.onFileEnd?.(E));
|
|
238
|
+
}).catch((E) => m(E));
|
|
239
|
+
F.push(h);
|
|
240
|
+
}), a.on("partsLimit", () => m(new o("Parts limit exceeded", n.LIMIT_PARTS))), a.on("filesLimit", () => m(new o("Files limit exceeded", n.LIMIT_FILE_COUNT))), a.on("fieldsLimit", () => m(new o("Fields limit exceeded", n.LIMIT_FIELD_COUNT))), a.on("error", (l) => m(l)), r.on("error", (l) => m(l)), a.on("close", async () => {
|
|
241
|
+
try {
|
|
242
|
+
await Promise.all(F);
|
|
243
|
+
} catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
p || (p = !0, s({
|
|
247
|
+
fields: d,
|
|
248
|
+
files: I,
|
|
249
|
+
metadata: {
|
|
250
|
+
processingTime: Date.now() - i,
|
|
251
|
+
totalSize: u,
|
|
252
|
+
fieldsCount: f,
|
|
253
|
+
filesCount: I.length
|
|
254
|
+
}
|
|
255
|
+
}));
|
|
256
|
+
}), r.pipe(a);
|
|
257
|
+
}) : {
|
|
258
|
+
fields: {},
|
|
259
|
+
files: [],
|
|
260
|
+
metadata: { processingTime: Date.now() - i, totalSize: 0, fieldsCount: 0, filesCount: 0 }
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async consumeFile(e, i, r, s, c) {
|
|
264
|
+
let p = {
|
|
265
|
+
fieldName: e,
|
|
266
|
+
filename: r.filename || "unknown",
|
|
267
|
+
encoding: r.encoding,
|
|
268
|
+
mimeType: r.mimeType
|
|
269
|
+
}, u = !1;
|
|
270
|
+
i.once("limit", () => {
|
|
271
|
+
u = !0, s(new o(`File "${p.filename}" exceeds the configured size limit`, n.LIMIT_FILE_SIZE));
|
|
272
|
+
}), i.on("error", (a) => s(a));
|
|
273
|
+
let f = await this.validateFile(p);
|
|
274
|
+
if (!f.valid)
|
|
275
|
+
return i.resume(), s(f.error), null;
|
|
276
|
+
this.events.onFileStart?.(p);
|
|
277
|
+
let d = 0, I = (0, M.extname)(p.filename), F = (a) => {
|
|
278
|
+
d += a.length, c(a), this.events.onFileData?.(p, a);
|
|
279
|
+
};
|
|
280
|
+
try {
|
|
281
|
+
if (this.config.fileHandling.keepInMemory) {
|
|
282
|
+
let m = [];
|
|
283
|
+
return i.on("data", (l) => {
|
|
284
|
+
F(l), m.push(l);
|
|
285
|
+
}), await (0, T.finished)(i), u ? null : new _({ ...p, size: d, extension: I, buffer: Buffer.concat(m) });
|
|
286
|
+
}
|
|
287
|
+
await (0, y.mkdir)(this.config.fileHandling.tempDir, { recursive: !0 });
|
|
288
|
+
let a = (0, M.join)(this.config.fileHandling.tempDir, `multipart-${(0, H.randomUUID)()}${I}`);
|
|
289
|
+
return i.on("data", F), await (0, T.pipeline)(i, (0, w.createWriteStream)(a)), u ? null : new _({ ...p, size: d, extension: I, tempPath: a });
|
|
290
|
+
} catch (a) {
|
|
291
|
+
return u || s(a), null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async validateFile(e) {
|
|
295
|
+
let { validation: i } = this.config;
|
|
296
|
+
if (i.allowedMimeTypes && i.allowedMimeTypes.length > 0 && !i.allowedMimeTypes.includes(e.mimeType))
|
|
297
|
+
return {
|
|
298
|
+
valid: !1,
|
|
299
|
+
error: new o(`MIME type "${e.mimeType}" is not allowed`, n.INVALID_MIME_TYPE)
|
|
300
|
+
};
|
|
301
|
+
if (i.allowedExtensions && i.allowedExtensions.length > 0) {
|
|
302
|
+
let r = (0, M.extname)(e.filename).toLowerCase();
|
|
303
|
+
if (!i.allowedExtensions.some((s) => s.toLowerCase() === r))
|
|
304
|
+
return {
|
|
305
|
+
valid: !1,
|
|
306
|
+
error: new o(`Extension "${r}" is not allowed`, n.INVALID_EXTENSION)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (i.validator)
|
|
310
|
+
try {
|
|
311
|
+
if (!await i.validator(e))
|
|
312
|
+
return {
|
|
313
|
+
valid: !1,
|
|
314
|
+
error: new o(`File "${e.filename}" failed custom validation`, n.VALIDATION_FAILED)
|
|
315
|
+
};
|
|
316
|
+
} catch (r) {
|
|
317
|
+
return {
|
|
318
|
+
valid: !1,
|
|
319
|
+
error: new o(
|
|
320
|
+
`Validator threw an error: ${r instanceof Error ? r.message : "Unknown error"}`,
|
|
321
|
+
n.VALIDATION_FAILED
|
|
322
|
+
)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return { valid: !0 };
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
async function k(t, e, i) {
|
|
329
|
+
return new v(e, i).parse(t);
|
|
330
|
+
}
|
|
331
|
+
// plugins/multipart/src/utils/errors.ts
|
|
332
|
+
function A(t, e) {
|
|
333
|
+
let i = {
|
|
334
|
+
code: e.code,
|
|
335
|
+
message: e.message,
|
|
336
|
+
...e.details !== void 0 ? { details: e.details } : {}
|
|
337
|
+
};
|
|
338
|
+
switch (e.code) {
|
|
339
|
+
case n.LIMIT_FILE_SIZE:
|
|
340
|
+
case n.LIMIT_FILE_COUNT:
|
|
341
|
+
case n.LIMIT_FIELD_SIZE:
|
|
342
|
+
case n.LIMIT_FIELD_COUNT:
|
|
343
|
+
case n.LIMIT_PARTS:
|
|
344
|
+
return t.status(413).json({ error: "PAYLOAD_TOO_LARGE", ...i });
|
|
345
|
+
case n.INVALID_MIME_TYPE:
|
|
346
|
+
case n.INVALID_EXTENSION:
|
|
347
|
+
return t.status(415).json({ error: "UNSUPPORTED_MEDIA_TYPE", ...i });
|
|
348
|
+
case n.VALIDATION_FAILED:
|
|
349
|
+
case n.FIELD_REQUIRED:
|
|
350
|
+
case n.FIELD_TOO_MANY_FILES:
|
|
351
|
+
return t.validationError({ error: "VALIDATION_FAILED", ...i });
|
|
352
|
+
default:
|
|
353
|
+
return t.badRequest({ error: "MULTIPART_ERROR", ...i });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// plugins/multipart/src/utils/log.ts
|
|
357
|
+
var N = {
|
|
358
|
+
reset: "\x1B[0m",
|
|
359
|
+
red: "\x1B[31m",
|
|
360
|
+
blue: "\x1B[34m"
|
|
361
|
+
};
|
|
362
|
+
function Z(...t) {
|
|
363
|
+
process.env.DEBUG === "true" && console.log(`${N.blue}[AsterFlow Multipart]${N.reset}`, ...t);
|
|
364
|
+
}
|
|
365
|
+
function X(t, e) {
|
|
366
|
+
console.error(`${N.red}%s %s${N.reset}`, "[AsterFlow Multipart]", t), console.group(), console.error("Error:", e instanceof Error ? e.message : e), e instanceof Error && "code" in e && console.error("Code:", e.code), console.groupEnd();
|
|
367
|
+
}
|
|
368
|
+
// plugins/multipart/src/index.ts
|
|
369
|
+
function de(t) {
|
|
370
|
+
return {
|
|
371
|
+
body: t.fields,
|
|
372
|
+
files: t.files,
|
|
373
|
+
multipartMetadata: t.metadata,
|
|
374
|
+
getFile(e) {
|
|
375
|
+
return t.files.find((i) => i.fieldName === e);
|
|
376
|
+
},
|
|
377
|
+
getFiles(e) {
|
|
378
|
+
return e ? t.files.filter((i) => i.fieldName === e) : t.files;
|
|
379
|
+
},
|
|
380
|
+
hasFiles() {
|
|
381
|
+
return t.files.length > 0;
|
|
382
|
+
},
|
|
383
|
+
getFilesByType(e) {
|
|
384
|
+
return t.files.filter((i) => i.mimeType === e);
|
|
385
|
+
},
|
|
386
|
+
async saveAll(e) {
|
|
387
|
+
let i = [];
|
|
388
|
+
for (let r of t.files) {
|
|
389
|
+
let s = `${e}/${r.filename}`;
|
|
390
|
+
await r.save(s), i.push(s);
|
|
391
|
+
}
|
|
392
|
+
return i;
|
|
393
|
+
},
|
|
394
|
+
async cleanupMultipart() {
|
|
395
|
+
await Promise.all(t.files.map(async (e) => {
|
|
396
|
+
if (e.tempPath)
|
|
397
|
+
try {
|
|
398
|
+
await (0, J.unlink)(e.tempPath);
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
var K = G.Plugin.create({ name: "multipart" }).decorate("creator", "Ashu11-A").decorate("version", z).decorate("installed", B()).config(b).on("onRequest", async ({ request: t, response: e, router: i, plugin: r }) => {
|
|
406
|
+
let s = t.getMethod().toLowerCase(), c = i.route instanceof P.Method ? i.route.extensions.multipart : (0, P.getRouteExtensions)(i.route)?.[s]?.multipart, p = t.getHeaders()["content-type"];
|
|
407
|
+
if (!(!!p && p.toLowerCase().includes("multipart/form-data")))
|
|
408
|
+
return c ? A(e, new o(
|
|
409
|
+
"Expected a multipart/form-data request",
|
|
410
|
+
n.PARSE_ERROR
|
|
411
|
+
)) : void 0;
|
|
412
|
+
try {
|
|
413
|
+
let f = R(r.context, (0, Q.tmpdir)()), d = await k(t, f);
|
|
414
|
+
c && D(d, c), Z("Multipart request parsed", {
|
|
415
|
+
Fields: String(d.metadata.fieldsCount),
|
|
416
|
+
Files: String(d.metadata.filesCount),
|
|
417
|
+
"Total Size": `${d.metadata.totalSize} bytes`,
|
|
418
|
+
"Processing Time": `${d.metadata.processingTime}ms`
|
|
419
|
+
}), t.extend(de(d));
|
|
420
|
+
} catch (f) {
|
|
421
|
+
let d = f instanceof o ? f : new o(f instanceof Error ? f.message : "Unknown error", n.PARSE_ERROR);
|
|
422
|
+
return X("Failed to parse multipart request", d), A(e, d);
|
|
423
|
+
}
|
|
424
|
+
}).on("onResponse", async ({ request: t }) => {
|
|
425
|
+
await t.cleanupMultipart?.();
|
|
426
|
+
}), fe = K;
|
|
427
|
+
0 && (module.exports = {
|
|
428
|
+
DEFAULT_CONFIG,
|
|
429
|
+
ErrorCodes,
|
|
430
|
+
MultipartError,
|
|
431
|
+
MultipartParser,
|
|
432
|
+
errorResponse,
|
|
433
|
+
multipartPlugin,
|
|
434
|
+
parseMultipart,
|
|
435
|
+
resolveConfig,
|
|
436
|
+
validateFields
|
|
437
|
+
});
|