// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-storage // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; // Globalization var g = require('strong-globalize')(); /** * File system based on storage provider */ var fs = require('fs'), path = require('path'), stream = require('stream'), async = require('async'), File = require('./file').File, Container = require('./container').Container; var utils = require('./../../utils'); module.exports.storage = module.exports; // To make it consistent with pkgcloud module.exports.File = File; module.exports.Container = Container; module.exports.Client = FileSystemProvider; module.exports.createClient = function(options) { return new FileSystemProvider(options); }; function FileSystemProvider(options) { options = options || {}; if (!path.isAbsolute(options.root)) { var basePath = path.dirname(path.dirname(require.main.filename)); options.root = path.join(basePath, options.root); } this.root = options.root; var exists = fs.existsSync(this.root); if (!exists) { throw new Error(g.f('{{FileSystemProvider}}: Path does not exist: %s', this.root)); } var stat = fs.statSync(this.root); if (!stat.isDirectory()) { throw new Error(g.f('{{FileSystemProvider}}: Invalid directory: %s', this.root)); } } var namePattern = new RegExp('[^' + path.sep + '/]+'); // To detect any file/directory containing dotdot paths var containsDotDotPaths = /(^|[\\\/])\.\.([\\\/]|$)/; function validateName(name, cb) { if (!name || containsDotDotPaths.test(name)) { cb && process.nextTick(cb.bind(null, new Error(g.f('Invalid name: %s', name)))); if (!cb) { console.error(g.f('{{FileSystemProvider}}: Invalid name: %s', name)); } return false; } var match = namePattern.exec(name); if (match && match.index === 0 && match[0].length === name.length) { return true; } else { cb && process.nextTick(cb.bind(null, new Error(g.f('{{FileSystemProvider}}: Invalid name: %s', name)))); if (!cb) { console.error(g.f('{{FileSystemProvider}}: Invalid name: %s', name)); } return false; } } function streamError(errStream, err, cb) { process.nextTick(function() { errStream.emit('error', err); cb && cb(null, err); }); return errStream; } var writeStreamError = streamError.bind(null, new stream.Writable()); var readStreamError = streamError.bind(null, new stream.Readable()); /*! * Populate the metadata from file stat into props * @param {fs.Stats} stat The file stat instance * @param {Object} props The metadata object */ function populateMetadata(stat, props) { for (var p in stat) { switch (p) { case 'size': case 'atime': case 'mtime': case 'ctime': props[p] = stat[p]; break; } } } FileSystemProvider.prototype.getContainers = function(cb) { cb = cb || utils.createPromiseCallback(); var self = this; fs.readdir(self.root, function(err, files) { var containers = []; var tasks = []; if (!files) { files = []; } files.forEach(function(f) { tasks.push(fs.stat.bind(fs, path.join(self.root, f))); }); async.parallel(tasks, function(err, stats) { if (err) { cb && cb(err); } else { stats.forEach(function(stat, index) { if (stat.isDirectory()) { var name = files[index]; var props = {name: name}; populateMetadata(stat, props); var container = new Container(self, props); containers.push(container); } }); cb && cb(err, containers); } }); }); return cb.promise; }; FileSystemProvider.prototype.createContainer = function(options, cb) { cb = cb || utils.createPromiseCallback(); var self = this; var name = options.name; var dir = path.join(this.root, name); validateName(name, cb) && fs.mkdir(dir, options, function(err) { if (err) { cb && cb(err); return; } fs.stat(dir, function(err, stat) { var container = null; if (!err) { var props = {name: name}; populateMetadata(stat, props); container = new Container(self, props); } cb && cb(err, container); }); }); return cb.promise; }; FileSystemProvider.prototype.destroyContainer = function(containerName, cb) { cb = cb || utils.createPromiseCallback(); if (!validateName(containerName, cb)) return; var dir = path.join(this.root, containerName); fs.readdir(dir, function(err, files) { files = files || []; var tasks = []; files.forEach(function(f) { tasks.push(fs.unlink.bind(fs, path.join(dir, f))); }); async.parallel(tasks, function(err) { if (err) { cb && cb(err); } else { fs.rmdir(dir, cb); } }); }); return cb.promise; }; FileSystemProvider.prototype.getContainer = function(containerName, cb) { cb = cb || utils.createPromiseCallback(); var self = this; if (!validateName(containerName, cb)) return; var dir = path.join(this.root, containerName); fs.stat(dir, function(err, stat) { var container = null; if (!err) { var props = {name: containerName}; populateMetadata(stat, props); container = new Container(self, props); } cb && cb(err, container); }); return cb.promise; }; // File related functions FileSystemProvider.prototype.upload = function(options, cb) { var container = options.container; if (!validateName(container)) { return writeStreamError( new Error(g.f('{{FileSystemProvider}}: Invalid name: %s', container)), cb ); } var file = options.remote; if (!validateName(file)) { return writeStreamError( new Error(g.f('{{FileSystemProvider}}: Invalid name: %s', file)), cb ); } var filePath = path.join(this.root, container, file); var fileOpts = {flags: options.flags || 'w+', encoding: options.encoding || null, mode: options.mode || parseInt('0666', 8), }; try { // simulate the success event in filesystem provider // fixes: https://github.com/strongloop/loopback-component-storage/issues/58 // & #23 & #67 var stream = fs.createWriteStream(filePath, fileOpts); stream.on('finish', function() { stream.emit('success'); }); return stream; } catch (e) { return writeStreamError(e, cb); } }; FileSystemProvider.prototype.download = function(options, cb) { var container = options.container; if (!validateName(container, cb)) { return readStreamError( new Error(g.f('{{FileSystemProvider}}: Invalid name: %s', container)), cb ); } var file = options.remote; if (!validateName(file, cb)) { return readStreamError( new Error(g.f('{{FileSystemProvider}}: Invalid name: %s', file)), cb ); } var filePath = path.join(this.root, container, file); var fileOpts = {flags: 'r', autoClose: true}; if (options.start) { fileOpts.start = options.start; fileOpts.end = options.end; } try { return fs.createReadStream(filePath, fileOpts); } catch (e) { return readStreamError(e, cb); } }; FileSystemProvider.prototype.getFiles = function(container, options, cb) { if (typeof options === 'function' && !(options instanceof RegExp)) { cb = options; options = false; } cb = cb || utils.createPromiseCallback(); var self = this; if (!validateName(container, cb)) return; var dir = path.join(this.root, container); fs.readdir(dir, function(err, entries) { entries = entries || []; var files = []; var tasks = []; entries.forEach(function(f) { tasks.push(fs.stat.bind(fs, path.join(dir, f))); }); async.parallel(tasks, function(err, stats) { if (err) { cb && cb(err); } else { stats.forEach(function(stat, index) { if (stat.isFile()) { var props = {container: container, name: entries[index]}; populateMetadata(stat, props); var file = new File(self, props); files.push(file); } }); cb && cb(err, files); } }); }); return cb.promise; }; FileSystemProvider.prototype.getFile = function(container, file, cb) { cb = cb || utils.createPromiseCallback(); var self = this; if (!validateName(container, cb)) return; if (!validateName(file, cb)) return; var filePath = path.join(this.root, container, file); fs.stat(filePath, function(err, stat) { var f = null; if (!err) { var props = {container: container, name: file}; populateMetadata(stat, props); f = new File(self, props); } cb && cb(err, f); }); return cb.promise; }; FileSystemProvider.prototype.getUrl = function(options) { options = options || {}; var filePath = path.join(this.root, options.container, options.path); return filePath; }; FileSystemProvider.prototype.removeFile = function(container, file, cb) { cb = cb || utils.createPromiseCallback(); if (!validateName(container, cb)) return; if (!validateName(file, cb)) return; var filePath = path.join(this.root, container, file); fs.unlink(filePath, cb); return cb.promise; };