OpenVeo server API for plugins

API Docs for: 7.0.0
Show:

File: lib/middlewares/imageProcessorMiddleware.js

'use strict';

/**
 * @module middlewares
 */

var fs = require('fs');
var path = require('path');
var util = process.requireApi('lib/util.js');
var fileSystem = process.requireApi('lib/fileSystem.js');
var imageProcessor = process.requireApi('lib/imageProcessor.js');

/**
 * Defines an expressJS middleware to process images.
 *
 *     var openVeoApi = require('@openveo/api');
 *     expressApp.use('/mount-path', openVeoApi.middlewares.imageProcessorMiddleware(
 *       '/path/to/the/folder/containing/images'
 *       '/path/to/the/folder/containing/processed/images/cache'
 *       [
 *         {
 *           id: 'my-thumb', // Id of the style to apply when requesting an image processing
 *           width: 200, // Expected width (in px) of the image
 *           quality: 50 // Expected quality from 0 to 100 (default to 90 with 100 the best)
 *         }
 *       ]
 *     ));
 *
 *     // Then it's possible to apply style "my-thumb" to the image /mount-path/my-image.jpg using
 *     // parameter "style": /mount-path/my-image.jpg?style=my-thumb
 *
 * If path corresponds to an image with a parameter "style", the style is applied to the image before returning it to
 * the client. Actually only one type of manipulation can be applied to an image: generate a thumbnail.
 * If path does not correspond to an image the processor is skipped.
 *
 * @class imageProcessorMiddleware
 * @constructor
 * @static
 * @param {String} imagesDirectory The path of the directory containing the images to process
 * @param {String} cacheDirectory The path of the directory containing already processed images (for cache purpose)
 * @param {Array} styles The list of available styles to process images with for each style:
 *   - [String] **id** Id of the style to apply when requesting an image processing
 *   - [Number] **[width]** Expected width of the image (in px) (default to 10)
 *   - [Number] **[quality]** Expected quality from 0 to 100 (default to 90 with 100 the best)
 * @param {Object} headers The name / value list of headers to send with the image when responding to the client
 * @return {Function} The ExpressJS middleware
 * @throws {TypeError} If imagesdirectory or styles is empty
 */
module.exports = function(imagesDirectory, cacheDirectory, styles, headers) {
  if (!imagesDirectory) throw new TypeError('Missing imagesDirectory parameter');
  if (!styles || !styles.length) throw new TypeError('Missing styles');

  headers = headers || {};

  /**
   * Fetches a style by its id from the list of styles.
   *
   * @param {String} id The id of the style to fetch
   * @return {Object} The style description object
   */
  function fetchStyle(id) {
    for (var i = 0; i < styles.length; i++)
      if (styles[i].id === id) return styles[i];
  }

  return function(request, response, next) {
    var styleId = request.query && request.query.style;

    // No style or no file name: skip processor
    if (!styleId) return next();

    // File path without query parameters and absolute system file path
    var filePath = decodeURI(request.url.replace(/\?.*/, ''));
    var fileSystemPath = path.join(imagesDirectory, filePath);
    var isImage;

    /**
     * Sends an image to client as response.
     *
     * @param {String} imagePath The absolute image path
     */
    function sendFile(imagePath) {
      response.set(headers);
      response.download(imagePath, request.query.filename);
    }

    // Get information about the requested file
    fs.stat(fileSystemPath, function(error, stats) {

      // Error or not a file: skip image processing
      if (error || !stats.isFile()) return next();

      // Verify that file is an image
      var readable = fs.createReadStream(path.normalize(fileSystemPath), {start: 0, end: 300});
      readable.on('data', function(imageChunk) {
        try {
          util.shallowValidateObject({
            file: imageChunk
          }, {
            file: {type: 'file', required: true, in: [
              fileSystem.FILE_TYPES.JPG,
              fileSystem.FILE_TYPES.PNG,
              fileSystem.FILE_TYPES.GIF
            ]}
          });
          isImage = true;
        } catch (e) {
          isImage = false;
        }
      });

      readable.on('end', function() {

        // File is not an image: skip image processing
        if (!isImage) return next();

        var imageCachePath = path.join(cacheDirectory, styleId, filePath);

        // Find out if file has already been processed
        fs.stat(imageCachePath, function(error, stats) {

          // File was found in cache
          if (stats && stats.isFile())
            return sendFile(imageCachePath);

          // Apply style (only thumb is available right now)
          var style = fetchStyle(styleId);

          if (!style) return next();
          if (!style.width && !style.height) return next();

          // Create cache directory if it does not exist
          fileSystem.mkdir(path.dirname(imageCachePath), function(error) {
            if (error) return next(error);

            imageProcessor.generateThumbnail(
              fileSystemPath,
              imageCachePath,
              style.width,
              style.height,
              style.crop,
              style.quality,
              function(error) {
                if (error) return next(error);

                sendFile(imageCachePath);
              }
            );

          });

        });
      });
    });
  };
};