unifile-fs.js

'use strict';

const Promise = require('bluebird');
const Mime = require('mime');
const Fs = Promise.promisifyAll(require('fs'), {suffix: 'Promised'});
const Path = require('path');

const Tools = require('unifile-common-tools');

const NAME = 'fs';

const validatePath = Symbol('validatePath');

function statToFileInfos(filename, stat) {
	const isDir = stat.isDirectory();
	return {
		size: stat.size,
		modified: stat.mtime,
		name: filename,
		isDir: isDir,
		mime: isDir ? 'application/directory' : Mime.getType(filename)
	};
}

/**
 * Service connector for the local filesystem.
 */
class FsConnector {
	/**
   * @constructor
   * @param {Object} config - Configuration object
   * @param {string|Array<string>} [config.sandbox] - Restrict connector access to this path (if string)
   * or these paths (if array)
   * @param {string} [config.rootPath] - Path against all relative paths will be resolved.
   * Default to the first sandbox path if given or /.
   * @param {boolean} [config.showHiddenFiles=false] - Flag to show hidden files.
   * @param {ConnectorStaticInfos} [config.infos] - Connector infos to override
   * @throws {Error} Invalid sandbox path.
   */
	constructor(config) {
		const conf = config || {};
		this.showHiddenFile = conf.showHiddenFile;
		this.infos = Tools.mergeInfos(conf.infos, {
			name: NAME,
			displayName: 'Your Computer',
			icon: '',
			description: 'Edit files on your local drive.'
		});
		this.name = this.infos.name;

		if(!conf.sandbox) this.sandbox = [];
		else if(conf.sandbox.constructor === String) this.sandbox = [conf.sandbox];
		else if(Array.isArray(conf.sandbox)) this.sandbox = conf.sandbox;
		else throw new Error('Invalid sandbox path. Must be a string or an array');

		if(conf.rootPath) this.rootPath = conf.rootPath;
		else if(this.sandbox.length > 0) this.rootPath = this.sandbox[0];
		else this.rootPath = '/';
	}

	getInfos(session) {
		return Object.assign({
			isLoggedIn: true,
			isOAuth: false,
			username: process.env.USER
		}, this.infos);
	}

	// Auth methods are useless here

	getAuthorizeURL(session) {
		return Promise.resolve('');
	}

	setAccessToken(session, token) {
		return Promise.resolve(token);
	}

	clearAccessToken(session) {
		return Promise.resolve();
	}

	login(session, loginInfos) {
		return new Promise.resolve();
	}

	//Filesystem commands

	readdir(session, path) {
		try {
			const securePath = this[validatePath](path);
			return Fs.readdirPromised(securePath)
			.reduce((memo, entry) => {
				if(this.showHiddenFile || entry.charAt(0) != '.') {
					return Fs.lstatPromised(Path.resolve(securePath, entry))
					.then((stat) => {
						memo.push(statToFileInfos(entry, stat));
						return memo;
					});
				} else {
					return memo;
				}
			}, []);
		} catch (e) {
			return Promise.reject(e);
		}
	}

	stat(session, path) {
		try {
			const securePath = this[validatePath](path);
			return Fs.statPromised(securePath)
			.then((stat) => {
				return statToFileInfos(Path.basename(securePath), stat);
			});
		} catch (e) {
			return Promise.reject(e);
		}
	}

	mkdir(session, path) {
		try {
			return Fs.mkdirPromised(this[validatePath](path));
		} catch (e) {
			return Promise.reject(e);
		}
	}

	writeFile(session, path, data) {
		try {
			return Fs.writeFilePromised(this[validatePath](path), data);
		} catch (e) {
			return Promise.reject(e);
		}
	}

	createWriteStream(session, path) {
		return Fs.createWriteStream(this[validatePath](path));
	}

	readFile(session, path) {
		try {
			return Fs.readFilePromised(this[validatePath](path));
		} catch (e) {
			return Promise.reject(e);
		}
	}

	createReadStream(session, path) {
		return Fs.createReadStream(this[validatePath](path), {encoding: 'utf8'});
	}

	rename(session, src, dest) {
		try {
			return Fs.renamePromised(this[validatePath](src), this[validatePath](dest));
		} catch (e) {
			return Promise.reject(e);
		}
	}

	unlink(session, path) {
		try {
			return Fs.unlinkPromised(this[validatePath](path));
		} catch (e) {
			return Promise.reject(e);
		}
	}

	rmdir(session, path) {
		try {
			return Fs.rmdirPromised(this[validatePath](path));
		} catch (e) {
			return Promise.reject(e);
		}
	}

	batch(session, actions, message) {
		return Tools.simpleBatch(this, session, actions);
	}

	/**
   * Ensure the given path is in the user-defined sandbox
   * @private
   */
	[validatePath](path) {
		const absolutePath = Path.resolve(this.rootPath, path);
		if(this.sandbox.length !== 0 && !this.sandbox.some((sandboxPath) => absolutePath.startsWith(sandboxPath)))
			throw new Error(`Path is out of the sandbox: ${absolutePath}`);
		return absolutePath;
	}
}

module.exports = FsConnector;