jeresig/pharos-images

View on GitHub
schemas/ImageImport.js

Summary

Maintainability
B
6 hrs
Test Coverage
"use strict";

const os = require("os");
const fs = require("fs");
const path = require("path");

const async = require("async");
const unzip = require("unzip2");

const models = require("../lib/models");
const urls = require("../lib/urls");
const config = require("../lib/config");

const Import = require("./Import");

const states = [
    {
        id: "started",
        name: (req) => req.gettext("Awaiting processing..."),
        advance(batch, callback) {
            batch.processImages(callback);
        },
    },
    {
        id: "process.started",
        name: (req) => req.gettext("Processing..."),
    },
    {
        id: "process.completed",
        name: (req) => req.gettext("Completed."),
        advance(batch, callback) {
            // NOTE(jeresig): Currently nothing needs to be done to finish
            // up the import, other than moving it to the "completed" state.
            process.nextTick(callback);
        },
    },
    {
        id: "completed",
        name: (req) => req.gettext("Completed."),
    },
];

const errors = {
    ERROR_READING_ZIP: (req) => req.gettext("Error opening zip file."),
    ZIP_FILE_EMPTY: (req) => req.gettext("Zip file has no images in it."),
    MALFORMED_IMAGE: (req) => req.gettext("There was an error processing " +
        "the image. Perhaps it is malformed in some way."),
    EMPTY_IMAGE: (req) => req.gettext("The image is empty."),
    NEW_VERSION: (req) => req.gettext("A new version of the image was " +
        "uploaded, replacing the old one."),
    TOO_SMALL: (req) => req.gettext("The image is too small to work with " +
        "the image similarity algorithm. It must be at least 150px on " +
        "each side."),
    ERROR_SAVING: (req) => req.gettext("Error saving image."),
};

const ImageImport = Import.extend({
    // The location of the uploaded zip file
    // (temporary, deleted after processing)
    zipFile: {
        type: String,
        required: true,
    },

    // The name of the original file (e.g. `foo.zip`)
    fileName: {
        type: String,
        required: true,
    },
});

Object.assign(ImageImport.methods, {
    getURL(lang) {
        return urls.gen(lang,
            `/source/${this.source}/admin?images=${this._id}`);
    },

    getError(req) {
        return models("ImageImport").getError(req, this.error);
    },

    getStates() {
        return states;
    },

    processImages(callback) {
        const zipFile = fs.createReadStream(this.zipFile);
        let zipError;
        const files = [];
        const extractDir = path.join(os.tmpdir(),
            (new Date).getTime().toString());

        fs.mkdir(extractDir, () => {
            zipFile.pipe(unzip.Parse()).on("entry", (entry) => {
                const fileName = path.basename(entry.path);
                const outFileName = path.join(extractDir, fileName);

                // Ignore things that aren't files (e.g. directories)
                // Ignore files that don't end with .jpe?g
                // Ignore files that start with '.'
                if (entry.type !== "File" ||
                        !/.+\.jpe?g$/i.test(fileName) ||
                        fileName.indexOf(".") === 0) {
                    return entry.autodrain();
                }

                // Don't attempt to add files that already exist
                if (files.indexOf(outFileName) >= 0) {
                    return entry.autodrain();
                }

                /* istanbul ignore if */
                if (config.NODE_ENV !== "test") {
                    console.log("Extracting:", path.basename(outFileName));
                }

                files.push(outFileName);
                entry.pipe(fs.createWriteStream(outFileName));
            })
            .on("error", function() {
                // Hack from this ticket to force the stream to close:
                // https://github.com/glebdmitriew/node-unzip-2/issues/8
                this._streamEnd = true;
                this._streamFinish = true;
                zipError = true;
            })
            .on("close", () => {
                if (zipError) {
                    return callback(new Error("ERROR_READING_ZIP"));
                }

                if (files.length === 0) {
                    return callback(new Error("ZIP_FILE_EMPTY"));
                }

                // Import all of the files as images
                async.eachLimit(files, 1, (file, callback) => {
                    this.addResult(file, callback);
                }, (err) => {
                    /* istanbul ignore if */
                    if (err) {
                        return callback(err);
                    }

                    this.setSimilarityState(callback);
                });
            });
        });
    },

    setSimilarityState(callback) {
        const Image = models("Image");

        Image.update(
            {batch: this._id},
            {needsSimilarIndex: true},
            {multi: true},
            (err) => {
                /* istanbul ignore if */
                if (err) {
                    return callback(err);
                }

                Image.update(
                    {batch: {$ne: this._id}},
                    {needsSimilarUpdate: true},
                    {multi: true},
                    callback
                );
            }
        );
    },

    addResult(file, callback) {
        /* istanbul ignore if */
        if (config.NODE_ENV !== "test") {
            console.log("Adding Image:", path.basename(file));
        }

        models("Image").fromFile(this, file, (err, image, warnings) => {
            const fileName = path.basename(file);

            const result = {
                _id: fileName,
                fileName,
            };

            if (err) {
                result.error = err.message;

            } else {
                result.warnings = warnings;
                result.model = image._id;
            }

            // Add the result
            this.results.push(result);

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

                    this.save(callback);
                });
            } else {
                this.save(callback);
            }
        });
    },

    getFilteredResults() {
        return {
            models: this.results.filter((result) => result.model),
            errors: this.results.filter((result) => result.error),
            warnings: this.results
                .filter((result) => (result.warnings || []).length !== 0),
        };
    },
});

Object.assign(ImageImport.statics, {
    fromFile(fileName, source) {
        const ImageImport = models("ImageImport");
        return new ImageImport({source, fileName});
    },

    getError(req, error) {
        const msg = errors[error];
        return msg ? msg(req) : error;
    },
});

module.exports = ImageImport;