index.js

/** @namespace Unifile */
'use strict';

/**
 * The built-in Node.js WritableStream class
 * @external WritableStream
 * @see https://nodejs.org/api/stream.html#stream_writable_streams
 */

/**
 * The built-in Node.js ReadableStream class
 * @external ReadableStream
 * @see https://nodejs.org/api/stream.html#stream_readable_streams
 */

/**
 * Bluebird Promise class
 * @external Promise
 * @see http://bluebirdjs.com/docs/api-reference.html
 */

/**
 * State of the connector
 * @typedef {Object} ConnectorState
 * @property {boolean} isLoggedIn - Flag wether the user is logged in.
 * @property {boolean} isOAuth - Flag wether the connector uses OAuth as authentication mechanism.
 * @property {string} username - Name used to log in.
 */

/**
 * Static infos of the connector
 * @typedef {Object} ConnectorStaticInfos
 * @property {string} name - ID of the connector. This will be use to select the connector in unifile.
 * @property {string} displayName - Name that should be display. Allows characters forbidden in name.
 * @property {string} icon - Path to an icon for this connector.
 * @property {string} description - Description of the connector.
 */

/**
 * Representation of a connector infos
 * @typedef {Object} ConnectorInfos
 * @todo Use ConnectorState and ConnectorStaticInfos docs
 * @property {string} name - ID of the connector. This will be use to select the connector in unifile.
 * @property {string} displayName - Name that should be display. Allows characters forbidden in name.
 * @property {string} icon - Path to an icon for this connector.
 * @property {string} description - Description of the connector.
 * @property {boolean} isLoggedIn - Flag wether the user is logged in.
 * @property {boolean} isOAuth - Flag wether the connector uses OAuth as authentication mechanism.
 * @property {string} username - Name used to log in.
 */

/**
 * Credentials of a service
 * @typedef {Object} Credentials
 *
 * For non-OAuth services
 * @property {string} [host] - URL to the service
 * @property {string} [port] - Port the auth service is listening to
 * @property {string} [user] - Username for the service
 * @property {string} [password] - Password for the service
 *
 * For OAuth services
 * @property {string} [code] - OAuth code for the service
 * @property {string} [state] - OAuth state for the service
 */

/**
 * Representation of a file
 * @typedef {Object} FileInfos
 * @property {string} name - Name of the file
 * @property {number} size - Size of the file in bytes
 * @property {string} modified - ISO string representation of the date from last modification
 * @property {boolean} isDir - Wether this is a directory or not
 * @property {string} mime - MIME type of this file
 */

/**
 * Tells if a method needs authentification
 * @param {string} methodName - Name of the method to test
 * @return {boolean} true if the method needs to be authenticated
 * @private
 */
function isAuthentifiedFunction(methodName) {
  return ['readdir', 'mkdir', 'writeFile', 'createWriteStream',
    'readFile', 'createReadStream', 'rename', 'unlink', 'rmdir',
    'stat', 'batch'].indexOf(methodName) > -1;
}

/**
 * Unifile class
 * This will use connectors to distant services to manipulate the files.
 * An empty instance of Unifile cannot connect to any service. You must first call the use() function
 * to register a connector.
 */
class Unifile {

  /**
   * Create a new instance of Unifile.
   * This will regroup all the connectors you decided to use.
   * @constructor
   */
  constructor() {
    this.connectors = new Map();
  }

  /**
   * Adds a new connector into Unifile.
   * Once a connector has been register with this function, it can be used with all the commands.
   * @param {Connector} connector - A connector implementing all of Unifile functions
   */
  use(connector) {
    if(!connector) throw new Error('Connector cannot be undefined');
    if(!connector.name) throw new Error('Connector must have a name');
    this.connectors.set(connector.name.toLowerCase(), connector);
  }

  // Infos commands

  /**
   * Get all the info you need about a connector
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @return {ConnectorInfos} all the infos about this connector
   */
  getInfos(session, connectorName) {
    return this.callMethod(connectorName, session, 'getInfos');
  }

  /**
   * List all the connectors currently used in this instance of Unifile
   * @return {string[]} an array of connectors names
   */
  listConnectors() {
    return Array.from(this.connectors.keys());
  }

  // Auth commands

  /**
   * Log a connector in a distant service.
   * This must be called before any access to the service or an error will be thrown.
   * The result of a successful login attempt will be saved in the session.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {Credentials|string} credentials - Service credentials (user/password or OAuth code)
   *  or a authenticated URL to connect to the service.
   * @return {external:Promise<string|null>} a promise of OAuth token if the service uses it or null
   */
  login(session, connectorName, credentials) {
    return this.callMethod(connectorName, session, 'login', credentials);
  }

  /**
   * Log a connector by directly using a OAuth token.
   * You don't have to call the method if you use the login() method. This is only in the case
   * you got a token from anothe source (CLI, app,...)
   * This must be called before any access to the service or an error will be thrown.
   * The result of a successful login attempt will be saved in the session.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} token - Service access token generated by OAuth
   * @return {external:Promise<string|null>} a promise of OAuth token if the service uses it or null
   */
  setAccessToken(session, connectorName, token) {
    return this.callMethod(connectorName, session, 'setAccessToken', token);
  }

  /**
   * Log out from a connector.
   * After that you won't be able to make any request until you log in again.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @return {external:Promise<null>} an empty promise.
   */
  clearAccessToken(session, connectorName) {
    return this.callMethod(connectorName, session, 'clearAccessToken');
  }

  /**
   * Get the URL of the authorization endpoint for an OAuth service.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @return {external:Promise<string>} a promise of the authorization URL
   */
  getAuthorizeURL(session, connectorName) {
    return this.callMethod(connectorName, session, 'getAuthorizeURL');
  }

  // Filesystem commands

  /**
   * Reads the content of a directory.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} path - Path of the directory to read. Must be relative to the root of the service.
   * @return {external:Promise<FileInfos[]>} a promise of an array of FileInfos
   * @see {@link FileInfos} to get the properties of the return objects
   */
  readdir(session, connectorName, path) {
    return this.callMethod(connectorName, session, 'readdir', path);
  }

  /**
   * Give information about a file or directory.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} path - Path of the object to stat. Must be relative to the root of the service.
   * @return {external:Promise<FileInfos>} a promise of FileInfos
   * @see {@link FileInfos} to get the properties of the return object
   */
  stat(session, connectorName, path) {
    return this.callMethod(connectorName, session, 'stat', path);
  }

  /**
   * Create a directory.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} path - Path of the directory to create. Must be relative to the root of the service.
   * @return {external:Promise<null>} an empty promise
   */
  mkdir(session, connectorName, path) {
    return this.callMethod(connectorName, session, 'mkdir', path);
  }

  /**
   * Write content to a file.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} path - Path of the file to write. Must be relative to the root of the service.
   * @param {string} content - Content to write into the file
   * @return {external:Promise<null>} an empty promise.
   */
  writeFile(session, connectorName, path, content) {
    return this.callMethod(connectorName, session, 'writeFile', path, content);
  }

  /**
   * Create a write stream to a file.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} path - Path of the file to write. Must be relative to the root of the service.
   * @return {external:WritableStream} a writable stream into the file
   */
  createWriteStream(session, connectorName, path) {
    return this.callMethod(connectorName, session, 'createWriteStream', path);
  }

  /**
   * Read the content of the file.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} path - Path of the file to read. Must be relative to the root of the service.
   * @return {external:Promise<string>} a promise of the content of the file
   */
  readFile(session, connectorName, path) {
    return this.callMethod(connectorName, session, 'readFile', path);
  }

  /**
   * Create a read stream to a file.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} path - Path of the file to read. Must be relative to the root of the service.
   * @return {external:ReadableStream} a readable stream from the file
   */
  createReadStream(session, connectorName, path) {
    return this.callMethod(connectorName, session, 'createReadStream', path);
  }

  /**
   * Rename a file.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} source - Path to the file to rename. Must be relative to the root of the service.
   * @param {string} destination - New path to give to the file. Must be relative to the root of the service.
   * @return {external:Promise<null>} an empty promise.
   */
  rename(session, connectorName, source, destination) {
    return this.callMethod(connectorName, session, 'rename', source, destination);
  }

  /**
   * Unlink (delete) a file.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} path - Path of the file to delete. Must be relative to the root of the service.
   * @return {external:Promise<null>} an empty promise.
   */
  unlink(session, connectorName, path) {
    return this.callMethod(connectorName, session, 'unlink', path);
  }

  /**
   * Remove a directory.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {string} path - Path of the directory to delete. Must be relative to the root of the service.
   * @return {external:Promise<null>} an empty promise.
   */
  rmdir(session, connectorName, path) {
    return this.callMethod(connectorName, session, 'rmdir', path);
  }

  // Batch operation
  /**
   * Execute batch operation.
   * Available actions are UNLINK, RMDIR, RENAME, MKDIR and WRITEFILE.
   * @param {Object} session - Object where session data will be stored
   * @param {string} connectorName - Name of the connector
   * @param {Object[]} actions - Array of actions to execute in this batch.
   * @param {string} actions[].name - Name of this action.
   * @param {string} actions[].path - Path parameter for this action.
   * @param {string} [actions[].destination] - Destination parameter for this action.
   * @param {string} [actions[].content] - Content parameter for this action.
   * @param {string} [message] - Message to describe this batch
   * @return {external:Promise<null>} an empty promise.
   */
  batch(session, connectorName, actions, message) {
    return this.callMethod(connectorName, session, 'batch', actions, message);
  }

  // Privates

  callMethod(connectorName, session, methodName, ...params) {
    // Check connector
    if(!connectorName) throw new Error('You should specify a connector name!');
    const name = connectorName.toLowerCase();
    if(!this.connectors.has(name)) throw new Error(`Unknown connector: ${connectorName}`);
    const connector = this.connectors.get(name);
    if(!(methodName in connector)) throw new Error(`This connector does not implement ${methodName}()`);

    // Check session
    if(!session) session = {name: {}};
    else if(!(name in session)) session[name] = {};

    // Check authentification
    if(isAuthentifiedFunction(methodName) && !connector.getInfos(session[name]).isLoggedIn)
      return Promise.reject('User not logged in yet. You need to call the login() first.');

    //console.log(`Calling ${methodName} on ${connectorName} with ${params}`);
    return connector[methodName](session[name], ...params);
  }
}

// Register out-of-the-box plugins
Unifile.GitHubConnector = require('./unifile-github.js');
Unifile.DropboxConnector = require('./unifile-dropbox.js');
Unifile.FtpConnector = require('./unifile-ftp.js');
Unifile.WebDavConnector = require('./unifile-webdav.js');
Unifile.RemoteStorageConnector = require('./unifile-remoteStorage.js');
Unifile.FsConnector = require('./unifile-fs.js');
Unifile.SftpConnector = require('./unifile-sftp.js');

module.exports = Unifile;