@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
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// plugins/multipart/src/index.ts
|
|
2
|
+
import { Plugin as re } from "@asterflow/plugin";
|
|
3
|
+
import { getRouteExtensions as ne, Method as oe } from "@asterflow/router";
|
|
4
|
+
import { unlink as se } from "fs/promises";
|
|
5
|
+
import { tmpdir as ae } from "os";
|
|
6
|
+
// plugins/multipart/package.json
|
|
7
|
+
var _ = "1.1.0";
|
|
8
|
+
// plugins/multipart/src/controllers/multipartExtension.ts
|
|
9
|
+
import { Method as H, RouteMethodBuilder as j } from "@asterflow/router";
|
|
10
|
+
var v = !1;
|
|
11
|
+
function A() {
|
|
12
|
+
return v || (v = !0, H.prototype.multipart = function(i) {
|
|
13
|
+
return this.extend({}, { multipart: i });
|
|
14
|
+
}, j.prototype.multipart = function(i) {
|
|
15
|
+
return this.extend({}, { multipart: i });
|
|
16
|
+
}), !0;
|
|
17
|
+
}
|
|
18
|
+
// plugins/multipart/src/types/multipart.ts
|
|
19
|
+
var h = {
|
|
20
|
+
limits: {
|
|
21
|
+
fieldNameSize: 100,
|
|
22
|
+
fieldSize: 1048576,
|
|
23
|
+
fields: 1 / 0,
|
|
24
|
+
fileSize: 10485760,
|
|
25
|
+
files: 10,
|
|
26
|
+
parts: 1 / 0
|
|
27
|
+
},
|
|
28
|
+
fileHandling: {
|
|
29
|
+
keepInMemory: !0
|
|
30
|
+
},
|
|
31
|
+
validation: {
|
|
32
|
+
allowedMimeTypes: [],
|
|
33
|
+
allowedExtensions: []
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function w(i, e) {
|
|
37
|
+
return {
|
|
38
|
+
limits: {
|
|
39
|
+
...h.limits,
|
|
40
|
+
...i.limits
|
|
41
|
+
},
|
|
42
|
+
fileHandling: {
|
|
43
|
+
keepInMemory: !0,
|
|
44
|
+
tempDir: e,
|
|
45
|
+
...i.fileHandling
|
|
46
|
+
},
|
|
47
|
+
validation: {
|
|
48
|
+
...h.validation,
|
|
49
|
+
...i.validation
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
var r = {
|
|
54
|
+
LIMIT_FILE_SIZE: "LIMIT_FILE_SIZE",
|
|
55
|
+
LIMIT_FILE_COUNT: "LIMIT_FILE_COUNT",
|
|
56
|
+
LIMIT_FIELD_SIZE: "LIMIT_FIELD_SIZE",
|
|
57
|
+
LIMIT_FIELD_COUNT: "LIMIT_FIELD_COUNT",
|
|
58
|
+
LIMIT_PARTS: "LIMIT_PARTS",
|
|
59
|
+
INVALID_MIME_TYPE: "INVALID_MIME_TYPE",
|
|
60
|
+
INVALID_EXTENSION: "INVALID_EXTENSION",
|
|
61
|
+
VALIDATION_FAILED: "VALIDATION_FAILED",
|
|
62
|
+
FIELD_REQUIRED: "FIELD_REQUIRED",
|
|
63
|
+
FIELD_TOO_MANY_FILES: "FIELD_TOO_MANY_FILES",
|
|
64
|
+
PARSE_ERROR: "PARSE_ERROR"
|
|
65
|
+
}, o = class extends Error {
|
|
66
|
+
code;
|
|
67
|
+
details;
|
|
68
|
+
constructor(e, t, n) {
|
|
69
|
+
super(e), this.name = "MultipartError", this.code = t, this.details = n;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// plugins/multipart/src/controllers/validateMultipartFields.ts
|
|
73
|
+
function N(i, e) {
|
|
74
|
+
for (let [t, n] of Object.entries(e)) {
|
|
75
|
+
let d = i.files.filter((l) => l.fieldName === t), c = n.multiple ?? !1;
|
|
76
|
+
if ((n.required ?? !1) && d.length === 0)
|
|
77
|
+
throw new o(`Field "${t}" is required`, r.FIELD_REQUIRED, { field: t });
|
|
78
|
+
if (!c && d.length > 1)
|
|
79
|
+
throw new o(
|
|
80
|
+
`Field "${t}" accepts only a single file`,
|
|
81
|
+
r.FIELD_TOO_MANY_FILES,
|
|
82
|
+
{ field: t, count: d.length }
|
|
83
|
+
);
|
|
84
|
+
for (let l of d) {
|
|
85
|
+
if (n.mimeTypes && !n.mimeTypes.includes(l.mimeType))
|
|
86
|
+
throw new o(
|
|
87
|
+
`Field "${t}" has unsupported MIME type: ${l.mimeType}`,
|
|
88
|
+
r.INVALID_MIME_TYPE,
|
|
89
|
+
{ field: t, mimeType: l.mimeType }
|
|
90
|
+
);
|
|
91
|
+
if (n.extensions && !n.extensions.map((p) => p.toLowerCase()).includes(l.extension.toLowerCase()))
|
|
92
|
+
throw new o(
|
|
93
|
+
`Field "${t}" has unsupported extension: ${l.extension}`,
|
|
94
|
+
r.INVALID_EXTENSION,
|
|
95
|
+
{ field: t, extension: l.extension }
|
|
96
|
+
);
|
|
97
|
+
if (n.maxSize && l.size > n.maxSize)
|
|
98
|
+
throw new o(
|
|
99
|
+
`Field "${t}" exceeds max size of ${n.maxSize} bytes`,
|
|
100
|
+
r.LIMIT_FILE_SIZE,
|
|
101
|
+
{ field: t, size: l.size, maxSize: n.maxSize }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// plugins/multipart/src/controllers/MultipartParser.ts
|
|
107
|
+
import * as L from "busboy";
|
|
108
|
+
import { randomUUID as X } from "crypto";
|
|
109
|
+
import { createReadStream as D, createWriteStream as k } from "fs";
|
|
110
|
+
import { mkdir as O, readFile as G, writeFile as J } from "fs/promises";
|
|
111
|
+
import { tmpdir as Q } from "os";
|
|
112
|
+
import { dirname as K, extname as C, join as W } from "path";
|
|
113
|
+
import { Readable as ee } from "stream";
|
|
114
|
+
import { finished as te, pipeline as z } from "stream/promises";
|
|
115
|
+
// plugins/multipart/src/utils/stream.ts
|
|
116
|
+
import { Readable as Y } from "stream";
|
|
117
|
+
function P(i) {
|
|
118
|
+
return typeof i == "object" && i !== null && typeof i.pipe == "function";
|
|
119
|
+
}
|
|
120
|
+
function Z(i) {
|
|
121
|
+
return typeof i == "object" && i !== null && typeof i.headers?.forEach == "function" && "body" in i;
|
|
122
|
+
}
|
|
123
|
+
function S(i) {
|
|
124
|
+
if (P(i)) return i;
|
|
125
|
+
let e = i?.raw;
|
|
126
|
+
if (P(e)) return e;
|
|
127
|
+
if (Z(i)) {
|
|
128
|
+
let t = i.body;
|
|
129
|
+
return t ? Y.fromWeb(t) : null;
|
|
130
|
+
}
|
|
131
|
+
throw new o(
|
|
132
|
+
"Unsupported request type for multipart parsing",
|
|
133
|
+
r.PARSE_ERROR
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
// plugins/multipart/src/controllers/MultipartParser.ts
|
|
137
|
+
var ie = L.default ?? L, F = class {
|
|
138
|
+
fieldName;
|
|
139
|
+
filename;
|
|
140
|
+
encoding;
|
|
141
|
+
mimeType;
|
|
142
|
+
size;
|
|
143
|
+
extension;
|
|
144
|
+
buffer;
|
|
145
|
+
tempPath;
|
|
146
|
+
constructor(e) {
|
|
147
|
+
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;
|
|
148
|
+
}
|
|
149
|
+
async toBuffer() {
|
|
150
|
+
if (this.buffer) return this.buffer;
|
|
151
|
+
if (this.tempPath) return G(this.tempPath);
|
|
152
|
+
throw new Error(`No data available for file "${this.filename}"`);
|
|
153
|
+
}
|
|
154
|
+
async save(e) {
|
|
155
|
+
if (await O(K(e), { recursive: !0 }), this.buffer) {
|
|
156
|
+
await J(e, this.buffer);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (this.tempPath) {
|
|
160
|
+
await z(D(this.tempPath), k(e));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`No data available for file "${this.filename}"`);
|
|
164
|
+
}
|
|
165
|
+
stream() {
|
|
166
|
+
if (this.buffer) return ee.from(this.buffer);
|
|
167
|
+
if (this.tempPath) return D(this.tempPath);
|
|
168
|
+
throw new Error(`No data available for file "${this.filename}"`);
|
|
169
|
+
}
|
|
170
|
+
}, T = class {
|
|
171
|
+
config;
|
|
172
|
+
events;
|
|
173
|
+
constructor(e = {}, t = {}) {
|
|
174
|
+
this.config = w(e, e.fileHandling?.tempDir ?? Q()), this.events = t;
|
|
175
|
+
}
|
|
176
|
+
async parse(e) {
|
|
177
|
+
let t = Date.now(), n = S(e.raw);
|
|
178
|
+
return n ? new Promise((d, c) => {
|
|
179
|
+
let u = !1, l = 0, f = 0, p = {}, I = [], y = [], s;
|
|
180
|
+
try {
|
|
181
|
+
s = ie({ headers: e.getHeaders(), limits: this.config.limits });
|
|
182
|
+
} catch (a) {
|
|
183
|
+
c(new o(
|
|
184
|
+
a instanceof Error ? a.message : "Failed to initialize the multipart parser",
|
|
185
|
+
r.PARSE_ERROR
|
|
186
|
+
));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let m = (a) => {
|
|
190
|
+
if (u) return;
|
|
191
|
+
u = !0;
|
|
192
|
+
let g = a instanceof o ? a : new o(
|
|
193
|
+
a instanceof Error ? a.message : "Unknown multipart parsing error",
|
|
194
|
+
r.PARSE_ERROR
|
|
195
|
+
);
|
|
196
|
+
this.events.onError?.(g), n.unpipe(s), queueMicrotask(() => s.destroy()), c(g);
|
|
197
|
+
};
|
|
198
|
+
s.on("field", (a, g, R) => {
|
|
199
|
+
if (u) return;
|
|
200
|
+
if (R.valueTruncated) {
|
|
201
|
+
m(new o(`Field "${a}" exceeds the configured size limit`, r.LIMIT_FIELD_SIZE));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.events.onField?.(a, g);
|
|
205
|
+
let M = p[a];
|
|
206
|
+
p[a] = M === void 0 ? g : Array.isArray(M) ? [...M, g] : [M, g], f++;
|
|
207
|
+
}), s.on("file", (a, g, R) => {
|
|
208
|
+
if (u) {
|
|
209
|
+
g.resume();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
let M = this.consumeFile(a, g, R, m, (E) => {
|
|
213
|
+
l += E.length, this.events.onProgress?.(l);
|
|
214
|
+
}).then((E) => {
|
|
215
|
+
u || !E || (I.push(E), this.events.onFileEnd?.(E));
|
|
216
|
+
}).catch((E) => m(E));
|
|
217
|
+
y.push(M);
|
|
218
|
+
}), s.on("partsLimit", () => m(new o("Parts limit exceeded", r.LIMIT_PARTS))), s.on("filesLimit", () => m(new o("Files limit exceeded", r.LIMIT_FILE_COUNT))), s.on("fieldsLimit", () => m(new o("Fields limit exceeded", r.LIMIT_FIELD_COUNT))), s.on("error", (a) => m(a)), n.on("error", (a) => m(a)), s.on("close", async () => {
|
|
219
|
+
try {
|
|
220
|
+
await Promise.all(y);
|
|
221
|
+
} catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
u || (u = !0, d({
|
|
225
|
+
fields: p,
|
|
226
|
+
files: I,
|
|
227
|
+
metadata: {
|
|
228
|
+
processingTime: Date.now() - t,
|
|
229
|
+
totalSize: l,
|
|
230
|
+
fieldsCount: f,
|
|
231
|
+
filesCount: I.length
|
|
232
|
+
}
|
|
233
|
+
}));
|
|
234
|
+
}), n.pipe(s);
|
|
235
|
+
}) : {
|
|
236
|
+
fields: {},
|
|
237
|
+
files: [],
|
|
238
|
+
metadata: { processingTime: Date.now() - t, totalSize: 0, fieldsCount: 0, filesCount: 0 }
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
async consumeFile(e, t, n, d, c) {
|
|
242
|
+
let u = {
|
|
243
|
+
fieldName: e,
|
|
244
|
+
filename: n.filename || "unknown",
|
|
245
|
+
encoding: n.encoding,
|
|
246
|
+
mimeType: n.mimeType
|
|
247
|
+
}, l = !1;
|
|
248
|
+
t.once("limit", () => {
|
|
249
|
+
l = !0, d(new o(`File "${u.filename}" exceeds the configured size limit`, r.LIMIT_FILE_SIZE));
|
|
250
|
+
}), t.on("error", (s) => d(s));
|
|
251
|
+
let f = await this.validateFile(u);
|
|
252
|
+
if (!f.valid)
|
|
253
|
+
return t.resume(), d(f.error), null;
|
|
254
|
+
this.events.onFileStart?.(u);
|
|
255
|
+
let p = 0, I = C(u.filename), y = (s) => {
|
|
256
|
+
p += s.length, c(s), this.events.onFileData?.(u, s);
|
|
257
|
+
};
|
|
258
|
+
try {
|
|
259
|
+
if (this.config.fileHandling.keepInMemory) {
|
|
260
|
+
let m = [];
|
|
261
|
+
return t.on("data", (a) => {
|
|
262
|
+
y(a), m.push(a);
|
|
263
|
+
}), await te(t), l ? null : new F({ ...u, size: p, extension: I, buffer: Buffer.concat(m) });
|
|
264
|
+
}
|
|
265
|
+
await O(this.config.fileHandling.tempDir, { recursive: !0 });
|
|
266
|
+
let s = W(this.config.fileHandling.tempDir, `multipart-${X()}${I}`);
|
|
267
|
+
return t.on("data", y), await z(t, k(s)), l ? null : new F({ ...u, size: p, extension: I, tempPath: s });
|
|
268
|
+
} catch (s) {
|
|
269
|
+
return l || d(s), null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async validateFile(e) {
|
|
273
|
+
let { validation: t } = this.config;
|
|
274
|
+
if (t.allowedMimeTypes && t.allowedMimeTypes.length > 0 && !t.allowedMimeTypes.includes(e.mimeType))
|
|
275
|
+
return {
|
|
276
|
+
valid: !1,
|
|
277
|
+
error: new o(`MIME type "${e.mimeType}" is not allowed`, r.INVALID_MIME_TYPE)
|
|
278
|
+
};
|
|
279
|
+
if (t.allowedExtensions && t.allowedExtensions.length > 0) {
|
|
280
|
+
let n = C(e.filename).toLowerCase();
|
|
281
|
+
if (!t.allowedExtensions.some((d) => d.toLowerCase() === n))
|
|
282
|
+
return {
|
|
283
|
+
valid: !1,
|
|
284
|
+
error: new o(`Extension "${n}" is not allowed`, r.INVALID_EXTENSION)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (t.validator)
|
|
288
|
+
try {
|
|
289
|
+
if (!await t.validator(e))
|
|
290
|
+
return {
|
|
291
|
+
valid: !1,
|
|
292
|
+
error: new o(`File "${e.filename}" failed custom validation`, r.VALIDATION_FAILED)
|
|
293
|
+
};
|
|
294
|
+
} catch (n) {
|
|
295
|
+
return {
|
|
296
|
+
valid: !1,
|
|
297
|
+
error: new o(
|
|
298
|
+
`Validator threw an error: ${n instanceof Error ? n.message : "Unknown error"}`,
|
|
299
|
+
r.VALIDATION_FAILED
|
|
300
|
+
)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return { valid: !0 };
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
async function $(i, e, t) {
|
|
307
|
+
return new T(e, t).parse(i);
|
|
308
|
+
}
|
|
309
|
+
// plugins/multipart/src/utils/errors.ts
|
|
310
|
+
function x(i, e) {
|
|
311
|
+
let t = {
|
|
312
|
+
code: e.code,
|
|
313
|
+
message: e.message,
|
|
314
|
+
...e.details !== void 0 ? { details: e.details } : {}
|
|
315
|
+
};
|
|
316
|
+
switch (e.code) {
|
|
317
|
+
case r.LIMIT_FILE_SIZE:
|
|
318
|
+
case r.LIMIT_FILE_COUNT:
|
|
319
|
+
case r.LIMIT_FIELD_SIZE:
|
|
320
|
+
case r.LIMIT_FIELD_COUNT:
|
|
321
|
+
case r.LIMIT_PARTS:
|
|
322
|
+
return i.status(413).json({ error: "PAYLOAD_TOO_LARGE", ...t });
|
|
323
|
+
case r.INVALID_MIME_TYPE:
|
|
324
|
+
case r.INVALID_EXTENSION:
|
|
325
|
+
return i.status(415).json({ error: "UNSUPPORTED_MEDIA_TYPE", ...t });
|
|
326
|
+
case r.VALIDATION_FAILED:
|
|
327
|
+
case r.FIELD_REQUIRED:
|
|
328
|
+
case r.FIELD_TOO_MANY_FILES:
|
|
329
|
+
return i.validationError({ error: "VALIDATION_FAILED", ...t });
|
|
330
|
+
default:
|
|
331
|
+
return i.badRequest({ error: "MULTIPART_ERROR", ...t });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// plugins/multipart/src/utils/log.ts
|
|
335
|
+
var b = {
|
|
336
|
+
reset: "\x1B[0m",
|
|
337
|
+
red: "\x1B[31m",
|
|
338
|
+
blue: "\x1B[34m"
|
|
339
|
+
};
|
|
340
|
+
function B(...i) {
|
|
341
|
+
process.env.DEBUG === "true" && console.log(`${b.blue}[AsterFlow Multipart]${b.reset}`, ...i);
|
|
342
|
+
}
|
|
343
|
+
function U(i, e) {
|
|
344
|
+
console.error(`${b.red}%s %s${b.reset}`, "[AsterFlow Multipart]", i), console.group(), console.error("Error:", e instanceof Error ? e.message : e), e instanceof Error && "code" in e && console.error("Code:", e.code), console.groupEnd();
|
|
345
|
+
}
|
|
346
|
+
// plugins/multipart/src/index.ts
|
|
347
|
+
function le(i) {
|
|
348
|
+
return {
|
|
349
|
+
body: i.fields,
|
|
350
|
+
files: i.files,
|
|
351
|
+
multipartMetadata: i.metadata,
|
|
352
|
+
getFile(e) {
|
|
353
|
+
return i.files.find((t) => t.fieldName === e);
|
|
354
|
+
},
|
|
355
|
+
getFiles(e) {
|
|
356
|
+
return e ? i.files.filter((t) => t.fieldName === e) : i.files;
|
|
357
|
+
},
|
|
358
|
+
hasFiles() {
|
|
359
|
+
return i.files.length > 0;
|
|
360
|
+
},
|
|
361
|
+
getFilesByType(e) {
|
|
362
|
+
return i.files.filter((t) => t.mimeType === e);
|
|
363
|
+
},
|
|
364
|
+
async saveAll(e) {
|
|
365
|
+
let t = [];
|
|
366
|
+
for (let n of i.files) {
|
|
367
|
+
let d = `${e}/${n.filename}`;
|
|
368
|
+
await n.save(d), t.push(d);
|
|
369
|
+
}
|
|
370
|
+
return t;
|
|
371
|
+
},
|
|
372
|
+
async cleanupMultipart() {
|
|
373
|
+
await Promise.all(i.files.map(async (e) => {
|
|
374
|
+
if (e.tempPath)
|
|
375
|
+
try {
|
|
376
|
+
await se(e.tempPath);
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
var ue = re.create({ name: "multipart" }).decorate("creator", "Ashu11-A").decorate("version", _).decorate("installed", A()).config(h).on("onRequest", async ({ request: i, response: e, router: t, plugin: n }) => {
|
|
384
|
+
let d = i.getMethod().toLowerCase(), c = t.route instanceof oe ? t.route.extensions.multipart : ne(t.route)?.[d]?.multipart, u = i.getHeaders()["content-type"];
|
|
385
|
+
if (!(!!u && u.toLowerCase().includes("multipart/form-data")))
|
|
386
|
+
return c ? x(e, new o(
|
|
387
|
+
"Expected a multipart/form-data request",
|
|
388
|
+
r.PARSE_ERROR
|
|
389
|
+
)) : void 0;
|
|
390
|
+
try {
|
|
391
|
+
let f = w(n.context, ae()), p = await $(i, f);
|
|
392
|
+
c && N(p, c), B("Multipart request parsed", {
|
|
393
|
+
Fields: String(p.metadata.fieldsCount),
|
|
394
|
+
Files: String(p.metadata.filesCount),
|
|
395
|
+
"Total Size": `${p.metadata.totalSize} bytes`,
|
|
396
|
+
"Processing Time": `${p.metadata.processingTime}ms`
|
|
397
|
+
}), i.extend(le(p));
|
|
398
|
+
} catch (f) {
|
|
399
|
+
let p = f instanceof o ? f : new o(f instanceof Error ? f.message : "Unknown error", r.PARSE_ERROR);
|
|
400
|
+
return U("Failed to parse multipart request", p), x(e, p);
|
|
401
|
+
}
|
|
402
|
+
}).on("onResponse", async ({ request: i }) => {
|
|
403
|
+
await i.cleanupMultipart?.();
|
|
404
|
+
}), Ue = ue;
|
|
405
|
+
export {
|
|
406
|
+
h as DEFAULT_CONFIG,
|
|
407
|
+
r as ErrorCodes,
|
|
408
|
+
o as MultipartError,
|
|
409
|
+
T as MultipartParser,
|
|
410
|
+
Ue as default,
|
|
411
|
+
x as errorResponse,
|
|
412
|
+
ue as multipartPlugin,
|
|
413
|
+
$ as parseMultipart,
|
|
414
|
+
w as resolveConfig,
|
|
415
|
+
N as validateFields
|
|
416
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type MultipartConfig, type MultipartResult, type ParserEvents } from '../types/multipart';
|
|
2
|
+
/**
|
|
3
|
+
* The minimal request shape the parser needs. `AsterRequest` satisfies this
|
|
4
|
+
* structurally, so no dependency on `@asterflow/request`'s generics is needed here.
|
|
5
|
+
*/
|
|
6
|
+
export interface MultipartRequestLike {
|
|
7
|
+
raw: unknown;
|
|
8
|
+
getHeaders(): Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export declare class MultipartParser {
|
|
11
|
+
private readonly config;
|
|
12
|
+
private readonly events;
|
|
13
|
+
constructor(config?: MultipartConfig, events?: ParserEvents);
|
|
14
|
+
parse(request: MultipartRequestLike): Promise<MultipartResult>;
|
|
15
|
+
private consumeFile;
|
|
16
|
+
private validateFile;
|
|
17
|
+
}
|
|
18
|
+
export declare function parseMultipart(request: MultipartRequestLike, config?: MultipartConfig, events?: ParserEvents): Promise<MultipartResult>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime half of `.multipart(schema)`, declared in `types/asterflow.d.ts`
|
|
3
|
+
* (declaration merging is types-only). Called from `index.ts`; its return
|
|
4
|
+
* value is threaded into `.decorate(...)` rather than called as a bare
|
|
5
|
+
* statement because Bun's transpiler dead-code-eliminates a side-effecting
|
|
6
|
+
* call whose result isn't otherwise used.
|
|
7
|
+
*/
|
|
8
|
+
export declare function installExtension(): true;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type MultipartFields, type MultipartResult } from '../types/multipart';
|
|
2
|
+
/** Validates parsed files against a route's declared `multipart` field criteria. Throws the first `MultipartError` found. */
|
|
3
|
+
export declare function validateFields(result: MultipartResult, schema: MultipartFields): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Plugin } from '@asterflow/plugin';
|
|
2
|
+
import './types/asterflow.d.ts';
|
|
3
|
+
import { type MultipartConfig } from './types/multipart';
|
|
4
|
+
export * from './controllers/MultipartParser';
|
|
5
|
+
export * from './controllers/validateMultipartFields';
|
|
6
|
+
export * from './types/inferRequest';
|
|
7
|
+
export * from './types/mime';
|
|
8
|
+
export * from './types/multipart';
|
|
9
|
+
export * from './utils/errors';
|
|
10
|
+
export declare const multipartPlugin: Plugin<{
|
|
11
|
+
decorate: {
|
|
12
|
+
creator: string;
|
|
13
|
+
version: string;
|
|
14
|
+
installed: true;
|
|
15
|
+
};
|
|
16
|
+
path: "multipart";
|
|
17
|
+
instance: import("asterflow").AnyAsterflow;
|
|
18
|
+
derive: {};
|
|
19
|
+
extension: {};
|
|
20
|
+
config: {
|
|
21
|
+
limits?: import("./index.ts").MultipartLimits | undefined;
|
|
22
|
+
fileHandling?: import("./index.ts").MultipartFileHandling | undefined;
|
|
23
|
+
validation?: import("./index.ts").MultipartValidation | undefined;
|
|
24
|
+
defaultConfig: MultipartConfig;
|
|
25
|
+
};
|
|
26
|
+
}>;
|
|
27
|
+
export default multipartPlugin;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DefaultMethodProps,
|
|
3
|
+
DefaultRouteMethodBuilderProps,
|
|
4
|
+
MergeProps,
|
|
5
|
+
MethodProps,
|
|
6
|
+
RouteMethodBuilderProps
|
|
7
|
+
} from '@asterflow/router'
|
|
8
|
+
import type { InferMultipartRequestFragment } from './inferRequest'
|
|
9
|
+
import type { MultipartFields, MultipartFile, MultipartResult } from './multipart'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ambient fallback for routes that don't call `.multipart(...)` - optional,
|
|
13
|
+
* since these only exist on `request` for an actual multipart request.
|
|
14
|
+
* Routes that do call `.multipart(...)` get the stricter, non-optional
|
|
15
|
+
* typing below instead, which wins for `getFile`/`getFiles`.
|
|
16
|
+
*/
|
|
17
|
+
declare module '@asterflow/request' {
|
|
18
|
+
interface AsterRequest {
|
|
19
|
+
/** Parsed form fields. Only set when the request was `multipart/form-data`. */
|
|
20
|
+
body?: Record<string, string | string[]>
|
|
21
|
+
/** All processed files from the request. Only set when it was `multipart/form-data`. */
|
|
22
|
+
files?: MultipartFile[]
|
|
23
|
+
/** Parsing metadata (timing, counts). Only set when it was `multipart/form-data`. */
|
|
24
|
+
multipartMetadata?: MultipartResult['metadata']
|
|
25
|
+
|
|
26
|
+
getFile?(fieldName: string): MultipartFile | undefined
|
|
27
|
+
getFiles?(fieldName?: string): MultipartFile[]
|
|
28
|
+
hasFiles?(): boolean
|
|
29
|
+
getFilesByType?(mimeType: string): MultipartFile[]
|
|
30
|
+
/** Saves every file into `directory`, keyed by original filename. Returns the written paths. */
|
|
31
|
+
saveAll?(directory: string): Promise<string[]>
|
|
32
|
+
/** Removes any temp files created on disk for this request. Safe to call more than once. */
|
|
33
|
+
cleanupMultipart?(): Promise<void>
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Adds `.multipart(schema)` onto `Method` and `RouteMethodBuilder` via
|
|
39
|
+
* declaration merging - the parameter list must match each class's real one
|
|
40
|
+
* exactly (count, constraints, defaults). No `const` modifier here: Bun's
|
|
41
|
+
* transpiler can't parse `const` type parameters inside a merged interface,
|
|
42
|
+
* even though `tsc` accepts it. Runtime implementation lives in
|
|
43
|
+
* `controllers/multipartExtension.ts`.
|
|
44
|
+
*/
|
|
45
|
+
declare module '@asterflow/router' {
|
|
46
|
+
interface Method<
|
|
47
|
+
Props extends MethodProps = DefaultMethodProps
|
|
48
|
+
> {
|
|
49
|
+
multipart<Fields extends MultipartFields>(
|
|
50
|
+
schema: Fields
|
|
51
|
+
): Method<MergeProps<Props, { requestExt: Props['requestExt'] & InferMultipartRequestFragment<Fields>, handler: Props['handler'] extends undefined ? undefined : any }>>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface RouteMethodBuilder<
|
|
55
|
+
Props extends RouteMethodBuilderProps = DefaultRouteMethodBuilderProps
|
|
56
|
+
> {
|
|
57
|
+
multipart<Fields extends MultipartFields>(
|
|
58
|
+
schema: Fields
|
|
59
|
+
): RouteMethodBuilder<MergeProps<Props, { requestExt: Props['requestExt'] & InferMultipartRequestFragment<Fields> }>>
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MultipartFieldCriteria, MultipartFields, MultipartFile } from './multipart';
|
|
2
|
+
type NarrowedFile<Criteria extends MultipartFieldCriteria> = Criteria['mimeTypes'] extends readonly (infer M extends string)[] ? Omit<MultipartFile, 'mimeType'> & {
|
|
3
|
+
mimeType: M;
|
|
4
|
+
} : MultipartFile;
|
|
5
|
+
type InferFieldFile<Criteria extends MultipartFieldCriteria> = Criteria['required'] extends true ? NarrowedFile<Criteria> : NarrowedFile<Criteria> | undefined;
|
|
6
|
+
type InferFieldFiles<Criteria extends MultipartFieldCriteria> = NarrowedFile<Criteria>[];
|
|
7
|
+
/**
|
|
8
|
+
* `request` extension for a route that called `.multipart(schema)` -
|
|
9
|
+
* `getFile`/`getFiles` keyed to the declared field names, schema-narrowed.
|
|
10
|
+
* `ExtendedRequest` (in `@asterflow/router`) omits the ambient untyped
|
|
11
|
+
* versions before intersecting this in, so this is the only signature
|
|
12
|
+
* available, not an extra overload.
|
|
13
|
+
*/
|
|
14
|
+
export type InferMultipartRequestFragment<Schema extends MultipartFields> = {
|
|
15
|
+
getFile<K extends keyof Schema & string>(field: K): InferFieldFile<Schema[K]>;
|
|
16
|
+
getFiles<K extends keyof Schema & string>(field: K): InferFieldFiles<Schema[K]>;
|
|
17
|
+
};
|
|
18
|
+
export {};
|