UNPKG

10.1 kBJavaScriptView Raw
1// src/multipart/file.ts
2import { join } from "node:path";
3import { Exception } from "@poppinss/utils";
4import Macroable from "@poppinss/macroable";
5
6// src/helpers.ts
7import mediaTyper from "media-typer";
8import { dirname, extname } from "node:path";
9import { RuntimeException } from "@poppinss/utils";
10import { fileTypeFromBuffer, supportedExtensions } from "file-type";
11import { access, mkdir, copyFile, unlink, rename } from "node:fs/promises";
12var supportMagicFileTypes = supportedExtensions;
13function parseMimeType(mime) {
14 try {
15 const { type, subtype } = mediaTyper.parse(mime);
16 return { type, subtype };
17 } catch (error) {
18 return null;
19 }
20}
21async function getFileType(fileContents) {
22 const magicType = await fileTypeFromBuffer(fileContents);
23 if (magicType) {
24 return Object.assign({ ext: magicType.ext }, parseMimeType(magicType.mime));
25 }
26 return null;
27}
28function computeFileTypeFromName(clientName, headers) {
29 return Object.assign(
30 { ext: extname(clientName).replace(/^\./, "") },
31 parseMimeType(headers["content-type"])
32 );
33}
34async function pathExists(filePath) {
35 try {
36 await access(filePath);
37 return true;
38 } catch {
39 return false;
40 }
41}
42async function moveFile(sourcePath, destinationPath, options = { overwrite: true }) {
43 if (!sourcePath || !destinationPath) {
44 throw new RuntimeException('"sourcePath" and "destinationPath" required');
45 }
46 if (!options.overwrite && await pathExists(destinationPath)) {
47 throw new RuntimeException(`The destination file already exists: "${destinationPath}"`);
48 }
49 await mkdir(dirname(destinationPath), {
50 recursive: true,
51 mode: options.directoryMode
52 });
53 try {
54 await rename(sourcePath, destinationPath);
55 } catch (error) {
56 if (error.code === "EXDEV") {
57 await copyFile(sourcePath, destinationPath);
58 await unlink(sourcePath);
59 } else {
60 throw error;
61 }
62 }
63}
64
65// src/multipart/validators/size.ts
66import bytes from "bytes";
67var SizeValidator = class {
68 #file;
69 #maximumAllowedLimit;
70 #bytesLimit = 0;
71 validated = false;
72 /**
73 * Defining the maximum bytes the file can have
74 */
75 get maxLimit() {
76 return this.#maximumAllowedLimit;
77 }
78 set maxLimit(limit) {
79 if (this.#maximumAllowedLimit !== void 0) {
80 throw new Error("Cannot reset sizeLimit after file has been validated");
81 }
82 this.validated = false;
83 this.#maximumAllowedLimit = limit;
84 if (this.#maximumAllowedLimit) {
85 this.#bytesLimit = typeof this.#maximumAllowedLimit === "string" ? bytes(this.#maximumAllowedLimit) : this.#maximumAllowedLimit;
86 }
87 }
88 constructor(file) {
89 this.#file = file;
90 }
91 /**
92 * Reporting error to the file
93 */
94 #reportError() {
95 this.#file.errors.push({
96 fieldName: this.#file.fieldName,
97 clientName: this.#file.clientName,
98 message: `File size should be less than ${bytes(this.#bytesLimit)}`,
99 type: "size"
100 });
101 }
102 /**
103 * Validating file size while it is getting streamed. We only mark
104 * the file as `validated` when it's validation fails. Otherwise
105 * we keep re-validating the file as we receive more data.
106 */
107 #validateWhenGettingStreamed() {
108 if (this.#file.size > this.#bytesLimit) {
109 this.validated = true;
110 this.#reportError();
111 }
112 }
113 /**
114 * We have the final file size after the stream has been consumed. At this
115 * stage we always mark `validated = true`.
116 */
117 #validateAfterConsumed() {
118 this.validated = true;
119 if (this.#file.size > this.#bytesLimit) {
120 this.#reportError();
121 }
122 }
123 /**
124 * Validate the file size
125 */
126 validate() {
127 if (this.validated) {
128 return;
129 }
130 if (this.#maximumAllowedLimit === void 0) {
131 this.validated = true;
132 return;
133 }
134 if (this.#file.state === "streaming") {
135 this.#validateWhenGettingStreamed();
136 return;
137 }
138 if (this.#file.state === "consumed") {
139 this.#validateAfterConsumed();
140 return;
141 }
142 }
143};
144
145// src/multipart/validators/extensions.ts
146var ExtensionValidator = class {
147 #file;
148 #allowedExtensions = [];
149 validated = false;
150 /**
151 * Update the expected file extensions
152 */
153 get extensions() {
154 return this.#allowedExtensions;
155 }
156 set extensions(extnames) {
157 if (this.#allowedExtensions && this.#allowedExtensions.length) {
158 throw new Error("Cannot update allowed extension names after file has been validated");
159 }
160 this.validated = false;
161 this.#allowedExtensions = extnames;
162 }
163 constructor(file) {
164 this.#file = file;
165 }
166 /**
167 * Report error to the file
168 */
169 #reportError() {
170 const suffix = this.#allowedExtensions.length === 1 ? "is" : "are";
171 const message = [
172 `Invalid file extension ${this.#file.extname}.`,
173 `Only ${this.#allowedExtensions.join(", ")} ${suffix} allowed`
174 ].join(" ");
175 this.#file.errors.push({
176 fieldName: this.#file.fieldName,
177 clientName: this.#file.clientName,
178 message,
179 type: "extname"
180 });
181 }
182 /**
183 * Validating the file in the streaming mode. During this mode
184 * we defer the validation, until we get the file extname.
185 */
186 #validateWhenGettingStreamed() {
187 if (!this.#file.extname) {
188 return;
189 }
190 this.validated = true;
191 if (this.#allowedExtensions.includes(this.#file.extname)) {
192 return;
193 }
194 this.#reportError();
195 }
196 /**
197 * Validate the file extension after it has been streamed
198 */
199 #validateAfterConsumed() {
200 this.validated = true;
201 if (this.#allowedExtensions.includes(this.#file.extname || "")) {
202 return;
203 }
204 this.#reportError();
205 }
206 /**
207 * Validate the file
208 */
209 validate() {
210 if (this.validated) {
211 return;
212 }
213 if (!Array.isArray(this.#allowedExtensions) || this.#allowedExtensions.length === 0) {
214 this.validated = true;
215 return;
216 }
217 if (this.#file.state === "streaming") {
218 this.#validateWhenGettingStreamed();
219 return;
220 }
221 if (this.#file.state === "consumed") {
222 this.#validateAfterConsumed();
223 }
224 }
225};
226
227// src/multipart/file.ts
228var MultipartFile = class extends Macroable {
229 /**
230 * File validators
231 */
232 #sizeValidator = new SizeValidator(this);
233 #extensionValidator = new ExtensionValidator(this);
234 /**
235 * A boolean to know if file is an instance of this class
236 * or not
237 */
238 isMultipartFile = true;
239 /**
240 * Field name is the name of the field
241 */
242 fieldName;
243 /**
244 * Client name is the file name on the user client
245 */
246 clientName;
247 /**
248 * The headers sent as part of the multipart request
249 */
250 headers;
251 /**
252 * File size in bytes
253 */
254 size = 0;
255 /**
256 * The extname for the file.
257 */
258 extname;
259 /**
260 * Upload errors
261 */
262 errors = [];
263 /**
264 * Type and subtype are extracted from the `content-type`
265 * header or from the file magic number
266 */
267 type;
268 subtype;
269 /**
270 * File path is only set after the move operation
271 */
272 filePath;
273 /**
274 * File name is only set after the move operation. It is the relative
275 * path of the moved file
276 */
277 fileName;
278 /**
279 * Tmp path, only exists when file is uploaded using the
280 * classic mode.
281 */
282 tmpPath;
283 /**
284 * The file meta data
285 */
286 meta = {};
287 /**
288 * The state of the file
289 */
290 state = "idle";
291 /**
292 * Whether or not the validations have been executed
293 */
294 get validated() {
295 return this.#sizeValidator.validated && this.#extensionValidator.validated;
296 }
297 /**
298 * A boolean to know if file has one or more errors
299 */
300 get isValid() {
301 return this.errors.length === 0;
302 }
303 /**
304 * Opposite of [[this.isValid]]
305 */
306 get hasErrors() {
307 return !this.isValid;
308 }
309 /**
310 * The maximum file size limit
311 */
312 get sizeLimit() {
313 return this.#sizeValidator.maxLimit;
314 }
315 set sizeLimit(limit) {
316 this.#sizeValidator.maxLimit = limit;
317 }
318 /**
319 * Extensions allowed
320 */
321 get allowedExtensions() {
322 return this.#extensionValidator.extensions;
323 }
324 set allowedExtensions(extensions) {
325 this.#extensionValidator.extensions = extensions;
326 }
327 constructor(data, validationOptions) {
328 super();
329 this.sizeLimit = validationOptions.size;
330 this.allowedExtensions = validationOptions.extnames;
331 this.fieldName = data.fieldName;
332 this.clientName = data.clientName;
333 this.headers = data.headers;
334 }
335 /**
336 * Validate the file
337 */
338 validate() {
339 this.#extensionValidator.validate();
340 this.#sizeValidator.validate();
341 }
342 /**
343 * Mark file as moved
344 */
345 markAsMoved(fileName, filePath) {
346 this.filePath = filePath;
347 this.fileName = fileName;
348 this.state = "moved";
349 }
350 /**
351 * Moves the file to a given location. Multiple calls to the `move` method are allowed,
352 * incase you want to move a file to multiple locations.
353 */
354 async move(location, options) {
355 if (!this.tmpPath) {
356 throw new Exception('property "tmpPath" must be set on the file before moving it', {
357 status: 500,
358 code: "E_MISSING_FILE_TMP_PATH"
359 });
360 }
361 options = Object.assign({ name: this.clientName, overwrite: true }, options);
362 const filePath = join(location, options.name);
363 try {
364 await moveFile(this.tmpPath, filePath, { overwrite: options.overwrite });
365 this.markAsMoved(options.name, filePath);
366 } catch (error) {
367 if (error.message.includes("destination file already")) {
368 throw new Exception(
369 `"${options.name}" already exists at "${location}". Set "overwrite = true" to overwrite it`
370 );
371 }
372 throw error;
373 }
374 }
375 /**
376 * Returns file JSON representation
377 */
378 toJSON() {
379 return {
380 fieldName: this.fieldName,
381 clientName: this.clientName,
382 size: this.size,
383 filePath: this.filePath,
384 fileName: this.fileName,
385 type: this.type,
386 extname: this.extname,
387 subtype: this.subtype,
388 state: this.state,
389 isValid: this.isValid,
390 validated: this.validated,
391 errors: this.errors,
392 meta: this.meta
393 };
394 }
395};
396
397export {
398 supportMagicFileTypes,
399 getFileType,
400 computeFileTypeFromName,
401 MultipartFile
402};
403//# sourceMappingURL=chunk-NRCULWNL.js.map
\No newline at end of file