eexit/ghost-storage-cloudinary

View on GitHub
index.js

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
'use strict';

require('bluebird');

const StorageBase = require('ghost-storage-base'),
    path = require('path'),
    errors = require(path.join(__dirname, 'errors')),
    debug = require('@tryghost/debug')('adapter'),
    cloudinary = require('cloudinary').v2,
    got = require('got'),
    plugins = require(path.join(__dirname, '/plugins'));

class CloudinaryAdapter extends StorageBase {

    /**
     *  @override
     */
    constructor(options) {
        super(options);

        const config = options || {},
            auth = config.auth || config,
            // Kept to avoid a BCB with forked repo
            legacy = config.configuration || {},
            uploadOptions = config.upload || legacy.file || {},
            fetchOptions = config.fetch || legacy.image || {};

        this.useDatedFolder = config.useDatedFolder || false;
        this.plugins = config.plugins || {};
        this.uploaderOptions = {
            upload: uploadOptions,
            fetch: fetchOptions
        };

        debug('constructor:useDatedFolder:', this.useDatedFolder);
        debug('constructor:uploaderOptions:', this.uploaderOptions);
        debug('constructor:plugins:', this.plugins);

        cloudinary.config(auth);
    }

    /**
     *  @override
     */
    exists(filename) {
        debug('exists:filename', filename);

        const pubId = this.toCloudinaryId(filename);

        return new Promise((resolve) => cloudinary.uploader.explicit(pubId, { type: 'upload' }, (err) => {
            if (err) {
                return resolve(false);
            }
            return resolve(true);
        }));
    }

    /**
     *  @override
     */
    save(image) {
        // Creates a deep clone of Cloudinary options
        const uploaderOptions = JSON.parse(JSON.stringify(Object.assign({}, this.uploaderOptions)));

        // Forces the Cloudinary Public ID value based on the file name when upload option
        // "use_filename" is set to true.
        if (uploaderOptions.upload.use_filename !== 'undefined' && uploaderOptions.upload.use_filename) {
            Object.assign(
                uploaderOptions.upload,
                { public_id: path.parse(this.getSanitizedFileName(image.name)).name }
            );
        }

        // Appends the dated folder if enabled
        if (this.useDatedFolder) {
            uploaderOptions.upload.folder = this.getTargetDir(uploaderOptions.upload.folder);
        }

        debug('save:uploadOptions:', uploaderOptions);

        // Retinizes images if there is any config provided
        if (this.plugins.retinajs) {
            const retinajs = new plugins.RetinaJS(this.uploader, uploaderOptions, this.plugins.retinajs);
            return retinajs.retinize(image);
        }

        return this.uploader(image.path, uploaderOptions, true);
    }

    /**
     *  Uploads an image with options to Cloudinary
     *  @param {string} imagePath The image path to upload (local or remote)
     *  @param {object} options Cloudinary upload + fetch options
     *  @param {boolean} url If true, will do an extra Cloudinary API call to fetch the uploaded image with fetch options
     *  @return {Promise} The uploader Promise
     */
    uploader(imagePath, options, url) {
        debug('uploader:imagePath', imagePath);
        debug('uploader:options', options);
        debug('uploader:url', url);

        return new Promise((resolve, reject) => cloudinary.uploader.upload(imagePath, options.upload, (err, res) => {
            if (err) {
                debug('cloudinary.uploader:error', err);

                return reject(new errors.CloudinaryAdapterError({
                    err: err,
                    message: `Could not upload image ${imagePath}`
                }));
            }

            debug('cloudinary.uploader:res', res);

            if (url) {
                return resolve(cloudinary.url(res.public_id.concat('.', res.format), options.fetch));
            }
            return resolve();
        }));
    }

    /**
     *  @override
     */
    serve() {
        return (req, res, next) => {
            next();
        };
    }

    /**
     *  @override
     */
    delete(filename) {
        debug('delete:filename', filename);

        const pubId = this.toCloudinaryId(filename);

        return new Promise((resolve, reject) => cloudinary.uploader.destroy(pubId, (err, res) => {
            if (err) {
                debug('delete:error', err);

                return reject(new errors.CloudinaryAdapterError({
                    err: err,
                    message: `Could not delete image ${filename}`
                }));
            }
            return resolve(res);
        }));
    }

    /**
     *  @override
     */
    read(options) {
        const opts = options || {};

        debug('read:opts', opts);

        return new Promise(async (resolve, reject) => {
            try {
                return resolve(await got(opts.path, {
                    responseType: 'buffer',
                    resolveBodyOnly: true
                }));
            } catch (err) {

                debug('read:error', err);

                return reject(new errors.CloudinaryAdapterError({
                    err: err,
                    message: `Could not read image ${opts.path}`
                }));
            }
        });
    }

    /**
     *  Extracts the a Cloudinary-ready file name for API usage.
     *  If a "folder" upload option is set, it will prepend its
     *  value.
     *  @param {string} filename The wanted image filename
     *  @return {string} Cloudinary-ready image name
     */
    toCloudinaryFile(filename) {
        const file = path.parse(filename).base;
        if (typeof this.uploaderOptions.upload.folder !== 'undefined') {
            return path.join(this.uploaderOptions.upload.folder, file);
        }
        return file;
    }

    /**
     *  Returns the Cloudinary public ID off a given filename
     *  @param {string} filename The wanted image filename
     *  @return {string} Cloudinary-ready ID for given image
     */
    toCloudinaryId(filename) {
        const parsed = path.parse(this.toCloudinaryFile(filename));
        return path.join(parsed.dir, parsed.name);
    }
}

module.exports = CloudinaryAdapter;