index.js

/**
 * HTML5 Filesystem API
 * @module fs
 */

const utils = require('./utils');
const errors = require('./errors');
const root = require('./root');
const file = require('./file');
const directory = require('./directory');
const stat = require('./stat');
const quota = require('./quota');

/**
 * Is filesystem API supported by current browser
 *
 * @returns {Boolean}
 */
exports.isSupported = function () {
  return Boolean(window.webkitRequestFileSystem);
};

/**
 * Init filesystem
 *
 * @param {Object} [options]
 * @param {Number} [options.type=window.PERSISTENT] window.PERSISTENT | window.TEMPORARY
 * @param {Number} [options.bytes=1Mb]
 * @param {Boolean} [options.requestQuota=true] show request quota popup for PERSISTENT type.
 * (for Chrome extensions with `unlimitedStorage` permission it is useful to pass options.requestQuota = false)
 * @returns {Promise}
 */
exports.init = function (options = {}) {
  const type = Object.prototype.hasOwnProperty.call(options, 'type') ? options.type : window.PERSISTENT;
  const bytes = options.bytes || 1024 * 1024;
  assertType(type);
  const shouldRequestQuota = type === window.PERSISTENT
    ? (Object.prototype.hasOwnProperty.call(options, 'requestQuota') ? options.requestQuota : true)
    : false;
  return Promise.resolve()
    .then(() => shouldRequestQuota ? quota.requestPersistent(bytes) : bytes)
    // webkitRequestFileSystem always returns fs even if quota not granted
    .then(grantedBytes => utils.promiseCall(window, 'webkitRequestFileSystem', type, grantedBytes))
    .then(fs => {
      root.set(fs.root, type);
      return fs;
    });
};

/**
 * Gets used and granted bytes
 *
 * @returns {Promise<{usedBytes, grantedBytes}>}
 */
exports.usage = function () {
  return quota.usage(root.getType());
};

/**
 * Returns root directory
 *
 * @returns {FileSystemDirectoryEntry}
 */
exports.getRoot = function () {
  return root.get();
};

/**
 * Reads file content.
 * - `options.type='Blob'` returns a immutable snapshot of the file. Slower but safer.
 * - `options.type='File'` returns a mutable instance of {@link https://developer.mozilla.org/en-US/docs/Web/API/File File}. Faster but may have a data race.
 *
 * If file does not exist - error thrown.
 *
 * @param {String|FileSystemFileEntry} path
 * @param {Object} [options]
 * @param {String} [options.type='Text'] how content should be read: Text|ArrayBuffer|BinaryString|DataURL|Blob|File
 * @returns {Promise<String>}
 */
exports.readFile = function (path, options = {}) {
  return file.get(path)
    .then(fileEntry => file.read(fileEntry, options));
};

/**
 * Writes data to file.
 * If file does not exist - it will be created.
 * If file already exists - it will be overwritten.
 *
 * @param {String} path
 * @param {String|Blob|File|ArrayBuffer} data
 * @returns {Promise}
 */
exports.writeFile = function (path, data) {
  return file.get(path, {create: true, overwrite: true})
    .then(fileEntry => file.write(fileEntry, data, {append: false}));
};

/**
 * Appends data to file.
 * If file does not exist - it will be created.
 *
 * @param {String|FileSystemFileEntry} path
 * @param {String|Blob|File|ArrayBuffer} data
 * @returns {Promise}
 */
exports.appendFile = function (path, data) {
  return file.get(path, {create: true, overwrite: false})
    .then(fileEntry => file.write(fileEntry, data, {append: true}));
};

/**
 * Removes file.
 * If file does not exist - no error thrown.
 *
 * @param {String|FileSystemFileEntry} path
 * @returns {Promise}
 */
exports.unlink = function (path) {
  return file.get(path)
    .then(
      fileEntry => utils.promiseCall(fileEntry, 'remove'),
      e => errors.isNotFoundError(e)
        ? Promise.resolve(false)
        : Promise.reject(e)
    );
};

/**
 * Renames file or directory.
 * If source file or directory does not exist - error thrown.
 * If target already exists - it will be overwritten.
 *
 * @param {String|FileSystemEntry} oldPath
 * @param {String} newPath
 * @param {Object} [options]
 * @param {Boolean} [options.create=false] create missing directories
 * @returns {Promise<FileSystemEntry>}
 */
exports.rename = function (oldPath, newPath, options = {}) {
  return moveOrCopy(oldPath, newPath, 'moveTo', options);
};

/**
 * Copies file or entire directory.
 * If file or directory does not exist - error thrown.
 *
 * @param {String|FileSystemEntry} oldPath
 * @param {String} newPath
 * @param {Object} [options]
 * @param {Boolean} [options.create=false] create missing directories
 * @returns {Promise<FileSystemEntry>}
 */
exports.copy = function (oldPath, newPath, options = {}) {
  return moveOrCopy(oldPath, newPath, 'copyTo', options);
};

/**
 * Removes directory recursively.
 * If directory does not exist - method does nothing without error.
 *
 * @param {String|FileSystemDirectoryEntry} path
 * @returns {Promise}
 */
exports.rmdir = function (path) {
  return directory.get(path)
    .then(
      dir => dir === root.get()
        ? Promise.reject('Can not remove root. Use clear() to clear fs.')
        : utils.promiseCall(dir, 'removeRecursively'),
      e => errors.isNotFoundError(e)
        ? Promise.resolve(false)
        : Promise.reject(e)
    )
};

/**
 * Recursively creates required directories in provided path.
 *
 * @param {String} path
 * @returns {Promise<FileSystemDirectoryEntry>}
 */
exports.mkdir = function (path) {
  return directory.get(path, {create: true});
};

/**
 * Checks that file or directory exists by provided path.
 *
 * @param {String} path
 * @returns {Promise<Boolean>}
 */
exports.exists = function (path) {
  return exports.getEntry(path)
    .then(() => true, e => errors.isNotFoundError(e)
      ? false
      : Promise.reject(e)
    );
};

/**
 * Gets info about file or directory.
 * If it does not exist - error thrown.
 *
 * @param {String|FileSystemEntry} path
 * @returns {Promise<StatObject>}
 */
exports.stat = function (path) {
  return exports.getEntry(path)
    .then(entry => stat.get(entry));
};

/**
 * Reads directory content
 *
 * @param {String|FileSystemDirectoryEntry} path
 * @param {Object} [options]
 * @param {Boolean} [options.deep=false] read recursively and attach data as `children` property
 * @returns {Promise<Array<FileSystemEntry>>}
 */
exports.readdir = function (path, options = {}) {
  return directory.get(path)
    .then(dir => options.deep
      ? directory.readDeep(dir)
      : directory.read(dir)
    )
};

/**
 * Clears whole filesystem
 * @returns {Promise}
 */
exports.clear = function () {
  return exports.readdir('/')
    .then(entries => {
      const tasks = entries.map(entry => entry.isDirectory
        ? utils.promiseCall(entry, 'removeRecursively')
        : utils.promiseCall(entry, 'remove')
      );
      return Promise.all(tasks);
    });
};

/**
 * Gets URL for path
 *
 * @param {String|FileSystemEntry} path
 * @returns {String}
 */
exports.getUrl = function (path) {
  return exports.getEntry(path)
    .then(entry => entry.toURL())
};

/**
 * Gets file or directory
 *
 * @param {String|FileSystemEntry} path
 * @returns {Promise<FileSystemEntry>}
 */
exports.getEntry = function (path) {
  return file.get(path)
    .catch(e => errors.isTypeMismatchError(e)
      ? directory.get(path)
      : Promise.reject(e)
    );
};

/**
 * Create a {@link https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream ReadableStream} for reading
 *
 * @param {String|FileSystemFileEntry} path
 * @returns {ReadableStream}
 */
exports.createReadStream = function (path) {
  return file.createReadStream(path);
};

/**
 * Create a {@link https://developer.mozilla.org/en-US/docs/Web/API/WritableStream WritableStream} for writing
 *
 * @param {String|FileSystemFileEntry} path
 * @returns {WritableStream}
 */
exports.createWriteStream = function (path) {
  return file.createWriteStream(path);
};

function moveOrCopy(oldPath, newPath, method, options) {
  if (oldPath === newPath) { // runtyper-disable-line
    return Promise.resolve();
  }
  const {
    dirPath: newParentDirPath,
    fileName: newName,
  } = utils.parsePath(newPath);
  return Promise.all([
    exports.getEntry(oldPath),
    directory.get(newParentDirPath, options)
  ]).then(([enrty, newParent]) => {
    return utils.promiseCall(enrty, method, newParent, newName);
  });
}

function assertType(type) {
  if (type !== window.PERSISTENT && type !== window.TEMPORARY) {
    throw new Error(`Unknown storage type ${type}`);
  }
}