"use strict";
/*
 * @adonisjs/bodyparser
 *
 * (c) Harminder Virk <virk@adonisjs.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Multipart = void 0;
/// <reference path="../../adonis-typings/bodyparser.ts" />
const bytes_1 = __importDefault(require("bytes"));
const utils_1 = require("@poppinss/utils");
const multiparty_1 = __importDefault(require("@poppinss/multiparty"));
const FormFields_1 = require("../FormFields");
const PartHandler_1 = require("./PartHandler");
/**
 * Multipart class offers a low level API to interact the incoming
 * HTTP request data as a stream. This makes it super easy to
 * write files to s3 without saving them to the disk first.
 */
class Multipart {
    constructor(ctx, config = {}, drive) {
        this.ctx = ctx;
        this.config = config;
        this.drive = drive;
        /**
         * The registered handlers to handle the file uploads
         */
        this.handlers = {};
        /**
         * Collected fields from the multipart stream
         */
        this.fields = new FormFields_1.FormFields({
            convertEmptyStringsToNull: this.config.convertEmptyStringsToNull === true,
        });
        /**
         * Collected files from the multipart stream. Files are only collected
         * when there is an attached listener for a given file.
         */
        this.files = new FormFields_1.FormFields({
            convertEmptyStringsToNull: this.config.convertEmptyStringsToNull === true,
        });
        /**
         * We track the finishing of `this.onFile` async handlers
         * to make sure that `process` promise resolves for all
         * handlers to finish.
         */
        this.pendingHandlers = 0;
        /**
         * A track of total number of file bytes processed so far
         */
        this.processedBytes = 0;
        /**
         * The current state of the multipart form handler
         */
        this.state = 'idle';
    }
    /**
     * Returns a boolean telling whether all streams have been
     * consumed along with all handlers execution
     */
    isClosed() {
        return this.form['flushing'] <= 0 && this.pendingHandlers <= 0;
    }
    /**
     * Removes array like expression from the part name to
     * find the handler
     */
    getHandlerName(name) {
        return name.replace(/\[\d*\]/, '');
    }
    /**
     * Validates and returns an error when upper limit is defined and
     * processed bytes is over the upper limit
     */
    validateProcessedBytes(chunkLength) {
        if (!this.upperLimit) {
            return;
        }
        this.processedBytes += chunkLength;
        if (this.processedBytes > this.upperLimit) {
            return new utils_1.Exception('request entity too large', 413, 'E_REQUEST_ENTITY_TOO_LARGE');
        }
    }
    /**
     * Handles a given part by invoking it's handler or
     * by resuming the part, if there is no defined
     * handler
     */
    async handlePart(part) {
        /**
         * Skip parts with empty name or empty filenames. The empty
         * filenames takes place when user doesn't upload a file
         * and empty name is more of a bad client scanerio.
         */
        if (!part.name || !part.filename) {
            part.resume();
            return;
        }
        const name = this.getHandlerName(part.name);
        /**
         * Skip, if their is no handler to consume the part.
         */
        const handler = this.handlers[name] || this.handlers['*'];
        if (!handler) {
            part.resume();
            return;
        }
        this.pendingHandlers++;
        /**
         * Instantiate the part handler
         */
        const partHandler = new PartHandler_1.PartHandler(part, handler.options, this.drive);
        partHandler.begin();
        /**
         * Track the file instance created by the part handler. The end user
         * must be able to access these files.
         */
        this.files.add(partHandler.file.fieldName, partHandler.file);
        part.file = partHandler.file;
        try {
            const response = await handler.handler(part, async (line) => {
                if (this.state !== 'processing') {
                    return;
                }
                const lineLength = line.length;
                /**
                 * Keeping an eye on total bytes processed so far and shortcircuit
                 * request when more than expected bytes have been received.
                 */
                const error = this.validateProcessedBytes(lineLength);
                if (error) {
                    part.emit('error', error);
                    this.abort(error);
                    return;
                }
                try {
                    await partHandler.reportProgress(line, lineLength);
                }
                catch (err) {
                    this.ctx.logger.fatal('Unhandled multipart stream error. Make sure to handle "error" events for all manually processed streams');
                }
            });
            /**
             * Stream consumed successfully
             */
            await partHandler.reportSuccess(response || {});
        }
        catch (error) {
            /**
             * The stream handler reported an exception
             */
            await partHandler.reportError(error);
        }
        this.pendingHandlers--;
    }
    /**
     * Record the fields inside multipart contract
     */
    handleField(key, value) {
        if (!key) {
            return;
        }
        this.fields.add(key, value);
    }
    /**
     * Processes the user config and computes the `upperLimit` value from
     * it.
     */
    processConfig(config) {
        this.config = Object.assign(this.config, config);
        /**
         * Getting bytes from the `config.fieldsLimit` option, which can
         * also be a string.
         */
        this.maxFieldsSize =
            typeof this.config.fieldsLimit === 'string'
                ? (0, bytes_1.default)(this.config.fieldsLimit)
                : this.config.fieldsLimit;
        /**
         * Getting bytes from the `config.limit` option, which can
         * also be a string
         */
        this.upperLimit =
            typeof this.config.limit === 'string' ? (0, bytes_1.default)(this.config.limit) : this.config.limit;
    }
    /**
     * Mark the process as finished
     */
    finish(newState) {
        if (this.state === 'idle' || this.state === 'processing') {
            this.state = newState;
            this.ctx.request['__raw_files'] = this.files.get();
            this.ctx.request.setInitialBody(this.fields.get());
        }
    }
    /**
     * Attach handler for a given file. To handle all files, you
     * can attach a wildcard handler.
     *
     * @example
     * ```ts
     * multipart.onFile('package', {}, async (stream) => {
     * })
     *
     * multipart.onFile('*', {}, async (stream) => {
     * })
     * ```
     */
    onFile(name, options, handler) {
        this.handlers[name] = { handler, options };
        return this;
    }
    /**
     * Abort request by emitting error
     */
    abort(error) {
        this.form.emit('error', error);
    }
    /**
     * Process the request by going all the file and field
     * streams.
     */
    process(config) {
        return new Promise((resolve, reject) => {
            if (this.state !== 'idle') {
                reject(new utils_1.Exception('multipart stream has already been consumed', 500, 'E_RUNTIME_EXCEPTION'));
                return;
            }
            this.state = 'processing';
            this.processConfig(config);
            this.form = new multiparty_1.default.Form({
                maxFields: this.config.maxFields,
                maxFieldsSize: this.maxFieldsSize,
            });
            /**
             * Raise error when form encounters an
             * error
             */
            this.form.on('error', (error) => {
                this.finish('error');
                process.nextTick(() => {
                    if (this.ctx.request.request.readable) {
                        this.ctx.request.request.resume();
                    }
                    if (error.message.match(/stream ended unexpectedly/)) {
                        reject(new utils_1.Exception('Invalid multipart request', 400, 'E_INVALID_MULTIPART_REQUEST'));
                    }
                    else if (error.message.match(/maxFields [0-9]+ exceeded/)) {
                        reject(new utils_1.Exception('Fields length limit exceeded', 413, 'E_REQUEST_ENTITY_TOO_LARGE'));
                    }
                    else if (error.message.match(/maxFieldsSize [0-9]+ exceeded/)) {
                        reject(new utils_1.Exception('Fields size in bytes exceeded', 413, 'E_REQUEST_ENTITY_TOO_LARGE'));
                    }
                    else {
                        reject(error);
                    }
                });
            });
            /**
             * Process each part at a time and also resolve the
             * promise when all parts are consumed and processed
             * by their handlers
             */
            this.form.on('part', async (part) => {
                await this.handlePart(part);
                /**
                 * When a stream finishes before the handler, the close `event`
                 * will not resolve the current Promise. So in that case, we
                 * check and resolve from here
                 */
                if (this.isClosed()) {
                    this.finish('success');
                    resolve();
                }
            });
            /**
             * Listen for fields
             */
            this.form.on('field', (key, value) => {
                try {
                    this.handleField(key, value);
                }
                catch (error) {
                    this.abort(error);
                }
            });
            /**
             * Resolve promise on close, when all internal
             * file handlers are done processing files
             */
            this.form.on('close', () => {
                if (this.isClosed()) {
                    this.finish('success');
                    resolve();
                }
            });
            this.form.parse(this.ctx.request.request);
        });
    }
}
exports.Multipart = Multipart;
