OpenVeo server API for plugins

API Docs for: 3.0.0
Show:

File: lib/fileSystem.js

'use strict';

/**
 * Provides functions to interact with the file system as an extension to the Node.js filesystem module.
 *
 * @module fileSystem
 * @class fileSystem
 * @main fileSystem
 */

var fs = require('fs');
var path = require('path');
var tar = require('tar');

/**
 * Creates a directory recursively and asynchronously.
 *
 * If parent directories do not exist, they will be automatically created.
 *
 * @method mkdirRecursive
 * @private
 * @async
 * @param {String} directoryPath The directory system path to create
 * @param {Function} callback The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 */
function mkdirRecursive(directoryPath, callback) {
  directoryPath = path.resolve(directoryPath);

  // Try to create directory
  fs.mkdir(directoryPath, function(error) {

    if (error && error.code === 'EEXIST') {

      // Can't create directory it already exists
      // It may have been created by another loop
      callback();

    } else if (error && error.code === 'ENOENT') {

      // Can't create directory, parent directory does not exist

      // Create parent directory
      mkdirRecursive(path.dirname(directoryPath), function(error) {
        if (!error) {

          // Now that parent directory is created, create requested directory
          fs.mkdir(directoryPath, function(error) {
            if (error && error.code === 'EEXIST') {

              // Can't create directory it already exists
              // It may have been created by another loop
              callback();

            } else
              callback(error);
          });

        } else
          callback(error);
      });
    } else
      callback(error);
  });
}

/**
 * Removes a directory and all its content recursively and asynchronously.
 *
 * It is assumed that the directory exists.
 *
 * @method rmdirRecursive
 * @private
 * @async
 * @param {String} directoryPath Path of the directory to remove
 * @param {Function} callback The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 */
function rmdirRecursive(directoryPath, callback) {

  // Open directory
  fs.readdir(directoryPath, function(error, resources) {

    // Failed reading directory
    if (error)
      return callback(error);

    var pendingResourceNumber = resources.length;

    // No more pending resources, done for this directory
    if (!pendingResourceNumber) {

      // Remove directory
      fs.rmdir(directoryPath, callback);

    }

    // Iterate through the list of resources in the directory
    resources.forEach(function(resource) {

      var resourcePath = path.join(directoryPath, resource);

      // Get resource stats
      fs.stat(resourcePath, function(error, stats) {
        if (error)
          return callback(error);

        // Resource correspond to a directory
        if (stats.isDirectory()) {

          resources = rmdirRecursive(path.join(directoryPath, resource), function(error) {
            if (error)
              return callback(error);

            pendingResourceNumber--;

            if (!pendingResourceNumber)
              fs.rmdir(directoryPath, callback);

          });

        } else {

          // Resource does not correspond to a directory
          // Mark resource as treated

          // Remove file
          fs.unlink(resourcePath, function(error) {
            if (error)
              return callback(error);
            else {
              pendingResourceNumber--;

              if (!pendingResourceNumber)
                fs.rmdir(directoryPath, callback);

            }
          });

        }

      });

    });

  });

}

/**
 * Copies a file.
 *
 * If directory does not exist it will be automatically created.
 *
 * @method copyFile
 * @private
 * @async
 * @param {String} sourceFilePath Path of the file
 * @param {String} destinationFilePath Final path of the file
 * @param {Function} callback The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 */
function copyFile(sourceFilePath, destinationFilePath, callback) {
  var onError = function(error) {
    callback(error);
  };

  var safecopy = function(sourceFilePath, destinationFilePath, callback) {
    if (sourceFilePath && destinationFilePath && callback) {
      try {
        var is = fs.createReadStream(sourceFilePath);
        var os = fs.createWriteStream(destinationFilePath);

        is.on('error', onError);
        os.on('error', onError);

        is.on('end', function() {
          os.end();
        });

        os.on('finish', function() {
          callback();
        });

        is.pipe(os);
      } catch (e) {
        callback(new Error(e.message));
      }
    } else callback(new Error('File path not defined'));
  };

  var pathDir = path.dirname(destinationFilePath);

  this.mkdir(pathDir,
    function(error) {
      if (error) callback(error);
      else safecopy(sourceFilePath, destinationFilePath, callback);
    }
  );
}

/**
 * Extracts a tar file to the given directory.
 *
 * @method extract
 * @async
 * @param {String} filePath Path of the file to extract
 * @param {String} destinationPath Path of the directory where to
 * extract files
 * @param {Function} [callback] The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 */
module.exports.extract = function(filePath, destinationPath, callback) {
  var extractTimeout;
  var streamError;

  callback = callback || function(error) {
    if (error)
      process.logger.error('Extract error', {error: error});
    else
      process.logger.silly(filePath + ' extracted into ' + destinationPath);
  };

  if (filePath && destinationPath) {

    // Prepare the extractor with destination path
    var extractor = tar.Extract(
      {
        path: path.normalize(destinationPath)
      }
    );

    var onError = function(error) {
      if (extractTimeout)
        clearTimeout(extractTimeout);

      streamError = error;
      extractor.end();
    };

    // Handle extraction end
    extractor.on('end', function() {
      if (extractTimeout)
        clearTimeout(extractTimeout);

      callback(streamError);
    });

    var tarFileReadableStream = fs.createReadStream(path.normalize(filePath));

    // Handle errors
    tarFileReadableStream.on('error', onError);
    extractor.on('error', onError);

    // Listen to readable stream close event
    tarFileReadableStream.on('close', function(chunk) {

      // In case of a broken archive, the readable stream close event is dispatched but not the close event of the
      // writable stream, wait for 10 seconds and dispatch an error if writable stream is still not closed
      extractTimeout = setTimeout(onError, 10000, new Error('Unexpected end of archive'));

    });

    // Extract file
    tarFileReadableStream.pipe(extractor);

  } else
    callback(new TypeError('Invalid filePath and / or destinationPath, expected strings'));
};

/**
 * Copies a file or a directory.
 *
 * @method copy
 * @async
 * @param {String} sourcePath Path of the source to copy
 * @param {String} destinationSourcePath Final path of the source
 * @param {Function} callback The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 */
module.exports.copy = function(sourcePath, destinationSourcePath, callback) {
  var self = this;

  // Get source stats to test if this is a directory or a file
  fs.stat(sourcePath, function(error, stats) {
    if (error)
      return callback(error);

    if (stats.isDirectory()) {

      // Resource is a directory

      // Open directory
      fs.readdir(sourcePath, function(error, resources) {

        // Failed reading directory
        if (error)
          return callback(error);

        var pendingResourceNumber = resources.length;

        // Directory is empty, create it and leave
        if (!pendingResourceNumber) {
          self.mkdir(destinationSourcePath, callback);
          return;
        }

        // Iterate through the list of resources in the directory
        resources.forEach(function(resource) {
          var resourcePath = path.join(sourcePath, resource);
          var resourceDestinationPath = path.join(destinationSourcePath, resource);

          // Copy resource
          self.copy(resourcePath, resourceDestinationPath, function(error) {
            if (error)
              return callback(error);

            pendingResourceNumber--;

            if (!pendingResourceNumber)
              callback();
          });
        });

      });

    } else {

      // Resource is a file
      copyFile.call(self, sourcePath, destinationSourcePath, callback);

    }

  });

};

/**
 * Gets a JSON file content.
 *
 * This will verify that the file exists first.
 *
 * @method getJSONFileContent
 * @async
 * @param {String} filePath The path of the file to read
 * @param {Function} callback The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **String** The file content or null if an error occurred
 * @throws {TypeError} An error if callback is not speficied
 */
module.exports.getJSONFileContent = function(filePath, callback) {
  if (!filePath)
    return callback(new TypeError('Invalid file path, expected a string'));

  // Check if file exists
  fs.exists(filePath, function(exists) {

    if (exists) {

      // Read file content
      fs.readFile(filePath, {
        encoding: 'utf8'
      },
      function(error, data) {
        if (error) {
          callback(error);
        } else {
          var dataAsJson;
          try {

            // Try to parse file data as JSON content
            dataAsJson = JSON.parse(data);

          } catch (e) {
            callback(new Error(e.message));
          }

          callback(null, dataAsJson);
        }
      });
    } else
      callback(new Error('Missing file ' + filePath));

  });
};

/**
 * Creates a directory.
 *
 * If parent directory does not exist, it will be automatically created.
 * If directory already exists, it won't do anything.
 *
 * @method mkdir
 * @async
 * @param {String} directoryPath The directory system path to create
 * @param {Function} [callback] The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 */
module.exports.mkdir = function(directoryPath, callback) {
  callback = callback || function(error) {
    if (error)
      process.logger.error('mkdir error', {error: error});
    else
      process.logger.silly(directoryPath + ' directory created');
  };

  if (!directoryPath)
    return callback(new TypeError('Invalid directory path, expected a string'));

  fs.exists(directoryPath, function(exists) {
    if (exists)
      callback();
    else
      mkdirRecursive(directoryPath, callback);
  });
};

/**
 * Removes a directory and all its content recursively and asynchronously.
 *
 * @method rmdir
 * @async
 * @param {String} directoryPath Path of the directory to remove
 * @param {Function} [callback] The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 */
module.exports.rmdir = function(directoryPath, callback) {
  callback = callback || function(error) {
    if (error)
      process.logger.error('rmdir error', {error: error});
    else
      process.logger.silly(directoryPath + ' directory removed');
  };

  if (!directoryPath)
    return callback(new TypeError('Invalid directory path, expected a string'));

  fs.exists(directoryPath, function(exists) {
    if (!exists)
      callback();
    else
      rmdirRecursive(directoryPath, callback);
  });
};

/**
 * Gets OpenVeo configuration directory path.
 *
 * OpenVeo configuration is stored in user home directory.
 *
 * @method getConfDir
 * @return {String} OpenVeo configuration directory path
 */
module.exports.getConfDir = function() {
  var env = process.env;
  var home = env.HOME;

  if (process.platform === 'win32')
    home = env.USERPROFILE || env.HOMEDRIVE + env.HOMEPATH || home || '';

  return path.join(home, '.openveo');
};