1 | ;
|
2 | /*
|
3 | * @adonisjs/bodyparser
|
4 | *
|
5 | * (c) Harminder Virk <virk@adonisjs.com>
|
6 | *
|
7 | * For the full copyright and license information, please view the LICENSE
|
8 | * file that was distributed with this source code.
|
9 | */
|
10 | Object.defineProperty(exports, "__esModule", { value: true });
|
11 | exports.PartHandler = void 0;
|
12 | /// <reference path="../../adonis-typings/index.ts" />
|
13 | const path_1 = require("path");
|
14 | const utils_1 = require("@poppinss/utils");
|
15 | const File_1 = require("./File");
|
16 | const utils_2 = require("../utils");
|
17 | /**
|
18 | * Part handler handles the progress of a stream and also internally validates
|
19 | * it's size and extension.
|
20 | *
|
21 | * This class offloads the task of validating a file stream, regardless of how
|
22 | * the stream is consumed. For example:
|
23 | *
|
24 | * In classic scanerio, we will process the file stream and write files to the
|
25 | * tmp directory and in more advanced cases, the end user can handle the
|
26 | * stream by themselves and report each chunk to this class.
|
27 | */
|
28 | class PartHandler {
|
29 | constructor(part, options, drive) {
|
30 | this.part = part;
|
31 | this.options = options;
|
32 | this.drive = drive;
|
33 | /**
|
34 | * A boolean to know if we can use the magic number to detect the file type. This is how it
|
35 | * works.
|
36 | *
|
37 | * - We begin by extracting the file extension from the file name
|
38 | * - If the extension is something we support via magic numbers, then we ignore the extension
|
39 | * and inspect the buffer
|
40 | * - Otherwise, we have no other option than to trust the extension
|
41 | *
|
42 | * Think of this as using the optimal way for validating the file type
|
43 | */
|
44 | this.canFileTypeBeDetected = utils_2.supportMagicFileTypes.includes((0, path_1.extname)(this.part.filename).replace(/^\./, ''));
|
45 | /**
|
46 | * Creating a new file object for each part inside the multipart
|
47 | * form data
|
48 | */
|
49 | this.file = new File_1.File({
|
50 | clientName: this.part.filename,
|
51 | fieldName: this.part.name,
|
52 | headers: this.part.headers,
|
53 | }, {
|
54 | size: this.options.size,
|
55 | extnames: this.options.extnames,
|
56 | }, this.drive);
|
57 | /**
|
58 | * A boolean to know, if we have emitted the error event after one or
|
59 | * more validation errors. We need this flag, since the race conditions
|
60 | * between `data` and `error` events will trigger multiple `error`
|
61 | * emit.
|
62 | */
|
63 | this.emittedValidationError = false;
|
64 | }
|
65 | /**
|
66 | * Detects the file type and extension and also validates it when validations
|
67 | * are not deferred.
|
68 | */
|
69 | async detectFileTypeAndExtension(force) {
|
70 | if (!this.buff) {
|
71 | return;
|
72 | }
|
73 | const fileType = await (0, utils_2.getFileType)(this.buff, this.file.clientName, this.file.headers, force);
|
74 | if (fileType) {
|
75 | this.file.extname = fileType.ext;
|
76 | this.file.type = fileType.type;
|
77 | this.file.subtype = fileType.subtype;
|
78 | }
|
79 | }
|
80 | /**
|
81 | * Skip the stream or end it forcefully. This is invoked when the
|
82 | * streaming consumer reports an error
|
83 | */
|
84 | skipEndStream() {
|
85 | this.part.emit('close');
|
86 | }
|
87 | /**
|
88 | * Finish the process of listening for any more events and mark the
|
89 | * file state as consumed.
|
90 | */
|
91 | finish() {
|
92 | this.file.state = 'consumed';
|
93 | if (!this.options.deferValidations) {
|
94 | this.file.validate();
|
95 | }
|
96 | }
|
97 | /**
|
98 | * Start the process the updating the file state
|
99 | * to streaming mode.
|
100 | */
|
101 | begin() {
|
102 | this.file.state = 'streaming';
|
103 | }
|
104 | /**
|
105 | * Handles the file upload progress by validating the file size and
|
106 | * extension.
|
107 | */
|
108 | async reportProgress(line, bufferLength) {
|
109 | /**
|
110 | * Do not consume stream data when file state is not `streaming`. Stream
|
111 | * events race conditions may emit the `data` event after the `error`
|
112 | * event in some cases, so we have to restrict it here.
|
113 | */
|
114 | if (this.file.state !== 'streaming') {
|
115 | return;
|
116 | }
|
117 | /**
|
118 | * Detect the file type and extension when extname is null, otherwise
|
119 | * empty out the buffer. We only need the buffer to find the
|
120 | * file extension from it's content.
|
121 | */
|
122 | if (this.file.extname === undefined) {
|
123 | this.buff = this.buff ? Buffer.concat([this.buff, line]) : line;
|
124 | await this.detectFileTypeAndExtension(false);
|
125 | }
|
126 | else {
|
127 | this.buff = undefined;
|
128 | }
|
129 | /**
|
130 | * The length of stream buffer
|
131 | */
|
132 | this.file.size = this.file.size + bufferLength;
|
133 | /**
|
134 | * Validate the file on every chunk, unless validations have been deferred.
|
135 | */
|
136 | if (this.options.deferValidations) {
|
137 | return;
|
138 | }
|
139 | /**
|
140 | * Attempt to validate the file after every chunk and report error
|
141 | * when it has one or more failures. After this the consumer must
|
142 | * call `reportError`.
|
143 | */
|
144 | this.file.validate();
|
145 | if (!this.file.isValid && !this.emittedValidationError) {
|
146 | this.emittedValidationError = true;
|
147 | this.part.emit('error', new utils_1.Exception('one or more validations failed', 400, 'E_STREAM_VALIDATION_FAILURE'));
|
148 | }
|
149 | }
|
150 | /**
|
151 | * Report errors encountered while processing the stream. These can be errors
|
152 | * apart from the one reported by this class. For example: The `s3` failure
|
153 | * due to some bad credentails.
|
154 | */
|
155 | async reportError(error) {
|
156 | if (this.file.state !== 'streaming') {
|
157 | return;
|
158 | }
|
159 | this.skipEndStream();
|
160 | this.finish();
|
161 | if (error.code === 'E_STREAM_VALIDATION_FAILURE') {
|
162 | return;
|
163 | }
|
164 | /**
|
165 | * Push to the array of file errors
|
166 | */
|
167 | this.file.errors.push({
|
168 | fieldName: this.file.fieldName,
|
169 | clientName: this.file.clientName,
|
170 | type: 'fatal',
|
171 | message: error.message,
|
172 | });
|
173 | }
|
174 | /**
|
175 | * Report success data about the file.
|
176 | */
|
177 | async reportSuccess(data) {
|
178 | if (this.file.state !== 'streaming') {
|
179 | return;
|
180 | }
|
181 | /**
|
182 | * Re-attempt to detect the file extension after we are done
|
183 | * consuming the stream
|
184 | */
|
185 | if (this.file.extname === undefined) {
|
186 | await this.detectFileTypeAndExtension(this.canFileTypeBeDetected ? false : true);
|
187 | }
|
188 | if (data) {
|
189 | const { filePath, tmpPath, ...meta } = data;
|
190 | if (filePath) {
|
191 | this.file.filePath = filePath;
|
192 | }
|
193 | if (tmpPath) {
|
194 | this.file.tmpPath = tmpPath;
|
195 | }
|
196 | this.file.meta = meta || {};
|
197 | }
|
198 | this.finish();
|
199 | }
|
200 | }
|
201 | exports.PartHandler = PartHandler;
|