mongaku/mongaku

View on GitHub
src/logic/uploads.js

Summary

Maintainability
C
1 day
Test Coverage
const fs = require("fs");
const os = require("os");
const path = require("path");

const async = require("async");
const request = require("request");
const formidable = require("formidable");

const {cloneModel} = require("../lib/clone");
const models = require("../lib/models");
const options = require("../lib/options");

// The maximum number of times to try downloading an image
const MAX_ATTEMPTS = 3;

// How long to wait, in milliseconds, for the download
const DOWNLOAD_TIMEOUT = 10000;

module.exports = app => {
    const Upload = models("Upload");
    const UploadImage = models("UploadImage");
    const Source = models("Source");

    const genTmpFile = () =>
        path.join(os.tmpdir(), new Date().getTime().toString());

    const handleUpload = ({i18n, params, lang}, res, next) => (err, file) => {
        /* istanbul ignore if */
        if (err) {
            return next(err);
        }

        UploadImage.fromFile(file, (err, image) => {
            // TODO: Display better error message
            if (err) {
                return next(new Error(i18n.gettext("Error processing image.")));
            }

            Upload.fromImage(image, params.type, (err, upload) => {
                /* istanbul ignore if */
                if (err) {
                    return next(err);
                }

                image.updateSimilarity(err => {
                    /* istanbul ignore if */
                    if (err) {
                        return next(err);
                    }

                    image.save(err => {
                        /* istanbul ignore if */
                        if (err) {
                            return next(err);
                        }

                        // TODO: Add in uploader's user name (once those exist)
                        upload.updateSimilarity(() => {
                            upload.save(() =>
                                res.redirect(upload.getURL(lang)),
                            );
                        });
                    });
                });
            });
        });
    };

    const download = (imageURL, callback) => {
        let attemptNum = 0;

        const downloadImage = () => {
            attemptNum += 1;

            const tmpFile = genTmpFile();
            const outStream = fs.createWriteStream(tmpFile);

            outStream.on("close", () => callback(null, tmpFile));

            const stream = request({
                url: imageURL,
                timeout: DOWNLOAD_TIMEOUT,
            });

            stream.on("response", res => {
                if (res.statusCode === 200) {
                    return stream.pipe(outStream);
                }

                if (attemptNum < MAX_ATTEMPTS) {
                    downloadImage();
                } else {
                    callback(new Error("Error Downloading image."));
                }
            });
        };

        downloadImage();
    };

    return {
        urlUpload(req, res, next) {
            const {
                query: {url},
                i18n,
            } = req;

            // Handle the user accidentally hitting enter
            if (!url || url === "http://") {
                return next(new Error(i18n.gettext("No image URL specified.")));
            }

            download(url, (err, file) =>
                handleUpload(req, res, next)(err, file),
            );
        },

        fileUpload(req, res, next) {
            const {i18n} = req;
            const form = new formidable.IncomingForm();
            form.encoding = "utf-8";
            form.maxFieldsSize = options.maxUploadSize;

            form.parse(req, (err, fields, files) => {
                /* istanbul ignore if */
                if (err) {
                    return next(
                        new Error(i18n.gettext("Error processing upload.")),
                    );
                }

                if (
                    files &&
                    files.file &&
                    files.file.path &&
                    files.file.size > 0
                ) {
                    handleUpload(req, res, next)(null, files.file.path);
                } else {
                    next(new Error(i18n.gettext("No image specified.")));
                }
            });
        },

        show({params, i18n, user}, res) {
            // TODO: Update similar matches if new image data has
            // since come in since it was last updated.
            const _id = `uploads/${params.upload}`;
            Upload.findById(_id, (err, upload) => {
                if (err || !upload) {
                    return res.status(404).render("Error", {
                        title: i18n.gettext("Uploaded image not found."),
                    });
                }

                upload.loadImages(true, () => {
                    async.eachLimit(
                        upload.similarRecords,
                        4,
                        (similar, callback) => {
                            similar.recordModel.loadImages(false, callback);
                        },
                        () => {
                            // Filter out record matches from sources that the
                            // user isn't allowed to see
                            const similarRecords = upload.similarRecords.filter(
                                match => match.recordModel.canView(user),
                            );

                            const formattedSimilarRecords = similarRecords.map(
                                match => ({
                                    _id: match._id,
                                    score: match.score,
                                    recordModel: cloneModel(
                                        match.recordModel,
                                        i18n,
                                    ),
                                    imageModel: cloneModel(
                                        match.recordModel.images.find(
                                            image =>
                                                image._id === match.images[0],
                                        ),
                                        i18n,
                                    ),
                                }),
                            );

                            res.render("Upload", {
                                title: upload.getTitle(i18n),
                                similar: formattedSimilarRecords,
                                image: cloneModel(upload.images[0], i18n),
                                sources: Source.getSourcesByViewable(user).map(
                                    source => cloneModel(source, i18n),
                                ),
                                noIndex: true,
                            });
                        },
                    );
                });
            });
        },

        routes() {
            app.get("/:type/uploads/:upload", this.show);
            app.get("/:type/url-upload", this.urlUpload);
            app.post("/:type/file-upload", this.fileUpload);
        },
    };
};