From 13e9097ff0deb9870e7a101285fd62e9c43ef1bf Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 10 Jan 2014 11:34:37 -0800 Subject: [PATCH 01/26] Refactor the storage service --- example/app-loopback.js | 25 +- index.js | 6 + lib/index.js | 150 ------------ lib/models/container.js | 8 + lib/models/file.js | 9 + lib/providers/filesystem/container.js | 16 +- lib/providers/filesystem/file.js | 16 +- lib/providers/filesystem/index.js | 335 +++++++++++++------------- lib/storage-connector.js | 4 +- lib/storage-handler.js | 25 +- lib/storage-service.js | 214 ++++++++++++++++ package.json | 4 +- test/images/album1/.gitignore | 1 + test/storage-service.test.js | 2 +- test/upload.test.js | 39 +++ 15 files changed, 487 insertions(+), 367 deletions(-) create mode 100644 index.js delete mode 100644 lib/index.js create mode 100644 lib/models/container.js create mode 100644 lib/models/file.js create mode 100644 lib/storage-service.js create mode 100644 test/images/album1/.gitignore create mode 100644 test/upload.test.js diff --git a/example/app-loopback.js b/example/app-loopback.js index 57386b5..ff30c6c 100644 --- a/example/app-loopback.js +++ b/example/app-loopback.js @@ -1,7 +1,7 @@ var loopback = require('loopback') , app = module.exports = loopback(); -// var StorageService = require('../'); +var path = require('path'); // expose a rest api app.use(loopback.rest()); @@ -11,31 +11,21 @@ app.configure(function () { }); var ds = loopback.createDataSource({ - connector: require('../lib/storage-connector'), + connector: require('../index'), provider: 'filesystem', - root: '/tmp/storage' + root: path.join(__dirname, 'storage') }); -var Container = ds.createModel('container', {name: String}); - -console.log(Container); -Container.getContainers(console.log); - -console.log('shared', Container.getContainers.shared); +var Container = ds.createModel('container'); app.model(Container); -/* -var handler = new StorageService({provider: 'filesystem', root: '/tmp/storage'}); - -app.service('storage', handler); - app.get('/', function (req, res, next) { res.setHeader('Content-Type', 'text/html'); var form = "

Storage Service Demo

" + - "List all containers

" + + "List all containers

" + "Upload to container c1:

" + - "

" + "" + "File to upload:
" + "Notes about the file:
" + "
" + @@ -44,8 +34,5 @@ app.get('/', function (req, res, next) { res.end(); }); -*/ - - app.listen(app.get('port')); console.log('http://127.0.0.1:' + app.get('port')); diff --git a/index.js b/index.js new file mode 100644 index 0000000..2e97726 --- /dev/null +++ b/index.js @@ -0,0 +1,6 @@ +var StorageConnector = require('./lib/storage-connector'); +StorageConnector.Container = require('./lib/models/container'); +StorageConnector.File = require('./lib/models/file'); + +module.exports = StorageConnector; + diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 6f106a1..0000000 --- a/lib/index.js +++ /dev/null @@ -1,150 +0,0 @@ -var factory = require('./factory'); -var handler = require('./storage-handler'); - -var storage = require('pkgcloud').storage; - -module.exports = StorageService; - -/** - * @param options The options to create a provider - * @returns {StorageService} - * @constructor - */ -function StorageService(options) { - if (!(this instanceof StorageService)) { - return new StorageService(options); - } - this.provider = options.provider; - this.client = factory.createClient(options); -} - -StorageService.prototype.getContainers = function (cb) { - return this.client.getContainers(cb); -} - -StorageService.prototype.createContainer = function (options, cb) { - options = options || {}; - if('object' === typeof options && !(options instanceof storage.Container)) { - var Container = factory.getProvider(this.provider).Container; - options = new Container(this.client, options); - } - return this.client.createContainer(options, cb); -} - -StorageService.prototype.destroyContainer = function (container, cb) { - return this.client.destroyContainer(container, cb); -} - -StorageService.prototype.getContainer = function (container, cb) { - return this.client.getContainer(container, cb); -} - -// File related functions -StorageService.prototype.uploadStream = function (container, file, options, cb) { - if(!cb && typeof options === 'function') { - cb = options; - options = {}; - } - options = options || {}; - if(container) options.container = container; - if(file) options.remote = file; - - return this.client.upload(options, cb); -} - -StorageService.prototype.downloadStream = function (container, file, options, cb) { - if(!cb && typeof options === 'function') { - cb = options; - options = {}; - } - options = options || {}; - if(container) options.container = container; - if(file) options.remote = file; - - return this.client.download(options, cb); -} - -StorageService.prototype.getFiles = function (container, download, cb) { - return this.client.getFiles(container, download, cb); -} - -StorageService.prototype.getFile = function (container, file, cb) { - return this.client.getFile(container, file, cb); -} - -StorageService.prototype.removeFile = function (container, file, cb) { - return this.client.removeFile(container, file, cb); -} - -StorageService.prototype.upload = function (req, res, cb) { - return handler.upload(this.client, req, res, cb); -} - -StorageService.prototype.download = function (req, res, cb) { - return handler.download(this.client, req, res, cb); -} - -StorageService.modelName = 'storage'; - -StorageService.prototype.getContainers.shared = true; -StorageService.prototype.getContainers.accepts = []; -StorageService.prototype.getContainers.returns = {arg: 'containers', type: 'array'}; -StorageService.prototype.getContainers.http = [ - {verb: 'get', path: '/'} -]; - -StorageService.prototype.getContainer.shared = true; -StorageService.prototype.getContainer.accepts = [{arg: 'container', type: 'string'}]; -StorageService.prototype.getContainer.returns = {arg: 'container', type: 'object'}; -StorageService.prototype.getContainer.http = [ - {verb: 'get', path: '/:container'} -]; - -StorageService.prototype.createContainer.shared = true; -StorageService.prototype.createContainer.accepts = [{arg: 'options', type: 'object'}]; -StorageService.prototype.createContainer.returns = {arg: 'container', type: 'object'}; -StorageService.prototype.createContainer.http = [ - {verb: 'post', path: '/'} -]; - -StorageService.prototype.destroyContainer.shared = true; -StorageService.prototype.destroyContainer.accepts = [{arg: 'container', type: 'string'}]; -StorageService.prototype.destroyContainer.returns = {}; -StorageService.prototype.destroyContainer.http = [ - {verb: 'delete', path: '/:container'} -]; - -StorageService.prototype.getFiles.shared = true; -StorageService.prototype.getFiles.accepts = [{arg: 'container', type: 'string'}]; -StorageService.prototype.getFiles.returns = {arg: 'files', type: 'array'}; -StorageService.prototype.getFiles.http = [ - {verb: 'get', path: '/:container/files'} -]; - -StorageService.prototype.getFile.shared = true; -StorageService.prototype.getFile.accepts = [{arg: 'container', type: 'string'}, {arg: 'file', type: 'string'}]; -StorageService.prototype.getFile.returns = {arg: 'file', type: 'object'}; -StorageService.prototype.getFile.http = [ - {verb: 'get', path: '/:container/files/:file'} -]; - -StorageService.prototype.removeFile.shared = true; -StorageService.prototype.removeFile.accepts = [{arg: 'container', type: 'string'}, {arg: 'file', type: 'string'}]; -StorageService.prototype.removeFile.returns = {}; -StorageService.prototype.removeFile.http = [ - {verb: 'delete', path: '/:container/files/:file'} -]; - -StorageService.prototype.upload.shared = true; -StorageService.prototype.upload.accepts = [{arg: 'req', type: 'undefined', 'http': {source: 'req'}}]; -StorageService.prototype.upload.returns = {arg: 'result', type: 'object'}; -StorageService.prototype.upload.http = [ - {verb: 'post', path: '/:container/upload/:file'} -]; - -StorageService.prototype.download.shared = true; -StorageService.prototype.download.accepts = [{arg: 'req', type: 'undefined', 'http': {source: 'req'}}]; -StorageService.prototype.download.returns = {arg: 'res', type: 'stream'}; -StorageService.prototype.download.http = [ - {verb: 'get', path: '/:container/download/:file'} -]; \ No newline at end of file diff --git a/lib/models/container.js b/lib/models/container.js new file mode 100644 index 0000000..dc81254 --- /dev/null +++ b/lib/models/container.js @@ -0,0 +1,8 @@ +var loopback = require('loopback'); + +var Container = loopback.createModel('container', { + name: String, + url: String +}, {idInjection: false, strict: false}); + +module.exports = Container; \ No newline at end of file diff --git a/lib/models/file.js b/lib/models/file.js new file mode 100644 index 0000000..fb98ccc --- /dev/null +++ b/lib/models/file.js @@ -0,0 +1,9 @@ +var loopback = require('loopback'); + +var File = loopback.createModel('file', { + name: String, + url: String, + container: String +}, {idInjection: false, strict: false}); + +module.exports = File; diff --git a/lib/providers/filesystem/container.js b/lib/providers/filesystem/container.js index 43a4b05..9329c8b 100644 --- a/lib/providers/filesystem/container.js +++ b/lib/providers/filesystem/container.js @@ -4,15 +4,15 @@ var util = require('util'); exports.Container = Container; function Container(client, details) { - base.Container.call(this, client, details); -}; + base.Container.call(this, client, details); +} util.inherits(Container, base.Container); -Container.prototype._setProperties = function(details) { - for(var k in details) { - if(typeof details[k] !== 'function') { - this[k] = details[k]; - } +Container.prototype._setProperties = function (details) { + for (var k in details) { + if (typeof details[k] !== 'function') { + this[k] = details[k]; } -} + } +}; diff --git a/lib/providers/filesystem/file.js b/lib/providers/filesystem/file.js index c280c68..09bc990 100644 --- a/lib/providers/filesystem/file.js +++ b/lib/providers/filesystem/file.js @@ -4,15 +4,15 @@ var util = require('util'); exports.File = File; function File(client, details) { - base.File.call(this, client, details); -}; + base.File.call(this, client, details); +} util.inherits(File, base.File); -File.prototype._setProperties = function(details) { - for(var k in details) { - if(typeof details[k] !== 'function') { - this[k] = details[k]; - } +File.prototype._setProperties = function (details) { + for (var k in details) { + if (typeof details[k] !== 'function') { + this[k] = details[k]; } -} \ No newline at end of file + } +}; \ No newline at end of file diff --git a/lib/providers/filesystem/index.js b/lib/providers/filesystem/index.js index e56106c..3854afa 100644 --- a/lib/providers/filesystem/index.js +++ b/lib/providers/filesystem/index.js @@ -3,215 +3,218 @@ */ var fs = require('fs'), - path = require('path'), - async = require('async'), - File = require('./file').File, - Container = require('./container').Container; + path = require('path'), + async = require('async'), + File = require('./file').File, + Container = require('./container').Container; module.exports.File = File; module.exports.Container = Container; module.exports.Client = FileSystemProvider; module.exports.createClient = function (options) { - return new FileSystemProvider(options); + return new FileSystemProvider(options); }; function FileSystemProvider(options) { - options = options || {}; - this.root = options.root; - var exists = fs.existsSync(this.root); - if (!exists) { - throw new Error('Path does not exist: ' + this.root); - } - var stat = fs.statSync(this.root); - if (!stat.isDirectory()) { - throw new Error('Invalid directory: ' + this.root); - } + options = options || {}; + this.root = options.root; + var exists = fs.existsSync(this.root); + if (!exists) { + throw new Error('Path does not exist: ' + this.root); + } + var stat = fs.statSync(this.root); + if (!stat.isDirectory()) { + throw new Error('Invalid directory: ' + this.root); + } } var namePattern = new RegExp('[^' + path.sep + '/]+'); function validateName(name, cb) { - if (!name) { - cb && process.nextTick(cb.bind(null, new Error('Invalid name: ' + name))); - if(!cb) { - console.error('Invalid name: ', name); - } - return false; + if (!name) { + cb && process.nextTick(cb.bind(null, new Error('Invalid name: ' + name))); + if (!cb) { + console.error('Invalid name: ', name); } - 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('Invalid name: ' + name))); - if(!cb) { - console.error('Invalid name: ', name); - } - return false; + 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('Invalid name: ' + name))); + if (!cb) { + console.error('Invalid name: ', name); } + return false; + } } // Container related functions FileSystemProvider.prototype.getContainers = function (cb) { - var self = this; - fs.readdir(self.root, function (err, files) { - var containers = []; - var tasks = []; - files.forEach(function (f) { - tasks.push(fs.stat.bind(null, 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}; - for (var p in stat) { - props[p] = stat[p]; - } - var container = new Container(self, props); - containers.push(container); - } - }); - cb && cb(err, containers); - } - }); + var self = this; + fs.readdir(self.root, function (err, files) { + var containers = []; + var tasks = []; + files.forEach(function (f) { + tasks.push(fs.stat.bind(null, 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}; + for (var p in stat) { + props[p] = stat[p]; + } + var container = new Container(self, props); + containers.push(container); + } + }); + cb && cb(err, containers); + } + }); + }); +}; FileSystemProvider.prototype.createContainer = function (options, cb) { - var self = this; - var name = options.name; - validateName(name, cb) && fs.mkdir(path.join(this.root, name), options, function (err) { - cb && cb(err, new Container(self, {name: name})); - }); -} + var self = this; + var name = options.name; + validateName(name, cb) && fs.mkdir(path.join(this.root, name), options, function (err) { + cb && cb(err, new Container(self, {name: name})); + }); +}; FileSystemProvider.prototype.destroyContainer = function (containerName, cb) { - if (!validateName(containerName, cb)) return; + if (!validateName(containerName, cb)) return; - var dir = path.join(this.root, containerName); - fs.readdir(dir, function (err, files) { - var tasks = []; - files.forEach(function (f) { - tasks.push(fs.unlink.bind(null, path.join(dir, f))); - }); - async.parallel(tasks, function (err) { - if (err) { - cb && cb(err); - } else { - fs.rmdir(dir, cb); - } - }); + var dir = path.join(this.root, containerName); + fs.readdir(dir, function (err, files) { + var tasks = []; + files.forEach(function (f) { + tasks.push(fs.unlink.bind(null, path.join(dir, f))); }); -} + async.parallel(tasks, function (err) { + if (err) { + cb && cb(err); + } else { + fs.rmdir(dir, cb); + } + }); + }); +}; FileSystemProvider.prototype.getContainer = function (containerName, cb) { - 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}; - for (var p in stat) { - props[p] = stat[p]; - } - container = new Container(self, props); - } - cb && cb(err, container); - }); -} - + 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}; + for (var p in stat) { + props[p] = stat[p]; + } + container = new Container(self, props); + } + cb && cb(err, container); + }); +}; // File related functions FileSystemProvider.prototype.upload = function (options, cb) { - var container = options.container; - if (!validateName(container, cb)) return; - var file = options.remote; - if (!validateName(file, cb)) return; - var filePath = path.join(this.root, container, file); + var container = options.container; + if (!validateName(container, cb)) return; + var file = options.remote; + if (!validateName(file, cb)) return; + var filePath = path.join(this.root, container, file); - var fileOpts = {flags: 'w+', - encoding: null, - mode: 0666 }; + var fileOpts = {flags: 'w+', + encoding: null, + mode: 0666 }; - return fs.createWriteStream(filePath, fileOpts); -} + return fs.createWriteStream(filePath, fileOpts); +}; FileSystemProvider.prototype.download = function (options, cb) { - var container = options.container; - if (!validateName(container, cb)) return; - var file = options.remote; - if (!validateName(file, cb)) return; + var container = options.container; + if (!validateName(container, cb)) return; + var file = options.remote; + if (!validateName(file, cb)) return; - var filePath = path.join(this.root, container, file); + var filePath = path.join(this.root, container, file); - var fileOpts = {flags: 'r', - autoClose: true }; + var fileOpts = {flags: 'r', + autoClose: true }; - return fs.createReadStream(filePath, fileOpts); - -} + return fs.createReadStream(filePath, fileOpts); +}; FileSystemProvider.prototype.getFiles = function (container, download, cb) { - if (typeof download === 'function' && !(download instanceof RegExp)) { - cb = download; - download = false; - } - var self = this; - if (!validateName(container, cb)) return; - var dir = path.join(this.root, container); - fs.readdir(dir, function (err, entries) { - var files = []; - var tasks = []; - entries.forEach(function (f) { - tasks.push(fs.stat.bind(null, 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]}; - for (var p in stat) { - props[p] = stat[p]; - } - var file = new File(self, props); - files.push(file); - } - }); - cb && cb(err, files); - } - }); + if (typeof download === 'function' && !(download instanceof RegExp)) { + cb = download; + download = false; + } + var self = this; + if (!validateName(container, cb)) return; + var dir = path.join(this.root, container); + fs.readdir(dir, function (err, entries) { + var files = []; + var tasks = []; + entries.forEach(function (f) { + tasks.push(fs.stat.bind(null, 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]}; + for (var p in stat) { + props[p] = stat[p]; + } + var file = new File(self, props); + files.push(file); + } + }); + cb && cb(err, files); + } + }); + }); +}; FileSystemProvider.prototype.getFile = function (container, file, cb) { - 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}; - for (var p in stat) { - props[p] = stat[p]; - } - f = new File(self, props); - } - cb && cb(err, f); - }); -} + 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}; + for (var p in stat) { + props[p] = stat[p]; + } + f = new File(self, props); + } + cb && cb(err, f); + }); +}; + +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) { - if (!validateName(container, cb)) return; - if (!validateName(file, cb)) return; + if (!validateName(container, cb)) return; + if (!validateName(file, cb)) return; - var filePath = path.join(this.root, container, file); - fs.unlink(filePath, cb); -} + var filePath = path.join(this.root, container, file); + fs.unlink(filePath, cb); +}; diff --git a/lib/storage-connector.js b/lib/storage-connector.js index 24ed13c..f72dc65 100644 --- a/lib/storage-connector.js +++ b/lib/storage-connector.js @@ -1,4 +1,4 @@ -var StorageService = require('./index'); +var StorageService = require('./storage-service'); /** * Export the initialize method to Loopback data * @param dataSource @@ -23,4 +23,4 @@ exports.initialize = function (dataSource, callback) { } connector.define = function(model, properties, settings) {}; -} +}; diff --git a/lib/storage-handler.js b/lib/storage-handler.js index 2e91eac..e7b6d01 100644 --- a/lib/storage-handler.js +++ b/lib/storage-handler.js @@ -4,13 +4,14 @@ var StringDecoder = require('string_decoder').StringDecoder; /** * Handle multipart/form-data upload to the storage service * @param provider The storage service provider - * @param req The HTTP request - * @param res The HTTP response - * @param cb The callback + * @param {Request} req The HTTP request + * @param {Response} res The HTTP response + * @param {String} container The container name + * @param {Function} cb The callback */ -exports.upload = function (provider, req, res, cb) { +exports.upload = function (provider, req, res, container, cb) { var form = new IncomingForm(this.options); - var container = req.params.container; + container = container || req.params.container; var fields = {}, files = {}; form.handlePart = function (part) { var self = this; @@ -104,14 +105,16 @@ exports.upload = function (provider, req, res, cb) { /** * Handle download from a container/file * @param provider The storage service provider - * @param req The HTTP request - * @param res The HTTP response - * @param cb The callback + * @param {Request} req The HTTP request + * @param {Response} res The HTTP response + * @param {String} container The container name + * @param {String} file The file name + * @param {Function} cb The callback */ -exports.download = function(provider, req, res, cb) { +exports.download = function(provider, req, res, container, file, cb) { var reader = provider.download({ - container: req.params.container, - remote: req.params.file + container: container || req.params.container, + remote: file || req.params.file }); reader.pipe(res); reader.on('error', function(err) { diff --git a/lib/storage-service.js b/lib/storage-service.js new file mode 100644 index 0000000..0f0ee15 --- /dev/null +++ b/lib/storage-service.js @@ -0,0 +1,214 @@ +var factory = require('./factory'); +var handler = require('./storage-handler'); + +var storage = require('pkgcloud').storage; + +var Container = require('./models/container'); +var File = require('./models/file'); + +module.exports = StorageService; + +/** + * @param options The options to create a provider + * @returns {StorageService} + * @constructor + */ +function StorageService(options) { + if (!(this instanceof StorageService)) { + return new StorageService(options); + } + this.provider = options.provider; + this.client = factory.createClient(options); +} + +function map(obj) { + if(!obj || typeof obj !== 'object') { + return obj; + } + var data = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i) && typeof obj[i] !== 'function' + && typeof obj[i] !== 'object') { + if (i === 'newListener' || i === 'delimiter' || i === 'wildcard') { + // Skip properties from the base class + continue; + } + data[i] = obj[i]; + } + } + return data; +} + +StorageService.prototype.getContainers = function (cb) { + this.client.getContainers(function(err, containers) { + if(err) { + cb(err, containers); + } else { + cb(err, containers.map(function(c) { + return new Container(map(c)); + })); + } + }); +}; + +StorageService.prototype.createContainer = function (options, cb) { + options = options || {}; + if ('object' === typeof options && !(options instanceof storage.Container)) { + var Container = factory.getProvider(this.provider).Container; + options = new Container(this.client, options); + } + return this.client.createContainer(options, function(err, container) { + return cb(err, map(container)); + }); +}; + +StorageService.prototype.destroyContainer = function (container, cb) { + return this.client.destroyContainer(container, cb); +}; + +StorageService.prototype.getContainer = function (container, cb) { + return this.client.getContainer(container, function(err, container) { + return cb(err, map(container)); + }); +}; + +// File related functions +StorageService.prototype.uploadStream = function (container, file, options, cb) { + if (!cb && typeof options === 'function') { + cb = options; + options = {}; + } + options = options || {}; + if (container) options.container = container; + if (file) options.remote = file; + + return this.client.upload(options, cb); +}; + +StorageService.prototype.downloadStream = function (container, file, options, cb) { + if (!cb && typeof options === 'function') { + cb = options; + options = {}; + } + options = options || {}; + if (container) options.container = container; + if (file) options.remote = file; + + return this.client.download(options, cb); +}; + +StorageService.prototype.getFiles = function (container, download, cb) { + return this.client.getFiles(container, download, function(err, files) { + if(err) { + cb(err, files); + } else { + cb(err, files.map(function(f) { + return new File(map(f)); + })); + } + }); +}; + +StorageService.prototype.getFile = function (container, file, cb) { + return this.client.getFile(container, file, function(err, f) { + return cb(err, map(f)); + }); +}; + +StorageService.prototype.removeFile = function (container, file, cb) { + return this.client.removeFile(container, file, cb); +}; + +StorageService.prototype.upload = function (req, res, cb) { + return handler.upload(this.client, req, res, req.params.container, cb); +}; + +StorageService.prototype.download = function (req, res, cb) { + return handler.download(this.client, req, res, + req.params.container, req.params.file, cb); +}; + +StorageService.modelName = 'storage'; + +StorageService.prototype.getContainers.shared = true; +StorageService.prototype.getContainers.accepts = []; +StorageService.prototype.getContainers.returns = {arg: 'containers', type: 'array', root: true}; +StorageService.prototype.getContainers.http = [ + {verb: 'get', path: '/'} +]; + +StorageService.prototype.getContainer.shared = true; +StorageService.prototype.getContainer.accepts = [ + {arg: 'container', type: 'string'} +]; +StorageService.prototype.getContainer.returns = {arg: 'container', type: 'object', root: true}; +StorageService.prototype.getContainer.http = [ + {verb: 'get', path: '/:container'} +]; + +StorageService.prototype.createContainer.shared = true; +StorageService.prototype.createContainer.accepts = [ + {arg: 'options', type: 'object'} +]; +StorageService.prototype.createContainer.returns = {arg: 'container', type: 'object', root: true}; +StorageService.prototype.createContainer.http = [ + {verb: 'post', path: '/'} +]; + +StorageService.prototype.destroyContainer.shared = true; +StorageService.prototype.destroyContainer.accepts = [ + {arg: 'container', type: 'string'} +]; +StorageService.prototype.destroyContainer.returns = {}; +StorageService.prototype.destroyContainer.http = [ + {verb: 'delete', path: '/:container'} +]; + +StorageService.prototype.getFiles.shared = true; +StorageService.prototype.getFiles.accepts = [ + {arg: 'container', type: 'string'} +]; +StorageService.prototype.getFiles.returns = {arg: 'files', type: 'array', root: true}; +StorageService.prototype.getFiles.http = [ + {verb: 'get', path: '/:container/files'} +]; + +StorageService.prototype.getFile.shared = true; +StorageService.prototype.getFile.accepts = [ + {arg: 'container', type: 'string'}, + {arg: 'file', type: 'string'} +]; +StorageService.prototype.getFile.returns = {arg: 'file', type: 'object', root: true}; +StorageService.prototype.getFile.http = [ + {verb: 'get', path: '/:container/files/:file'} +]; + +StorageService.prototype.removeFile.shared = true; +StorageService.prototype.removeFile.accepts = [ + {arg: 'container', type: 'string'}, + {arg: 'file', type: 'string'} +]; +StorageService.prototype.removeFile.returns = {}; +StorageService.prototype.removeFile.http = [ + {verb: 'delete', path: '/:container/files/:file'} +]; + +StorageService.prototype.upload.shared = true; +StorageService.prototype.upload.accepts = [ + {arg: 'req', type: 'undefined', 'http': {source: 'req'}}, + {arg: 'res', type: 'undefined', 'http': {source: 'res'}} +]; +StorageService.prototype.upload.returns = {arg: 'result', type: 'object'}; +StorageService.prototype.upload.http = [ + {verb: 'post', path: '/:container/upload'} +]; + +StorageService.prototype.download.shared = true; +StorageService.prototype.download.accepts = [ + {arg: 'req', type: 'undefined', 'http': {source: 'req'}}, + {arg: 'res', type: 'undefined', 'http': {source: 'res'}} +]; +StorageService.prototype.download.returns = {arg: 'res', type: 'stream'}; +StorageService.prototype.download.http = [ + {verb: 'get', path: '/:container/download/:file'} +]; \ No newline at end of file diff --git a/package.json b/package.json index 1300406..ab8f91e 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,12 @@ "name": "loopback-storage-service", "description": "Loopback Storage Service", "version": "1.0.0", - "main": "lib/index.js", + "main": "index.js", "scripts": { "test": "./node_modules/.bin/mocha --timeout 30000 test/*test.js" }, "dependencies": { - "pkgcloud": "~0.8.14", + "pkgcloud": "~0.8.17", "async": "~0.2.9" }, "devDependencies": { diff --git a/test/images/album1/.gitignore b/test/images/album1/.gitignore new file mode 100644 index 0000000..a25287a --- /dev/null +++ b/test/images/album1/.gitignore @@ -0,0 +1 @@ +test.jpg diff --git a/test/storage-service.test.js b/test/storage-service.test.js index 6af0c5d..1d756de 100644 --- a/test/storage-service.test.js +++ b/test/storage-service.test.js @@ -1,4 +1,4 @@ -var StorageService = require('../lib/index.js'); +var StorageService = require('../lib/storage-service.js'); var assert = require('assert'); var path = require('path'); diff --git a/test/upload.test.js b/test/upload.test.js new file mode 100644 index 0000000..f96b2af --- /dev/null +++ b/test/upload.test.js @@ -0,0 +1,39 @@ +var request = require('supertest'); +var loopback = require('loopback'); +var assert = require('assert'); + +var app = loopback(); +var path = require('path'); + +// expose a rest api +app.use(loopback.rest()); + +var ds = loopback.createDataSource({ + connector: require('../lib/storage-connector'), + provider: 'filesystem', + root: path.join(__dirname, 'images') +}); + +var Container = ds.createModel('container'); +app.model(Container); + +describe('storage service', function () { + it('uploads files', function (done) { + + var server = app.listen(3000, function () { + + request('http://localhost:3000') + .post('/containers/album1/upload') + .attach('image', path.join(__dirname, '../example/test.jpg')) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert.deepEqual(res.body, {"result": {"files": {"image": [ + {"container": "album1", "name": "test.jpg", "type": "image/jpeg"} + ]}, "fields": {}}}); + server.close(); + done(); + }); + }); + }); +}); From 9c580115305e65d47e948c20cd1669660965d1c7 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 14 Jan 2014 10:26:09 -0800 Subject: [PATCH 02/26] Clean up the download method --- example/app-loopback.js | 2 + lib/providers/filesystem/index.js | 12 ++++- lib/storage-handler.js | 9 ++-- lib/storage-service.js | 80 ++++++++++++++----------------- test/upload-download.test.js | 65 +++++++++++++++++++++++++ test/upload.test.js | 39 --------------- 6 files changed, 115 insertions(+), 92 deletions(-) create mode 100644 test/upload-download.test.js delete mode 100644 test/upload.test.js diff --git a/example/app-loopback.js b/example/app-loopback.js index ff30c6c..e81a84e 100644 --- a/example/app-loopback.js +++ b/example/app-loopback.js @@ -3,6 +3,8 @@ var loopback = require('loopback') var path = require('path'); +app.use(app.router); + // expose a rest api app.use(loopback.rest()); diff --git a/lib/providers/filesystem/index.js b/lib/providers/filesystem/index.js index 3854afa..7c29dee 100644 --- a/lib/providers/filesystem/index.js +++ b/lib/providers/filesystem/index.js @@ -136,7 +136,11 @@ FileSystemProvider.prototype.upload = function (options, cb) { encoding: null, mode: 0666 }; - return fs.createWriteStream(filePath, fileOpts); + try { + return fs.createWriteStream(filePath, fileOpts); + } catch (e) { + cb && cb(e); + } }; FileSystemProvider.prototype.download = function (options, cb) { @@ -150,7 +154,11 @@ FileSystemProvider.prototype.download = function (options, cb) { var fileOpts = {flags: 'r', autoClose: true }; - return fs.createReadStream(filePath, fileOpts); + try { + return fs.createReadStream(filePath, fileOpts); + } catch (e) { + cb && cb(e); + } }; FileSystemProvider.prototype.getFiles = function (container, download, cb) { diff --git a/lib/storage-handler.js b/lib/storage-handler.js index e7b6d01..2c48030 100644 --- a/lib/storage-handler.js +++ b/lib/storage-handler.js @@ -113,15 +113,12 @@ exports.upload = function (provider, req, res, container, cb) { */ exports.download = function(provider, req, res, container, file, cb) { var reader = provider.download({ - container: container || req.params.container, - remote: file || req.params.file + container: container || req && req.params.container, + remote: file || req && req.params.file }); reader.pipe(res); reader.on('error', function(err) { - cb && cb(err); - }); - reader.on('end', function(err, result) { - cb && cb(err, result); + res.send(500, { error: err }); }); } diff --git a/lib/storage-service.js b/lib/storage-service.js index 0f0ee15..16e462b 100644 --- a/lib/storage-service.js +++ b/lib/storage-service.js @@ -22,7 +22,7 @@ function StorageService(options) { } function map(obj) { - if(!obj || typeof obj !== 'object') { + if (!obj || typeof obj !== 'object') { return obj; } var data = {}; @@ -40,11 +40,11 @@ function map(obj) { } StorageService.prototype.getContainers = function (cb) { - this.client.getContainers(function(err, containers) { - if(err) { + this.client.getContainers(function (err, containers) { + if (err) { cb(err, containers); } else { - cb(err, containers.map(function(c) { + cb(err, containers.map(function (c) { return new Container(map(c)); })); } @@ -57,7 +57,7 @@ StorageService.prototype.createContainer = function (options, cb) { var Container = factory.getProvider(this.provider).Container; options = new Container(this.client, options); } - return this.client.createContainer(options, function(err, container) { + return this.client.createContainer(options, function (err, container) { return cb(err, map(container)); }); }; @@ -67,7 +67,7 @@ StorageService.prototype.destroyContainer = function (container, cb) { }; StorageService.prototype.getContainer = function (container, cb) { - return this.client.getContainer(container, function(err, container) { + return this.client.getContainer(container, function (err, container) { return cb(err, map(container)); }); }; @@ -98,11 +98,11 @@ StorageService.prototype.downloadStream = function (container, file, options, cb }; StorageService.prototype.getFiles = function (container, download, cb) { - return this.client.getFiles(container, download, function(err, files) { - if(err) { + return this.client.getFiles(container, download, function (err, files) { + if (err) { cb(err, files); } else { - cb(err, files.map(function(f) { + cb(err, files.map(function (f) { return new File(map(f)); })); } @@ -110,7 +110,7 @@ StorageService.prototype.getFiles = function (container, download, cb) { }; StorageService.prototype.getFile = function (container, file, cb) { - return this.client.getFile(container, file, function(err, f) { + return this.client.getFile(container, file, function (err, f) { return cb(err, map(f)); }); }; @@ -123,9 +123,8 @@ StorageService.prototype.upload = function (req, res, cb) { return handler.upload(this.client, req, res, req.params.container, cb); }; -StorageService.prototype.download = function (req, res, cb) { - return handler.download(this.client, req, res, - req.params.container, req.params.file, cb); +StorageService.prototype.download = function (container, file, res, cb) { + return handler.download(this.client, null, res, container, file, cb); }; StorageService.modelName = 'storage'; @@ -133,45 +132,40 @@ StorageService.modelName = 'storage'; StorageService.prototype.getContainers.shared = true; StorageService.prototype.getContainers.accepts = []; StorageService.prototype.getContainers.returns = {arg: 'containers', type: 'array', root: true}; -StorageService.prototype.getContainers.http = [ - {verb: 'get', path: '/'} -]; +StorageService.prototype.getContainers.http = +{verb: 'get', path: '/'}; StorageService.prototype.getContainer.shared = true; StorageService.prototype.getContainer.accepts = [ {arg: 'container', type: 'string'} ]; StorageService.prototype.getContainer.returns = {arg: 'container', type: 'object', root: true}; -StorageService.prototype.getContainer.http = [ - {verb: 'get', path: '/:container'} -]; +StorageService.prototype.getContainer.http = +{verb: 'get', path: '/:container'}; StorageService.prototype.createContainer.shared = true; StorageService.prototype.createContainer.accepts = [ {arg: 'options', type: 'object'} ]; StorageService.prototype.createContainer.returns = {arg: 'container', type: 'object', root: true}; -StorageService.prototype.createContainer.http = [ - {verb: 'post', path: '/'} -]; +StorageService.prototype.createContainer.http = +{verb: 'post', path: '/'}; StorageService.prototype.destroyContainer.shared = true; StorageService.prototype.destroyContainer.accepts = [ {arg: 'container', type: 'string'} ]; StorageService.prototype.destroyContainer.returns = {}; -StorageService.prototype.destroyContainer.http = [ - {verb: 'delete', path: '/:container'} -]; +StorageService.prototype.destroyContainer.http = +{verb: 'delete', path: '/:container'}; StorageService.prototype.getFiles.shared = true; StorageService.prototype.getFiles.accepts = [ {arg: 'container', type: 'string'} ]; StorageService.prototype.getFiles.returns = {arg: 'files', type: 'array', root: true}; -StorageService.prototype.getFiles.http = [ - {verb: 'get', path: '/:container/files'} -]; +StorageService.prototype.getFiles.http = +{verb: 'get', path: '/:container/files'}; StorageService.prototype.getFile.shared = true; StorageService.prototype.getFile.accepts = [ @@ -179,9 +173,8 @@ StorageService.prototype.getFile.accepts = [ {arg: 'file', type: 'string'} ]; StorageService.prototype.getFile.returns = {arg: 'file', type: 'object', root: true}; -StorageService.prototype.getFile.http = [ - {verb: 'get', path: '/:container/files/:file'} -]; +StorageService.prototype.getFile.http = +{verb: 'get', path: '/:container/files/:file'}; StorageService.prototype.removeFile.shared = true; StorageService.prototype.removeFile.accepts = [ @@ -189,26 +182,23 @@ StorageService.prototype.removeFile.accepts = [ {arg: 'file', type: 'string'} ]; StorageService.prototype.removeFile.returns = {}; -StorageService.prototype.removeFile.http = [ - {verb: 'delete', path: '/:container/files/:file'} -]; +StorageService.prototype.removeFile.http = +{verb: 'delete', path: '/:container/files/:file'}; StorageService.prototype.upload.shared = true; StorageService.prototype.upload.accepts = [ - {arg: 'req', type: 'undefined', 'http': {source: 'req'}}, - {arg: 'res', type: 'undefined', 'http': {source: 'res'}} + {arg: 'req', type: 'object', 'http': {source: 'req'}}, + {arg: 'res', type: 'object', 'http': {source: 'res'}} ]; StorageService.prototype.upload.returns = {arg: 'result', type: 'object'}; -StorageService.prototype.upload.http = [ - {verb: 'post', path: '/:container/upload'} -]; +StorageService.prototype.upload.http = +{verb: 'post', path: '/:container/upload'}; StorageService.prototype.download.shared = true; StorageService.prototype.download.accepts = [ - {arg: 'req', type: 'undefined', 'http': {source: 'req'}}, - {arg: 'res', type: 'undefined', 'http': {source: 'res'}} + {arg: 'container', type: 'string', 'http': {source: 'path'}}, + {arg: 'file', type: 'string', 'http': {source: 'path'}}, + {arg: 'res', type: 'object', 'http': {source: 'res'}} ]; -StorageService.prototype.download.returns = {arg: 'res', type: 'stream'}; -StorageService.prototype.download.http = [ - {verb: 'get', path: '/:container/download/:file'} -]; \ No newline at end of file +StorageService.prototype.download.http = +{verb: 'get', path: '/:container/download/:file'}; \ No newline at end of file diff --git a/test/upload-download.test.js b/test/upload-download.test.js new file mode 100644 index 0000000..ae3bb54 --- /dev/null +++ b/test/upload-download.test.js @@ -0,0 +1,65 @@ +var request = require('supertest'); +var loopback = require('loopback'); +var assert = require('assert'); + +var app = loopback(); +var path = require('path'); + +// expose a rest api +app.use(loopback.rest()); + +var ds = loopback.createDataSource({ + connector: require('../lib/storage-connector'), + provider: 'filesystem', + root: path.join(__dirname, 'images') +}); + +var Container = ds.createModel('container'); +app.model(Container); + +describe('storage service', function () { + var server = null; + before(function (done) { + server = app.listen(3000, function () { + done(); + }); + }); + + after(function () { + server.close(); + }); + + it('uploads files', function (done) { + + request('http://localhost:3000') + .post('/containers/album1/upload') + .attach('image', path.join(__dirname, '../example/test.jpg')) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert.deepEqual(res.body, {"result": {"files": {"image": [ + {"container": "album1", "name": "test.jpg", "type": "image/jpeg"} + ]}, "fields": {}}}); + done(); + }); + }); + + it('downloads files', function (done) { + + request('http://localhost:3000') + .get('/containers/album1/download/test.jpg') + .expect(200, function (err, res) { + done(); + }); + }); + + it('reports errors if it fails to find the file to download', function (done) { + + request('http://localhost:3000') + .get('/containers/album1/download/test_not_exist.jpg') + .expect(500, function (err, res) { + assert(res.body.error); + done(); + }); + }); +}); diff --git a/test/upload.test.js b/test/upload.test.js deleted file mode 100644 index f96b2af..0000000 --- a/test/upload.test.js +++ /dev/null @@ -1,39 +0,0 @@ -var request = require('supertest'); -var loopback = require('loopback'); -var assert = require('assert'); - -var app = loopback(); -var path = require('path'); - -// expose a rest api -app.use(loopback.rest()); - -var ds = loopback.createDataSource({ - connector: require('../lib/storage-connector'), - provider: 'filesystem', - root: path.join(__dirname, 'images') -}); - -var Container = ds.createModel('container'); -app.model(Container); - -describe('storage service', function () { - it('uploads files', function (done) { - - var server = app.listen(3000, function () { - - request('http://localhost:3000') - .post('/containers/album1/upload') - .attach('image', path.join(__dirname, '../example/test.jpg')) - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(200, function (err, res) { - assert.deepEqual(res.body, {"result": {"files": {"image": [ - {"container": "album1", "name": "test.jpg", "type": "image/jpeg"} - ]}, "fields": {}}}); - server.close(); - done(); - }); - }); - }); -}); From 0f9407908202b65dd3924a92948fa3b241136eff Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 14 Jan 2014 10:39:02 -0800 Subject: [PATCH 03/26] Fix the ref to StorageService --- example/app-loopback.js | 4 ++-- example/app.js | 2 +- example/upload-amazon.js | 2 +- example/upload.js | 2 +- index.js | 1 + lib/storage-service.js | 3 +++ 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/example/app-loopback.js b/example/app-loopback.js index e81a84e..291c1ca 100644 --- a/example/app-loopback.js +++ b/example/app-loopback.js @@ -18,9 +18,9 @@ var ds = loopback.createDataSource({ root: path.join(__dirname, 'storage') }); -var Container = ds.createModel('container'); +var Storage = ds.createModel('container'); -app.model(Container); +app.model(Storage); app.get('/', function (req, res, next) { res.setHeader('Content-Type', 'text/html'); diff --git a/example/app.js b/example/app.js index 6ea7482..e5a6463 100644 --- a/example/app.js +++ b/example/app.js @@ -1,4 +1,4 @@ -var StorageService = require('../'); +var StorageService = require('../').StorageService; var path = require('path'); var rs = StorageService({ diff --git a/example/upload-amazon.js b/example/upload-amazon.js index 6f9d354..7e60997 100644 --- a/example/upload-amazon.js +++ b/example/upload-amazon.js @@ -1,4 +1,4 @@ -var StorageService = require('../'); +var StorageService = require('../').StorageService; var express = require('express'); var app = express(); diff --git a/example/upload.js b/example/upload.js index d91b166..efb28cf 100644 --- a/example/upload.js +++ b/example/upload.js @@ -1,4 +1,4 @@ -var StorageService = require('../'); +var StorageService = require('../').StorageService; var express = require('express'); var app = express(); diff --git a/index.js b/index.js index 2e97726..e56592e 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ var StorageConnector = require('./lib/storage-connector'); StorageConnector.Container = require('./lib/models/container'); StorageConnector.File = require('./lib/models/file'); +StorageConnector.StorageService = require('./lib/storage-service'); module.exports = StorageConnector; diff --git a/lib/storage-service.js b/lib/storage-service.js index 16e462b..ff30886 100644 --- a/lib/storage-service.js +++ b/lib/storage-service.js @@ -22,6 +22,8 @@ function StorageService(options) { } function map(obj) { + return obj; + /* if (!obj || typeof obj !== 'object') { return obj; } @@ -37,6 +39,7 @@ function map(obj) { } } return data; + */ } StorageService.prototype.getContainers = function (cb) { From 160e1a44bf80297c9c9a5733aa4dac8d036b75a7 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 24 Jan 2014 09:32:48 -0800 Subject: [PATCH 04/26] Address some of the PR comments --- example/app-cloud.js | 91 +++++++++++++++++++++++++ example/app-loopback.js | 40 ----------- example/app.js | 109 ++++++++---------------------- lib/providers/filesystem/index.js | 24 ++++--- 4 files changed, 133 insertions(+), 131 deletions(-) create mode 100644 example/app-cloud.js delete mode 100644 example/app-loopback.js diff --git a/example/app-cloud.js b/example/app-cloud.js new file mode 100644 index 0000000..e5a6463 --- /dev/null +++ b/example/app-cloud.js @@ -0,0 +1,91 @@ +var StorageService = require('../').StorageService; +var path = require('path'); + +var rs = StorageService({ + provider: 'rackspace', + username: 'strongloop', + apiKey: 'your-rackspace-api-key' +}); + +// Container + +rs.getContainers(function (err, containers) { + if (err) { + console.error(err); + return; + } + containers.forEach(function (c) { + console.log('rackspace: ', c.name); + c.getFiles(function (err, files) { + files.forEach(function (f) { + console.log('....', f.name); + }); + }); + }); +}); + +/* + client.createContainer(options, function (err, container) { }); + client.destroyContainer(containerName, function (err) { }); + client.getContainer(containerName, function (err, container) { }); + + // File + + client.upload(options, function (err) { }); + client.download(options, function (err) { }); + client.getFiles(container, function (err, files) { }); + client.getFile(container, file, function (err, server) { }); + client.removeFile(container, file, function (err) { }); + */ + + +var s3 = StorageService({ + provider: 'amazon', + key: 'your-amazon-key', + keyId: 'your-amazon-key-id' +}); + +s3.getContainers(function (err, containers) { + if (err) { + console.error(err); + return; + } + containers.forEach(function (c) { + console.log('amazon: ', c.name); + c.getFiles(function (err, files) { + files.forEach(function (f) { + console.log('....', f.name); + }); + }); + }); +}); + + +var fs = require('fs'); +var path = require('path'); +var stream = s3.uploadStream('con1','test.jpg'); +var input = fs.createReadStream(path.join(__dirname, 'test.jpg')).pipe(stream); + + +var local = StorageService({ + provider: 'filesystem', + root: path.join(__dirname, 'storage') +}); + +// Container + +local.getContainers(function (err, containers) { + if (err) { + console.error(err); + return; + } + containers.forEach(function (c) { + console.log('filesystem: ', c.name); + c.getFiles(function (err, files) { + files.forEach(function (f) { + console.log('....', f.name); + }); + }); + }); +}); + diff --git a/example/app-loopback.js b/example/app-loopback.js deleted file mode 100644 index 291c1ca..0000000 --- a/example/app-loopback.js +++ /dev/null @@ -1,40 +0,0 @@ -var loopback = require('loopback') - , app = module.exports = loopback(); - -var path = require('path'); - -app.use(app.router); - -// expose a rest api -app.use(loopback.rest()); - -app.configure(function () { - app.set('port', process.env.PORT || 3000); -}); - -var ds = loopback.createDataSource({ - connector: require('../index'), - provider: 'filesystem', - root: path.join(__dirname, 'storage') -}); - -var Storage = ds.createModel('container'); - -app.model(Storage); - -app.get('/', function (req, res, next) { - res.setHeader('Content-Type', 'text/html'); - var form = "

Storage Service Demo

" + - "List all containers

" + - "Upload to container c1:

" + - "

" - + "File to upload:
" - + "Notes about the file:
" - + "
" + - ""; - res.send(form); - res.end(); -}); - -app.listen(app.get('port')); -console.log('http://127.0.0.1:' + app.get('port')); diff --git a/example/app.js b/example/app.js index e5a6463..339b367 100644 --- a/example/app.js +++ b/example/app.js @@ -1,91 +1,40 @@ -var StorageService = require('../').StorageService; +var loopback = require('loopback') + , app = module.exports = loopback(); + var path = require('path'); -var rs = StorageService({ - provider: 'rackspace', - username: 'strongloop', - apiKey: 'your-rackspace-api-key' +app.use(app.router); + +// expose a rest api +app.use(loopback.rest()); + +app.configure(function () { + app.set('port', process.env.PORT || 3000); }); -// Container - -rs.getContainers(function (err, containers) { - if (err) { - console.error(err); - return; - } - containers.forEach(function (c) { - console.log('rackspace: ', c.name); - c.getFiles(function (err, files) { - files.forEach(function (f) { - console.log('....', f.name); - }); - }); - }); -}); - -/* - client.createContainer(options, function (err, container) { }); - client.destroyContainer(containerName, function (err) { }); - client.getContainer(containerName, function (err, container) { }); - - // File - - client.upload(options, function (err) { }); - client.download(options, function (err) { }); - client.getFiles(container, function (err, files) { }); - client.getFile(container, file, function (err, server) { }); - client.removeFile(container, file, function (err) { }); - */ - - -var s3 = StorageService({ - provider: 'amazon', - key: 'your-amazon-key', - keyId: 'your-amazon-key-id' -}); - -s3.getContainers(function (err, containers) { - if (err) { - console.error(err); - return; - } - containers.forEach(function (c) { - console.log('amazon: ', c.name); - c.getFiles(function (err, files) { - files.forEach(function (f) { - console.log('....', f.name); - }); - }); - }); -}); - - -var fs = require('fs'); -var path = require('path'); -var stream = s3.uploadStream('con1','test.jpg'); -var input = fs.createReadStream(path.join(__dirname, 'test.jpg')).pipe(stream); - - -var local = StorageService({ +var ds = loopback.createDataSource({ + connector: require('../index'), provider: 'filesystem', root: path.join(__dirname, 'storage') }); -// Container +var container = ds.createModel('container'); -local.getContainers(function (err, containers) { - if (err) { - console.error(err); - return; - } - containers.forEach(function (c) { - console.log('filesystem: ', c.name); - c.getFiles(function (err, files) { - files.forEach(function (f) { - console.log('....', f.name); - }); - }); - }); +app.model(container); + +app.get('/', function (req, res, next) { + res.setHeader('Content-Type', 'text/html'); + var form = "

Storage Service Demo

" + + "List all containers

" + + "Upload to container c1:

" + + "

" + + "File to upload:
" + + "Notes about the file:
" + + "
" + + ""; + res.send(form); + res.end(); }); +app.listen(app.get('port')); +console.log('http://127.0.0.1:' + app.get('port')); diff --git a/lib/providers/filesystem/index.js b/lib/providers/filesystem/index.js index 7c29dee..12a7c70 100644 --- a/lib/providers/filesystem/index.js +++ b/lib/providers/filesystem/index.js @@ -20,11 +20,11 @@ function FileSystemProvider(options) { this.root = options.root; var exists = fs.existsSync(this.root); if (!exists) { - throw new Error('Path does not exist: ' + this.root); + throw new Error('FileSystemProvider: Path does not exist: ' + this.root); } var stat = fs.statSync(this.root); if (!stat.isDirectory()) { - throw new Error('Invalid directory: ' + this.root); + throw new Error('FileSystemProvider: Invalid directory: ' + this.root); } } @@ -34,7 +34,7 @@ function validateName(name, cb) { if (!name) { cb && process.nextTick(cb.bind(null, new Error('Invalid name: ' + name))); if (!cb) { - console.error('Invalid name: ', name); + console.error('FileSystemProvider: Invalid name: ', name); } return false; } @@ -42,9 +42,10 @@ function validateName(name, cb) { if (match && match.index === 0 && match[0].length === name.length) { return true; } else { - cb && process.nextTick(cb.bind(null, new Error('Invalid name: ' + name))); + cb && process.nextTick(cb.bind(null, + new Error('FileSystemProvider: Invalid name: ' + name))); if (!cb) { - console.error('Invalid name: ', name); + console.error('FileSystemProvider: Invalid name: ', name); } return false; } @@ -57,7 +58,7 @@ FileSystemProvider.prototype.getContainers = function (cb) { var containers = []; var tasks = []; files.forEach(function (f) { - tasks.push(fs.stat.bind(null, path.join(self.root, f))); + tasks.push(fs.stat.bind(fs, path.join(self.root, f))); }); async.parallel(tasks, function (err, stats) { if (err) { @@ -95,7 +96,7 @@ FileSystemProvider.prototype.destroyContainer = function (containerName, cb) { fs.readdir(dir, function (err, files) { var tasks = []; files.forEach(function (f) { - tasks.push(fs.unlink.bind(null, path.join(dir, f))); + tasks.push(fs.unlink.bind(fs, path.join(dir, f))); }); async.parallel(tasks, function (err) { if (err) { @@ -132,9 +133,10 @@ FileSystemProvider.prototype.upload = function (options, cb) { if (!validateName(file, cb)) return; var filePath = path.join(this.root, container, file); - var fileOpts = {flags: 'w+', - encoding: null, - mode: 0666 }; + var fileOpts = {flags: options.flags || 'w+', + encoding: options.encoding || null, + mode: options.mode || 0666 + }; try { return fs.createWriteStream(filePath, fileOpts); @@ -173,7 +175,7 @@ FileSystemProvider.prototype.getFiles = function (container, download, cb) { var files = []; var tasks = []; entries.forEach(function (f) { - tasks.push(fs.stat.bind(null, path.join(dir, f))); + tasks.push(fs.stat.bind(fs, path.join(dir, f))); }); async.parallel(tasks, function (err, stats) { if (err) { From 70f5d6dc352c3205a69db4353665432fed409786 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 24 Jan 2014 09:44:58 -0800 Subject: [PATCH 05/26] Reformat code --- example/app-cloud.js | 87 ++++++------ example/app.js | 32 ++--- example/upload-amazon.js | 94 ++++++------ example/upload.js | 90 ++++++------ lib/factory.js | 38 ++--- lib/storage-connector.js | 30 ++-- lib/storage-handler.js | 178 +++++++++++------------ lib/storage-service.js | 32 ++--- test/fs.test.js | 268 +++++++++++++++++------------------ test/storage-service.test.js | 228 ++++++++++++++--------------- 10 files changed, 538 insertions(+), 539 deletions(-) diff --git a/example/app-cloud.js b/example/app-cloud.js index e5a6463..3e8c7b1 100644 --- a/example/app-cloud.js +++ b/example/app-cloud.js @@ -2,26 +2,26 @@ var StorageService = require('../').StorageService; var path = require('path'); var rs = StorageService({ - provider: 'rackspace', - username: 'strongloop', - apiKey: 'your-rackspace-api-key' + provider: 'rackspace', + username: 'strongloop', + apiKey: 'your-rackspace-api-key' }); // Container rs.getContainers(function (err, containers) { - if (err) { - console.error(err); - return; - } - containers.forEach(function (c) { - console.log('rackspace: ', c.name); - c.getFiles(function (err, files) { - files.forEach(function (f) { - console.log('....', f.name); - }); - }); + if (err) { + console.error(err); + return; + } + containers.forEach(function (c) { + console.log('rackspace: ', c.name); + c.getFiles(function (err, files) { + files.forEach(function (f) { + console.log('....', f.name); + }); }); + }); }); /* @@ -38,54 +38,51 @@ rs.getContainers(function (err, containers) { client.removeFile(container, file, function (err) { }); */ - var s3 = StorageService({ - provider: 'amazon', - key: 'your-amazon-key', - keyId: 'your-amazon-key-id' + provider: 'amazon', + key: 'your-amazon-key', + keyId: 'your-amazon-key-id' }); s3.getContainers(function (err, containers) { - if (err) { - console.error(err); - return; - } - containers.forEach(function (c) { - console.log('amazon: ', c.name); - c.getFiles(function (err, files) { - files.forEach(function (f) { - console.log('....', f.name); - }); - }); + if (err) { + console.error(err); + return; + } + containers.forEach(function (c) { + console.log('amazon: ', c.name); + c.getFiles(function (err, files) { + files.forEach(function (f) { + console.log('....', f.name); + }); }); + }); }); - var fs = require('fs'); var path = require('path'); -var stream = s3.uploadStream('con1','test.jpg'); +var stream = s3.uploadStream('con1', 'test.jpg'); var input = fs.createReadStream(path.join(__dirname, 'test.jpg')).pipe(stream); - var local = StorageService({ - provider: 'filesystem', - root: path.join(__dirname, 'storage') + provider: 'filesystem', + root: path.join(__dirname, 'storage') }); // Container local.getContainers(function (err, containers) { - if (err) { - console.error(err); - return; - } - containers.forEach(function (c) { - console.log('filesystem: ', c.name); - c.getFiles(function (err, files) { - files.forEach(function (f) { - console.log('....', f.name); - }); - }); + if (err) { + console.error(err); + return; + } + containers.forEach(function (c) { + console.log('filesystem: ', c.name); + c.getFiles(function (err, files) { + files.forEach(function (f) { + console.log('....', f.name); + }); }); + }); }); diff --git a/example/app.js b/example/app.js index 339b367..56c91df 100644 --- a/example/app.js +++ b/example/app.js @@ -1,5 +1,5 @@ var loopback = require('loopback') - , app = module.exports = loopback(); + , app = module.exports = loopback(); var path = require('path'); @@ -9,13 +9,13 @@ app.use(app.router); app.use(loopback.rest()); app.configure(function () { - app.set('port', process.env.PORT || 3000); + app.set('port', process.env.PORT || 3000); }); var ds = loopback.createDataSource({ - connector: require('../index'), - provider: 'filesystem', - root: path.join(__dirname, 'storage') + connector: require('../index'), + provider: 'filesystem', + root: path.join(__dirname, 'storage') }); var container = ds.createModel('container'); @@ -23,17 +23,17 @@ var container = ds.createModel('container'); app.model(container); app.get('/', function (req, res, next) { - res.setHeader('Content-Type', 'text/html'); - var form = "

Storage Service Demo

" + - "List all containers

" + - "Upload to container c1:

" + - "

" - + "File to upload:
" - + "Notes about the file:
" - + "
" + - ""; - res.send(form); - res.end(); + res.setHeader('Content-Type', 'text/html'); + var form = "

Storage Service Demo

" + + "List all containers

" + + "Upload to container c1:

" + + "

" + + "File to upload:
" + + "Notes about the file:
" + + "
" + + ""; + res.send(form); + res.end(); }); app.listen(app.get('port')); diff --git a/example/upload-amazon.js b/example/upload-amazon.js index 7e60997..5122acd 100644 --- a/example/upload-amazon.js +++ b/example/upload-amazon.js @@ -4,75 +4,75 @@ var express = require('express'); var app = express(); app.configure(function () { - app.set('port', process.env.PORT || 3001); - app.set('views', __dirname + '/views'); - app.set('view engine', 'ejs'); - app.use(express.favicon()); - // app.use(express.logger('dev')); - app.use(express.methodOverride()); - app.use(app.router); + app.set('port', process.env.PORT || 3001); + app.set('views', __dirname + '/views'); + app.set('view engine', 'ejs'); + app.use(express.favicon()); + // app.use(express.logger('dev')); + app.use(express.methodOverride()); + app.use(app.router); }); var handler = new StorageService( -{ + { provider: 'amazon', key: 'your-amazon-key', keyId: 'your-amazon-key-id' -}); + }); app.get('/', function (req, res, next) { - res.setHeader('Content-Type', 'text/html'); - var form = "

Storage Service Demo

" + - "List all containers

" + - "Upload to container con1:

" + - "

" - + "File to upload:
" - + "Notes about the file:
" - + "
" + - ""; - res.send(form); - res.end(); + res.setHeader('Content-Type', 'text/html'); + var form = "

Storage Service Demo

" + + "List all containers

" + + "Upload to container con1:

" + + "

" + + "File to upload:
" + + "Notes about the file:
" + + "
" + + ""; + res.send(form); + res.end(); }); app.post('/upload/:container', function (req, res, next) { - handler.upload(req, res, function (err, result) { - if (!err) { - res.setHeader('Content-Type', 'application/json'); - res.send(200, result); - } else { - res.send(500, err); - } - }); + handler.upload(req, res, function (err, result) { + if (!err) { + res.setHeader('Content-Type', 'application/json'); + res.send(200, result); + } else { + res.send(500, err); + } + }); }); app.get('/download', function (req, res, next) { - handler.getContainers(function (err, containers) { - var html = "

Containers

    "; - containers.forEach(function (f) { - html += "
  • " + f.name + "
  • " - }); - html += "

Home

"; - res.send(200, html); + handler.getContainers(function (err, containers) { + var html = "

Containers

    "; + containers.forEach(function (f) { + html += "
  • " + f.name + "
  • " }); + html += "

Home

"; + res.send(200, html); + }); }); app.get('/download/:container', function (req, res, next) { - handler.getFiles(req.params.container, function (err, files) { - var html = "

Files in container " + req.params.container + "

Home

"; - res.send(200, html); + handler.getFiles(req.params.container, function (err, files) { + var html = "

Files in container " + req.params.container + "

Home

"; + res.send(200, html); + }); }); app.get('/download/:container/:file', function (req, res, next) { - handler.download(req, res, function (err, result) { - if (err) { - res.send(500, err); - } - }); + handler.download(req, res, function (err, result) { + if (err) { + res.send(500, err); + } + }); }); app.listen(app.get('port')); diff --git a/example/upload.js b/example/upload.js index efb28cf..105c038 100644 --- a/example/upload.js +++ b/example/upload.js @@ -4,13 +4,13 @@ var express = require('express'); var app = express(); app.configure(function () { - app.set('port', process.env.PORT || 3001); - app.set('views', __dirname + '/views'); - app.set('view engine', 'ejs'); - app.use(express.favicon()); - // app.use(express.logger('dev')); - app.use(express.methodOverride()); - app.use(app.router); + app.set('port', process.env.PORT || 3001); + app.set('views', __dirname + '/views'); + app.set('view engine', 'ejs'); + app.use(express.favicon()); + // app.use(express.logger('dev')); + app.use(express.methodOverride()); + app.use(app.router); }); // Create the container @@ -20,58 +20,58 @@ mkdirp.sync('/tmp/storage/con1'); var handler = new StorageService({provider: 'filesystem', root: '/tmp/storage'}); app.get('/', function (req, res, next) { - res.setHeader('Content-Type', 'text/html'); - var form = "

Storage Service Demo

" + - "List all containers

" + - "Upload to container con1:

" + - "

" - + "File to upload:
" - + "Notes about the file:
" - + "
" + - ""; - res.send(form); - res.end(); + res.setHeader('Content-Type', 'text/html'); + var form = "

Storage Service Demo

" + + "List all containers

" + + "Upload to container con1:

" + + "

" + + "File to upload:
" + + "Notes about the file:
" + + "
" + + ""; + res.send(form); + res.end(); }); app.post('/upload/:container', function (req, res, next) { - handler.upload(req, res, function (err, result) { - if (!err) { - res.setHeader('Content-Type', 'application/json'); - res.send(200, result); - } else { - res.send(500, err); - } - }); + handler.upload(req, res, function (err, result) { + if (!err) { + res.setHeader('Content-Type', 'application/json'); + res.send(200, result); + } else { + res.send(500, err); + } + }); }); app.get('/download', function (req, res, next) { - handler.getContainers(function (err, containers) { - var html = "

Containers

    "; - containers.forEach(function (f) { - html += "
  • " + f.name + "
  • " - }); - html += "

Home

"; - res.send(200, html); + handler.getContainers(function (err, containers) { + var html = "

Containers

    "; + containers.forEach(function (f) { + html += "
  • " + f.name + "
  • " }); + html += "

Home

"; + res.send(200, html); + }); }); app.get('/download/:container', function (req, res, next) { - handler.getFiles(req.params.container, function (err, files) { - var html = "

Files in container " + req.params.container + "

Home

"; - res.send(200, html); + handler.getFiles(req.params.container, function (err, files) { + var html = "

Files in container " + req.params.container + "

Home

"; + res.send(200, html); + }); }); app.get('/download/:container/:file', function (req, res, next) { - handler.download(req, res, function (err, result) { - if (err) { - res.send(500, err); - } - }); + handler.download(req, res, function (err, result) { + if (err) { + res.send(500, err); + } + }); }); app.listen(app.get('port')); diff --git a/lib/factory.js b/lib/factory.js index f59de4f..dcea329 100644 --- a/lib/factory.js +++ b/lib/factory.js @@ -4,19 +4,19 @@ * @returns {*} */ function createClient(options) { - options = options || {}; - var provider = options.provider || 'filesystem'; - var handler; + options = options || {}; + var provider = options.provider || 'filesystem'; + var handler; - try { - // Try to load the provider from providers folder - handler = require('./providers/' + provider); - } catch (err) { - // Fall back to pkgcloud - handler = require('pkgcloud').storage; - } - - return handler.createClient(options); + try { + // Try to load the provider from providers folder + handler = require('./providers/' + provider); + } catch (err) { + // Fall back to pkgcloud + handler = require('pkgcloud').storage; + } + + return handler.createClient(options); } /** @@ -25,13 +25,13 @@ function createClient(options) { * @returns {*} */ function getProvider(provider) { - try { - // Try to load the provider from providers folder - return require('./providers/' + provider); - } catch (err) { - // Fall back to pkgcloud - return require('pkgcloud').providers[provider]; - } + try { + // Try to load the provider from providers folder + return require('./providers/' + provider); + } catch (err) { + // Fall back to pkgcloud + return require('pkgcloud').providers[provider]; + } } module.exports.createClient = createClient; diff --git a/lib/storage-connector.js b/lib/storage-connector.js index f72dc65..8894d0d 100644 --- a/lib/storage-connector.js +++ b/lib/storage-connector.js @@ -5,22 +5,24 @@ var StorageService = require('./storage-service'); * @param callback */ exports.initialize = function (dataSource, callback) { - var settings = dataSource.settings || {}; + var settings = dataSource.settings || {}; - var connector = new StorageService(settings); - dataSource.connector = connector; - dataSource.connector.dataSource = dataSource; + var connector = new StorageService(settings); + dataSource.connector = connector; + dataSource.connector.dataSource = dataSource; - connector.DataAccessObject = function() {}; - for (var m in StorageService.prototype) { - var method = StorageService.prototype[m]; - if ('function' === typeof method) { - connector.DataAccessObject[m] = method.bind(connector); - for(var k in method) { - connector.DataAccessObject[m][k] = method[k]; - } - } + connector.DataAccessObject = function () { + }; + for (var m in StorageService.prototype) { + var method = StorageService.prototype[m]; + if ('function' === typeof method) { + connector.DataAccessObject[m] = method.bind(connector); + for (var k in method) { + connector.DataAccessObject[m][k] = method[k]; + } } + } - connector.define = function(model, properties, settings) {}; + connector.define = function (model, properties, settings) { + }; }; diff --git a/lib/storage-handler.js b/lib/storage-handler.js index 2c48030..2cb9118 100644 --- a/lib/storage-handler.js +++ b/lib/storage-handler.js @@ -10,96 +10,96 @@ var StringDecoder = require('string_decoder').StringDecoder; * @param {Function} cb The callback */ exports.upload = function (provider, req, res, container, cb) { - var form = new IncomingForm(this.options); - container = container || req.params.container; - var fields = {}, files = {}; - form.handlePart = function (part) { - var self = this; + var form = new IncomingForm(this.options); + container = container || req.params.container; + var fields = {}, files = {}; + form.handlePart = function (part) { + var self = this; - if (part.filename === undefined) { - var value = '' - , decoder = new StringDecoder(this.encoding); + if (part.filename === undefined) { + var value = '' + , decoder = new StringDecoder(this.encoding); - part.on('data', function (buffer) { - self._fieldsSize += buffer.length; - if (self._fieldsSize > self.maxFieldsSize) { - self._error(new Error('maxFieldsSize exceeded, received ' + self._fieldsSize + ' bytes of field data')); - return; - } - value += decoder.write(buffer); - }); - - part.on('end', function () { - var values = fields[part.name]; - if(values === undefined) { - values = [value]; - fields[part.name] = values; - } else { - values.push(value); - } - self.emit('field', part.name, value); - }); - return; + part.on('data', function (buffer) { + self._fieldsSize += buffer.length; + if (self._fieldsSize > self.maxFieldsSize) { + self._error(new Error('maxFieldsSize exceeded, received ' + self._fieldsSize + ' bytes of field data')); + return; } + value += decoder.write(buffer); + }); - this._flushing++; - - var file = { - container: container, - name: part.filename, - type: part.mime - }; - - self.emit('fileBegin', part.name, file); - - var headers = {}; - if('content-type' in part.headers) { - headers['content-type'] = part.headers['content-type']; + part.on('end', function () { + var values = fields[part.name]; + if (values === undefined) { + values = [value]; + fields[part.name] = values; + } else { + values.push(value); } - var writer = provider.upload({container: container, remote: part.filename}); + self.emit('field', part.name, value); + }); + return; + } - var endFunc = function () { - self._flushing--; - var values = files[part.name]; - if(values === undefined) { - values = [file]; - files[part.name] = values; - } else { - values.push(file); - } - self.emit('file', part.name, file); - self._maybeEnd(); - }; + this._flushing++; - /* - part.on('data', function (buffer) { - self.pause(); - writer.write(buffer, function () { - // pkgcloud stream doesn't make callbacks - }); - self.resume(); - }); - - part.on('end', function () { - - writer.end(); // pkgcloud stream doesn't make callbacks - endFunc(); - }); - */ - - part.pipe(writer, { end: false }); - part.on("end", function() { - writer.end(); - endFunc(); - }); + var file = { + container: container, + name: part.filename, + type: part.mime }; - form.parse(req, function (err, _fields, _files) { - if(err) { - console.error(err); - } - cb && cb(err, {files: files, fields: fields}); + self.emit('fileBegin', part.name, file); + + var headers = {}; + if ('content-type' in part.headers) { + headers['content-type'] = part.headers['content-type']; + } + var writer = provider.upload({container: container, remote: part.filename}); + + var endFunc = function () { + self._flushing--; + var values = files[part.name]; + if (values === undefined) { + values = [file]; + files[part.name] = values; + } else { + values.push(file); + } + self.emit('file', part.name, file); + self._maybeEnd(); + }; + + /* + part.on('data', function (buffer) { + self.pause(); + writer.write(buffer, function () { + // pkgcloud stream doesn't make callbacks + }); + self.resume(); + }); + + part.on('end', function () { + + writer.end(); // pkgcloud stream doesn't make callbacks + endFunc(); + }); + */ + + part.pipe(writer, { end: false }); + part.on("end", function () { + writer.end(); + endFunc(); }); + }; + + form.parse(req, function (err, _fields, _files) { + if (err) { + console.error(err); + } + cb && cb(err, {files: files, fields: fields}); + }); } /** @@ -111,15 +111,15 @@ exports.upload = function (provider, req, res, container, cb) { * @param {String} file The file name * @param {Function} cb The callback */ -exports.download = function(provider, req, res, container, file, cb) { - var reader = provider.download({ - container: container || req && req.params.container, - remote: file || req && req.params.file - }); - reader.pipe(res); - reader.on('error', function(err) { - res.send(500, { error: err }); - }); +exports.download = function (provider, req, res, container, file, cb) { + var reader = provider.download({ + container: container || req && req.params.container, + remote: file || req && req.params.file + }); + reader.pipe(res); + reader.on('error', function (err) { + res.send(500, { error: err }); + }); } diff --git a/lib/storage-service.js b/lib/storage-service.js index ff30886..fa83d6f 100644 --- a/lib/storage-service.js +++ b/lib/storage-service.js @@ -24,22 +24,22 @@ function StorageService(options) { function map(obj) { return obj; /* - if (!obj || typeof obj !== 'object') { - return obj; - } - var data = {}; - for (var i in obj) { - if (obj.hasOwnProperty(i) && typeof obj[i] !== 'function' - && typeof obj[i] !== 'object') { - if (i === 'newListener' || i === 'delimiter' || i === 'wildcard') { - // Skip properties from the base class - continue; - } - data[i] = obj[i]; - } - } - return data; - */ + if (!obj || typeof obj !== 'object') { + return obj; + } + var data = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i) && typeof obj[i] !== 'function' + && typeof obj[i] !== 'object') { + if (i === 'newListener' || i === 'delimiter' || i === 'wildcard') { + // Skip properties from the base class + continue; + } + data[i] = obj[i]; + } + } + return data; + */ } StorageService.prototype.getContainers = function (cb) { diff --git a/test/fs.test.js b/test/fs.test.js index 2ea5a47..27f19f3 100644 --- a/test/fs.test.js +++ b/test/fs.test.js @@ -5,143 +5,143 @@ var path = require('path'); describe('FileSystem based storage provider', function () { - describe('container apis', function () { - var client = null; - it('should require an existing directory as the root', function (done) { - client = new FileSystemProvider({root: path.join(__dirname, 'storage')}); - process.nextTick(done); - }); - - it('should complain if the root directory doesn\'t exist', function (done) { - try { - client = new FileSystemProvider({root: path.join(__dirname, '_storage')}); - process.nextTick(done.bind(null, 'Error')); - } catch (err) { - // Should be here - process.nextTick(done); - } - }); - - it('should return an empty list of containers', function (done) { - client.getContainers(function (err, containers) { - assert(!err); - assert.equal(0, containers.length); - done(err, containers); - }); - }); - - it('should create a new container', function (done) { - client.createContainer({name: 'c1'}, function (err, container) { - assert(!err); - done(err, container); - }); - }); - - it('should get a container c1', function (done) { - client.getContainer('c1', function (err, container) { - assert(!err); - done(err, container); - }); - }); - - it('should not get a container c2', function (done) { - client.getContainer('c2', function (err, container) { - assert(err); - done(null, container); - }); - }); - - it('should return one container', function (done) { - client.getContainers(function (err, containers) { - assert(!err); - assert.equal(1, containers.length); - done(err, containers); - }); - }); - - it('should destroy a container c1', function (done) { - client.destroyContainer('c1', function (err, container) { - assert(!err); - done(err, container); - }); - }); - - it('should not get a container c1 after destroy', function (done) { - client.getContainer('c1', function (err, container) { - assert(err); - done(null, container); - }); - }); + describe('container apis', function () { + var client = null; + it('should require an existing directory as the root', function (done) { + client = new FileSystemProvider({root: path.join(__dirname, 'storage')}); + process.nextTick(done); }); - describe('file apis', function () { - var fs = require('fs'); - var client = new FileSystemProvider({root: path.join(__dirname, 'storage')}); - - it('should create a new container', function (done) { - client.createContainer({name: 'c1'}, function (err, container) { - assert(!err); - done(err, container); - }); - }); - - it('should upload a file', function (done) { - var writer = client.upload({container: 'c1', remote: 'f1.txt'}); - fs.createReadStream(path.join(__dirname, 'files/f1.txt')).pipe(writer); - writer.on('finish', done); - writer.on('error', done); - }); - - it('should download a file', function (done) { - var reader = client.download({ - container: 'c1', - remote: 'f1.txt' - }); - reader.pipe(fs.createWriteStream(path.join(__dirname, 'files/f1_downloaded.txt'))); - reader.on('end', done); - reader.on('error', done); - }); - - it('should get files for a container', function (done) { - client.getFiles('c1', function (err, files) { - assert(!err); - assert.equal(1, files.length); - done(err, files); - }); - }); - - it('should get a file', function (done) { - client.getFile('c1', 'f1.txt', function (err, f) { - assert(!err); - assert.ok(f); - done(err, f); - }); - }); - - it('should remove a file', function (done) { - client.removeFile('c1', 'f1.txt', function (err) { - assert(!err); - done(err); - }); - }); - - it('should get no files from a container', function (done) { - client.getFiles('c1', function (err, files) { - assert(!err); - assert.equal(0, files.length); - done(err, files); - }); - }); - - it('should destroy a container c1', function (done) { - client.destroyContainer('c1', function (err, container) { - // console.error(err); - assert(!err); - done(err, container); - }); - }); - + it('should complain if the root directory doesn\'t exist', function (done) { + try { + client = new FileSystemProvider({root: path.join(__dirname, '_storage')}); + process.nextTick(done.bind(null, 'Error')); + } catch (err) { + // Should be here + process.nextTick(done); + } }); + + it('should return an empty list of containers', function (done) { + client.getContainers(function (err, containers) { + assert(!err); + assert.equal(0, containers.length); + done(err, containers); + }); + }); + + it('should create a new container', function (done) { + client.createContainer({name: 'c1'}, function (err, container) { + assert(!err); + done(err, container); + }); + }); + + it('should get a container c1', function (done) { + client.getContainer('c1', function (err, container) { + assert(!err); + done(err, container); + }); + }); + + it('should not get a container c2', function (done) { + client.getContainer('c2', function (err, container) { + assert(err); + done(null, container); + }); + }); + + it('should return one container', function (done) { + client.getContainers(function (err, containers) { + assert(!err); + assert.equal(1, containers.length); + done(err, containers); + }); + }); + + it('should destroy a container c1', function (done) { + client.destroyContainer('c1', function (err, container) { + assert(!err); + done(err, container); + }); + }); + + it('should not get a container c1 after destroy', function (done) { + client.getContainer('c1', function (err, container) { + assert(err); + done(null, container); + }); + }); + }); + + describe('file apis', function () { + var fs = require('fs'); + var client = new FileSystemProvider({root: path.join(__dirname, 'storage')}); + + it('should create a new container', function (done) { + client.createContainer({name: 'c1'}, function (err, container) { + assert(!err); + done(err, container); + }); + }); + + it('should upload a file', function (done) { + var writer = client.upload({container: 'c1', remote: 'f1.txt'}); + fs.createReadStream(path.join(__dirname, 'files/f1.txt')).pipe(writer); + writer.on('finish', done); + writer.on('error', done); + }); + + it('should download a file', function (done) { + var reader = client.download({ + container: 'c1', + remote: 'f1.txt' + }); + reader.pipe(fs.createWriteStream(path.join(__dirname, 'files/f1_downloaded.txt'))); + reader.on('end', done); + reader.on('error', done); + }); + + it('should get files for a container', function (done) { + client.getFiles('c1', function (err, files) { + assert(!err); + assert.equal(1, files.length); + done(err, files); + }); + }); + + it('should get a file', function (done) { + client.getFile('c1', 'f1.txt', function (err, f) { + assert(!err); + assert.ok(f); + done(err, f); + }); + }); + + it('should remove a file', function (done) { + client.removeFile('c1', 'f1.txt', function (err) { + assert(!err); + done(err); + }); + }); + + it('should get no files from a container', function (done) { + client.getFiles('c1', function (err, files) { + assert(!err); + assert.equal(0, files.length); + done(err, files); + }); + }); + + it('should destroy a container c1', function (done) { + client.destroyContainer('c1', function (err, container) { + // console.error(err); + assert(!err); + done(err, container); + }); + }); + + }); }); diff --git a/test/storage-service.test.js b/test/storage-service.test.js index 1d756de..f2ea79b 100644 --- a/test/storage-service.test.js +++ b/test/storage-service.test.js @@ -7,124 +7,124 @@ var storageService = new StorageService({root: path.join(__dirname, 'storage'), describe('Storage service', function () { - describe('container apis', function () { + describe('container apis', function () { - it('should return an empty list of containers', function (done) { - storageService.getContainers(function (err, containers) { - assert(!err); - assert.equal(0, containers.length); - done(err, containers); - }); - }); - - it('should create a new container', function (done) { - storageService.createContainer({name: 'c1'}, function (err, container) { - assert(!err); - done(err, container); - }); - }); - - it('should get a container c1', function (done) { - storageService.getContainer('c1', function (err, container) { - assert(!err); - done(err, container); - }); - }); - - it('should not get a container c2', function (done) { - storageService.getContainer('c2', function (err, container) { - assert(err); - done(null, container); - }); - }); - - it('should return one container', function (done) { - storageService.getContainers(function (err, containers) { - assert(!err); - assert.equal(1, containers.length); - done(err, containers); - }); - }); - - it('should destroy a container c1', function (done) { - storageService.destroyContainer('c1', function (err, container) { - assert(!err); - done(err, container); - }); - }); - - it('should not get a container c1 after destroy', function (done) { - storageService.getContainer('c1', function (err, container) { - assert(err); - done(null, container); - }); - }); + it('should return an empty list of containers', function (done) { + storageService.getContainers(function (err, containers) { + assert(!err); + assert.equal(0, containers.length); + done(err, containers); + }); }); - describe('file apis', function () { - var fs = require('fs'); - - it('should create a new container', function (done) { - storageService.createContainer({name: 'c1'}, function (err, container) { - assert(!err); - done(err, container); - }); - }); - - it('should upload a file', function (done) { - var writer = storageService.uploadStream('c1', 'f1.txt'); - fs.createReadStream(path.join(__dirname, 'files/f1.txt')).pipe(writer); - writer.on('finish', done); - writer.on('error', done); - }); - - it('should download a file', function (done) { - var reader = storageService.downloadStream('c1','f1.txt'); - reader.pipe(fs.createWriteStream(path.join(__dirname, 'files/f1_downloaded.txt'))); - reader.on('end', done); - reader.on('error', done); - }); - - it('should get files for a container', function (done) { - storageService.getFiles('c1', function (err, files) { - assert(!err); - assert.equal(1, files.length); - done(err, files); - }); - }); - - it('should get a file', function (done) { - storageService.getFile('c1', 'f1.txt', function (err, f) { - assert(!err); - assert.ok(f); - done(err, f); - }); - }); - - it('should remove a file', function (done) { - storageService.removeFile('c1', 'f1.txt', function (err) { - assert(!err); - done(err); - }); - }); - - it('should get no files from a container', function (done) { - storageService.getFiles('c1', function (err, files) { - assert(!err); - assert.equal(0, files.length); - done(err, files); - }); - }); - - it('should destroy a container c1', function (done) { - storageService.destroyContainer('c1', function (err, container) { - // console.error(err); - assert(!err); - done(err, container); - }); - }); - + it('should create a new container', function (done) { + storageService.createContainer({name: 'c1'}, function (err, container) { + assert(!err); + done(err, container); + }); }); + + it('should get a container c1', function (done) { + storageService.getContainer('c1', function (err, container) { + assert(!err); + done(err, container); + }); + }); + + it('should not get a container c2', function (done) { + storageService.getContainer('c2', function (err, container) { + assert(err); + done(null, container); + }); + }); + + it('should return one container', function (done) { + storageService.getContainers(function (err, containers) { + assert(!err); + assert.equal(1, containers.length); + done(err, containers); + }); + }); + + it('should destroy a container c1', function (done) { + storageService.destroyContainer('c1', function (err, container) { + assert(!err); + done(err, container); + }); + }); + + it('should not get a container c1 after destroy', function (done) { + storageService.getContainer('c1', function (err, container) { + assert(err); + done(null, container); + }); + }); + }); + + describe('file apis', function () { + var fs = require('fs'); + + it('should create a new container', function (done) { + storageService.createContainer({name: 'c1'}, function (err, container) { + assert(!err); + done(err, container); + }); + }); + + it('should upload a file', function (done) { + var writer = storageService.uploadStream('c1', 'f1.txt'); + fs.createReadStream(path.join(__dirname, 'files/f1.txt')).pipe(writer); + writer.on('finish', done); + writer.on('error', done); + }); + + it('should download a file', function (done) { + var reader = storageService.downloadStream('c1', 'f1.txt'); + reader.pipe(fs.createWriteStream(path.join(__dirname, 'files/f1_downloaded.txt'))); + reader.on('end', done); + reader.on('error', done); + }); + + it('should get files for a container', function (done) { + storageService.getFiles('c1', function (err, files) { + assert(!err); + assert.equal(1, files.length); + done(err, files); + }); + }); + + it('should get a file', function (done) { + storageService.getFile('c1', 'f1.txt', function (err, f) { + assert(!err); + assert.ok(f); + done(err, f); + }); + }); + + it('should remove a file', function (done) { + storageService.removeFile('c1', 'f1.txt', function (err) { + assert(!err); + done(err); + }); + }); + + it('should get no files from a container', function (done) { + storageService.getFiles('c1', function (err, files) { + assert(!err); + assert.equal(0, files.length); + done(err, files); + }); + }); + + it('should destroy a container c1', function (done) { + storageService.destroyContainer('c1', function (err, container) { + // console.error(err); + assert(!err); + done(err, container); + }); + }); + + }); }); From 0d09a7224254cf9ecf9f096c78106d907215af0f Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 24 Jan 2014 10:19:03 -0800 Subject: [PATCH 06/26] Extract cloud credentials --- example/app-cloud.js | 9 +++++---- example/providers.json | 10 ++++++++++ example/upload-amazon.js | 6 +++--- 3 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 example/providers.json diff --git a/example/app-cloud.js b/example/app-cloud.js index 3e8c7b1..fe9fddc 100644 --- a/example/app-cloud.js +++ b/example/app-cloud.js @@ -1,10 +1,11 @@ var StorageService = require('../').StorageService; var path = require('path'); +var providers = require('./providers.json'); var rs = StorageService({ provider: 'rackspace', - username: 'strongloop', - apiKey: 'your-rackspace-api-key' + username: providers.rackspace.username, + apiKey: providers.rackspace.apiKey }); // Container @@ -40,8 +41,8 @@ rs.getContainers(function (err, containers) { var s3 = StorageService({ provider: 'amazon', - key: 'your-amazon-key', - keyId: 'your-amazon-key-id' + key: providers.amazon.key, + keyId: providers.amazon.keyId }); s3.getContainers(function (err, containers) { diff --git a/example/providers.json b/example/providers.json new file mode 100644 index 0000000..14f76bf --- /dev/null +++ b/example/providers.json @@ -0,0 +1,10 @@ +{ + "rackspace": { + "username": "strongloop", + "apiKey": "your-rackspace-api-key" + }, + "amazon": { + "key": "your-amazon-key", + "keyId": "your-amazon-key-id" + } +} diff --git a/example/upload-amazon.js b/example/upload-amazon.js index 5122acd..2cdae8e 100644 --- a/example/upload-amazon.js +++ b/example/upload-amazon.js @@ -1,5 +1,5 @@ var StorageService = require('../').StorageService; - +var providers = require('./providers.json'); var express = require('express'); var app = express(); @@ -16,8 +16,8 @@ app.configure(function () { var handler = new StorageService( { provider: 'amazon', - key: 'your-amazon-key', - keyId: 'your-amazon-key-id' + key: providers.amazon.key, + keyId: providers.amazon.keyId }); app.get('/', function (req, res, next) { From e853235e82c05bf1077fc21c13b575d00950264c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 27 Jan 2014 08:42:29 -0800 Subject: [PATCH 07/26] Fix the samples --- example/upload-amazon.js | 2 +- example/upload.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/upload-amazon.js b/example/upload-amazon.js index 2cdae8e..96bd035 100644 --- a/example/upload-amazon.js +++ b/example/upload-amazon.js @@ -68,7 +68,7 @@ app.get('/download/:container', function (req, res, next) { }); app.get('/download/:container/:file', function (req, res, next) { - handler.download(req, res, function (err, result) { + handler.download(req.params.container, req.params.file, res, function (err, result) { if (err) { res.send(500, err); } diff --git a/example/upload.js b/example/upload.js index 105c038..70183ed 100644 --- a/example/upload.js +++ b/example/upload.js @@ -67,7 +67,7 @@ app.get('/download/:container', function (req, res, next) { }); app.get('/download/:container/:file', function (req, res, next) { - handler.download(req, res, function (err, result) { + handler.download(req.params.container, req.params.file, res, function (err, result) { if (err) { res.send(500, err); } From eb0d9a11bf11aa02c1e8ab1c26bdbba104ca67ae Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 29 Jan 2014 22:39:01 -0800 Subject: [PATCH 08/26] Set Content-Type based on the file path --- lib/storage-handler.js | 2 ++ test/upload-download.test.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/storage-handler.js b/lib/storage-handler.js index 2cb9118..1497ef0 100644 --- a/lib/storage-handler.js +++ b/lib/storage-handler.js @@ -116,8 +116,10 @@ exports.download = function (provider, req, res, container, file, cb) { container: container || req && req.params.container, remote: file || req && req.params.file }); + res.type(file); reader.pipe(res); reader.on('error', function (err) { + res.type('application/json'); res.send(500, { error: err }); }); } diff --git a/test/upload-download.test.js b/test/upload-download.test.js index ae3bb54..c975f15 100644 --- a/test/upload-download.test.js +++ b/test/upload-download.test.js @@ -48,6 +48,7 @@ describe('storage service', function () { request('http://localhost:3000') .get('/containers/album1/download/test.jpg') + .expect('Content-Type', 'image/jpeg') .expect(200, function (err, res) { done(); }); @@ -57,6 +58,7 @@ describe('storage service', function () { request('http://localhost:3000') .get('/containers/album1/download/test_not_exist.jpg') + .expect('Content-Type', /json/) .expect(500, function (err, res) { assert(res.body.error); done(); From 9c0885aa07b2743680fb3e9dbcfcafcfc954c6f7 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 3 Feb 2014 10:58:37 -0800 Subject: [PATCH 09/26] Fix remoting for container/file apis --- lib/storage-service.js | 2 +- test/upload-download.test.js | 98 ++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/lib/storage-service.js b/lib/storage-service.js index fa83d6f..aa26d91 100644 --- a/lib/storage-service.js +++ b/lib/storage-service.js @@ -148,7 +148,7 @@ StorageService.prototype.getContainer.http = StorageService.prototype.createContainer.shared = true; StorageService.prototype.createContainer.accepts = [ - {arg: 'options', type: 'object'} + {arg: 'options', type: 'object', http: {source: 'body'}} ]; StorageService.prototype.createContainer.returns = {arg: 'container', type: 'object', root: true}; StorageService.prototype.createContainer.http = diff --git a/test/upload-download.test.js b/test/upload-download.test.js index c975f15..a79aa85 100644 --- a/test/upload-download.test.js +++ b/test/upload-download.test.js @@ -29,6 +29,81 @@ describe('storage service', function () { server.close(); }); + it('should create a container', function (done) { + + request('http://localhost:3000') + .post('/containers') + .send({name: 'test-container'}) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert.equal(res.body.name, 'test-container'); + done(); + }); + }); + + it('should get a container', function (done) { + + request('http://localhost:3000') + .get('/containers/test-container') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert.equal(res.body.name, 'test-container'); + done(); + }); + }); + + it('should list containers', function (done) { + + request('http://localhost:3000') + .get('/containers') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert(Array.isArray(res.body)); + assert.equal(res.body.length, 2); + done(); + }); + }); + + it('should delete a container', function (done) { + + request('http://localhost:3000') + .del('/containers/test-container') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + done(); + }); + }); + + it('should list containers after delete', function (done) { + + request('http://localhost:3000') + .get('/containers') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert(Array.isArray(res.body)); + assert.equal(res.body.length, 1); + done(); + }); + }); + + it('should list files', function (done) { + + request('http://localhost:3000') + .get('/containers/album1/files') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert(Array.isArray(res.body)); + done(); + }); + }); + it('uploads files', function (done) { request('http://localhost:3000') @@ -44,6 +119,18 @@ describe('storage service', function () { }); }); + it('should get file by name', function (done) { + + request('http://localhost:3000') + .get('/containers/album1/files/test.jpg') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert.equal(res.body.name, 'test.jpg'); + done(); + }); + }); + it('downloads files', function (done) { request('http://localhost:3000') @@ -54,6 +141,17 @@ describe('storage service', function () { }); }); + it('should delete a file', function (done) { + + request('http://localhost:3000') + .del('/containers/album1/files/test.jpg') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + done(); + }); + }); + it('reports errors if it fails to find the file to download', function (done) { request('http://localhost:3000') From 1e51da1623ff41cac338f9ecbc8814e1847a0b40 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 6 Feb 2014 22:28:43 -0800 Subject: [PATCH 10/26] Upgrade deps --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ab8f91e..3879d61 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,15 @@ "test": "./node_modules/.bin/mocha --timeout 30000 test/*test.js" }, "dependencies": { - "pkgcloud": "~0.8.17", - "async": "~0.2.9" + "pkgcloud": "~0.9.0", + "async": "~0.2.10" }, "devDependencies": { - "express": "~3.4.0", + "express": "~3.4.8", "loopback": "1.x.x", "formidable": "~1.0.14", - "mocha": "~1.14.0", - "supertest": "~0.8.1", + "mocha": "~1.17.1", + "supertest": "~0.9.0", "mkdirp": "~0.3.5" }, "repository": { From 815acf355655314e3f7f11540e72a523609d605d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 17 Feb 2014 17:53:51 -0800 Subject: [PATCH 11/26] Update license to dual Artistic-2.0/StrongLoop --- LICENSE | 506 +++++++++++++++++++++++++++++++++++++++++++++++++-- package.json | 5 +- 2 files changed, 494 insertions(+), 17 deletions(-) diff --git a/LICENSE b/LICENSE index 4808ef3..715666e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,493 @@ -Copyright (c) 2013 StrongLoop, Inc. +Copyright (c) 2013-2014 StrongLoop, Inc. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +loopback-storage-service uses a 'dual license' model. Users may use +loopback-storage-service under the terms of the Artistic 2.0 license, or under +the StrongLoop License. The text of both is included below. -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +Artistic License 2.0 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +Copyright (c) 2000-2006, The Perl Foundation. + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +Preamble + +This license establishes the terms under which a given free software +Package may be copied, modified, distributed, and/or redistributed. +The intent is that the Copyright Holder maintains some artistic +control over the development of that Package while still keeping the +Package available as open source and free software. + +You are always permitted to make arrangements wholly outside of this +license directly with the Copyright Holder of a given Package. If the +terms of this license do not permit the full use that you propose to +make of the Package, you should contact the Copyright Holder and seek +a different licensing arrangement. + +Definitions + + "Copyright Holder" means the individual(s) or organization(s) + named in the copyright notice for the entire Package. + + "Contributor" means any party that has contributed code or other + material to the Package, in accordance with the Copyright Holder's + procedures. + + "You" and "your" means any person who would like to copy, + distribute, or modify the Package. + + "Package" means the collection of files distributed by the + Copyright Holder, and derivatives of that collection and/or of + those files. A given Package may consist of either the Standard + Version, or a Modified Version. + + "Distribute" means providing a copy of the Package or making it + accessible to anyone else, or in the case of a company or + organization, to others outside of your company or organization. + + "Distributor Fee" means any fee that you charge for Distributing + this Package or providing support for this Package to another + party. It does not mean licensing fees. + + "Standard Version" refers to the Package if it has not been + modified, or has been modified only in ways explicitly requested + by the Copyright Holder. + + "Modified Version" means the Package, if it has been changed, and + such changes were not explicitly requested by the Copyright + Holder. + + "Original License" means this Artistic License as Distributed with + the Standard Version of the Package, in its current version or as + it may be modified by The Perl Foundation in the future. + + "Source" form means the source code, documentation source, and + configuration files for the Package. + + "Compiled" form means the compiled bytecode, object code, binary, + or any other form resulting from mechanical transformation or + translation of the Source form. + + +Permission for Use and Modification Without Distribution + +(1) You are permitted to use the Standard Version and create and use +Modified Versions for any purpose without restriction, provided that +you do not Distribute the Modified Version. + + +Permissions for Redistribution of the Standard Version + +(2) You may Distribute verbatim copies of the Source form of the +Standard Version of this Package in any medium without restriction, +either gratis or for a Distributor Fee, provided that you duplicate +all of the original copyright notices and associated disclaimers. At +your discretion, such verbatim copies may or may not include a +Compiled form of the Package. + +(3) You may apply any bug fixes, portability changes, and other +modifications made available from the Copyright Holder. The resulting +Package will still be considered the Standard Version, and as such +will be subject to the Original License. + + +Distribution of Modified Versions of the Package as Source + +(4) You may Distribute your Modified Version as Source (either gratis +or for a Distributor Fee, and with or without a Compiled form of the +Modified Version) provided that you clearly document how it differs +from the Standard Version, including, but not limited to, documenting +any non-standard features, executables, or modules, and provided that +you do at least ONE of the following: + + (a) make the Modified Version available to the Copyright Holder + of the Standard Version, under the Original License, so that the + Copyright Holder may include your modifications in the Standard + Version. + + (b) ensure that installation of your Modified Version does not + prevent the user installing or running the Standard Version. In + addition, the Modified Version must bear a name that is different + from the name of the Standard Version. + + (c) allow anyone who receives a copy of the Modified Version to + make the Source form of the Modified Version available to others + under + + (i) the Original License or + + (ii) a license that permits the licensee to freely copy, + modify and redistribute the Modified Version using the same + licensing terms that apply to the copy that the licensee + received, and requires that the Source form of the Modified + Version, and of any works derived from it, be made freely + available in that license fees are prohibited but Distributor + Fees are allowed. + + +Distribution of Compiled Forms of the Standard Version +or Modified Versions without the Source + +(5) You may Distribute Compiled forms of the Standard Version without +the Source, provided that you include complete instructions on how to +get the Source of the Standard Version. Such instructions must be +valid at the time of your distribution. If these instructions, at any +time while you are carrying out such distribution, become invalid, you +must provide new instructions on demand or cease further distribution. +If you provide valid instructions or cease distribution within thirty +days after you become aware that the instructions are invalid, then +you do not forfeit any of your rights under this license. + +(6) You may Distribute a Modified Version in Compiled form without +the Source, provided that you comply with Section 4 with respect to +the Source of the Modified Version. + + +Aggregating or Linking the Package + +(7) You may aggregate the Package (either the Standard Version or +Modified Version) with other packages and Distribute the resulting +aggregation provided that you do not charge a licensing fee for the +Package. Distributor Fees are permitted, and licensing fees for other +components in the aggregation are permitted. The terms of this license +apply to the use and Distribution of the Standard or Modified Versions +as included in the aggregation. + +(8) You are permitted to link Modified and Standard Versions with +other works, to embed the Package in a larger work of your own, or to +build stand-alone binary or bytecode versions of applications that +include the Package, and Distribute the result without restriction, +provided the result does not expose a direct interface to the Package. + + +Items That are Not Considered Part of a Modified Version + +(9) Works (including, but not limited to, modules and scripts) that +merely extend or make use of the Package, do not, by themselves, cause +the Package to be a Modified Version. In addition, such works are not +considered parts of the Package itself, and are not subject to the +terms of this license. + + +General Provisions + +(10) Any use, modification, and distribution of the Standard or +Modified Versions is governed by this Artistic License. By using, +modifying or distributing the Package, you accept this license. Do not +use, modify, or distribute the Package, if you do not accept this +license. + +(11) If your Modified Version has been derived from a Modified +Version made by someone other than you, you are nevertheless required +to ensure that your Modified Version complies with the requirements of +this license. + +(12) This license does not grant you the right to use any trademark, +service mark, tradename, or logo of the Copyright Holder. + +(13) This license includes the non-exclusive, worldwide, +free-of-charge patent license to make, have made, use, offer to sell, +sell, import and otherwise transfer the Package with respect to any +patent claims licensable by the Copyright Holder that are necessarily +infringed by the Package. If you institute patent litigation +(including a cross-claim or counterclaim) against any party alleging +that the Package constitutes direct or contributory patent +infringement, then this Artistic License to you shall terminate on the +date that such litigation is filed. + +(14) Disclaimer of Warranty: +THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS +IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR +NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL +LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +StrongLoop License + +STRONGLOOP SUBSCRIPTION AGREEMENT +PLEASE READ THIS AGREEMENT CAREFULLY BEFORE YOU AGREE TO THESE TERMS. IF YOU +ARE ACTING ON BEHALF OF AN ENTITY, THEN YOU REPRESENT THAT YOU HAVE THE +AUTHORITY TO ENTER INTO THIS AGREEMENT ON BEHALF OF THAT ENTITY. IF YOU DO NOT +AGREE TO THESE TERMS, YOU SHOULD NOT AGREE TO THE TERMS OF THIS AGREEMENT OR +INSTALL OR USE THE SOFTWARE. +This StrongLoop Subscription Agreement ("Agreement") is made by and between +StrongLoop, Inc. ("StrongLoop") with its principal place of business at 107 S. +B St, Suite 220, San Mateo, CA 94401 and the person or entity entering into this +Agreement ("Customer"). The effective date ("Effective Date") of this Agreement +is the date Customer agrees to these terms or installs or uses the Software (as +defined below). This Agreement applies to Customer's use of the Software but it +shall be superseded by any signed agreement between you and StrongLoop +concerning the Software. +1. Subscriptions and Licenses. +1.1 Subscriptions. StrongLoop offers five different subscription levels to its +customers, each as more particularly described on StrongLoop's website located +at www.strongloop.com (the "StrongLoop Site"): (1) Free; (2) Developer; (3) +Professional; (4) Gold; and (5) Platinum. The actual subscription level +applicable to Customer (the "Subscription") will be specified in the purchase +order that Customer issues to StrongLoop. This Agreement applies to Customer +regardless of the level of the Subscription selected by Customer and whether or +not Customer upgrades or downgrades its Subscription. StrongLoop hereby agrees +to provide the services as described on the StrongLoop Site for each +Subscription level during the term for which Customer has purchased the +applicable Subscription, subject to Customer paying the fees applicable to the +Subscription level purchased, if any (the "Subscription Fees"). StrongLoop may +modify the services to be provided under any Subscription upon notice to +Customer. +1.2 License Grant. Subject to the terms and conditions of this Agreement, +StrongLoop grants to Customer, during the Subscription Term (as defined in +Section 7.1 (Term and Termination) of this Agreement, a limited, non-exclusive, +non-transferable right and license, to install and use the StrongLoop Suite +software (the "Software") and the documentation made available electronically as +part of the Software (the "Documentation"), either of which may be modified +during the Term (as defined in Section 7.1 below), solely for development, +production and commercial purposes so long as Customer is using the Software to +run only one process on a given operating system at a time. This Agreement, +including but not limited to the license and restrictions contained herein, +apply to Customer regardless of whether Customer accesses the Software via +download from the StrongLoop Site or through a third-party website or service, +even if Customer acquired the Software prior to agreeing to this Agreement. +1.3 License Restrictions. Customer shall not itself, or through any parent, +subsidiary, affiliate, agent or other third party: + 1.3.1 sell, lease, license, distribute, sublicense or otherwise transfer + in whole or in part, any Software or the Documentation to a third party; + or + 1.3.2 decompile, disassemble, translate, reverse engineer or otherwise + attempt to derive source code from the Software, in whole or in part, nor + shall Customer use any mechanical, electronic or other method to trace, + decompile, disassemble, or identify the source code of the Software or + encourage others to do so, except to the limited extent, if any, that + applicable law permits such acts notwithstanding any contractual + prohibitions, provided, however, before Customer exercises any rights that + Customer believes to be entitled to based on mandatory law, Customer shall + provide StrongLoop with thirty (30) days prior written notice and provide + all reasonably requested information to allow StrongLoop to assess + Customer's claim and, at StrongLoop's sole discretion, to provide + alternatives that reduce any adverse impact on StrongLoop's intellectual + property or other rights; or + 1.3.3 allow access or permit use of the Software by any users other than + Customer's employees or authorized third-party contractors who are + providing services to Customer and agree in writing to abide by the terms + of this Agreement, provided further that Customer shall be liable for any + failure by such employees and third-party contractors to comply with the + terms of this Agreement and no usage restrictions, if any, shall be + exceeded; or + 1.3.4 create, develop, license, install, use, or deploy any third party + software or services to circumvent or provide access, permissions or + rights which violate the license keys embedded within the Software; or + 1.3.5 modify or create derivative works based upon the Software or + Documentation; or disclose the results of any benchmark test of the + Software to any third party without StrongLoop's prior written approval; + or + 1.3.6 change any proprietary rights notices which appear in the Software + or Documentation; or + 1.3.7 use the Software as part of a time sharing or service bureau + purposes or in any other resale capacity. +1.4 Third-Party Software. The Software may include individual certain software +that is owned by third parties, including individual open source software +components (the "Third-Party Software"), each of which has its own copyright and +its own applicable license conditions. Such third-party software is licensed to +Customer under the terms of the applicable third-party licenses and/or copyright +notices that can be found in the LICENSES file, the Documentation or other +materials accompanying the Software, except that Sections 5 (Warranty +Disclaimer) and 6 (Limitation of Liability) also govern Customer's use of the +third-party software. Customer agrees to comply with the terms and conditions +of the relevant third-party software licenses. +2. Support Services. StrongLoop has no obligation to provide any support for +the Software other than the support services specifically described on the +StrongLoop Site for the Subscription level procured by Customer. However, +StrongLoop has endeavored to establish a community of users of the Software who +have provided their own feedback, hints and advice regarding their experiences +in using the Software. You can find that community and user feedback on the +StrongLoop Site. The use of any information, content or other materials from, +contained in or on the StrongLoop Site are subject to the StrongLoop website +terms of use located here http://www.strongloop.com/terms-of-service. +3. Confidentiality. For purposes of this Agreement, "Confidential Information" +means any and all information or proprietary materials (in every form and media) +not generally known in the relevant trade or industry and which has been or is +hereafter disclosed or made available by StrongLoop to Customer in connection +with the transactions contemplated under this Agreement, including (i) all trade +secrets, (ii) existing or contemplated Software, services, designs, technology, +processes, technical data, engineering, techniques, methodologies and concepts +and any related information, and (iii) information relating to business plans, +sales or marketing methods and customer lists or requirements. For a period of +five (5) years from the date of disclosure of the applicable Confidential +Information, Customer shall (i) hold the Confidential Information in trust and +confidence and avoid the disclosure or release thereof to any other person or +entity by using the same degree of care as it uses to avoid unauthorized use, +disclosure, or dissemination of its own Confidential Information of a similar +nature, but not less than reasonable care, and (ii) not use the Confidential +Information for any purpose whatsoever except as expressly contemplated under +this Agreement; provided that, to the extent the Confidential Information +constitutes a trade secret under law, Customer agrees to protect such +information for so long as it qualifies as a trade secret under applicable law. +Customer shall disclose the Confidential Information only to those of its +employees and contractors having a need to know such Confidential Information +and shall take all reasonable precautions to ensure that such employees and +contractors comply with the provisions of this Section. The obligations of +Customer under this Section shall not apply to information that Customer can +demonstrate (i) was in its possession at the time of disclosure and without +restriction as to confidentiality, (ii) at the time of disclosure is generally +available to the public or after disclosure becomes generally available to the +public through no breach of agreement or other wrongful act by Customer, (iii) +has been received from a third party without restriction on disclosure and +without breach of agreement by Customer, or (iv) is independently developed by +Customer without regard to the Confidential Information. In addition, Customer +may disclose Confidential Information as required to comply with binding orders +of governmental entities that have jurisdiction over it; provided that Customer +gives StrongLoop reasonable written notice to allow StrongLoop to seek a +protective order or other appropriate remedy, discloses only such Confidential +Information as is required by the governmental entity, and uses commercially +reasonable efforts to obtain confidential treatment for any Confidential +Information disclosed. Notwithstanding the above, Customer agrees that +StrongLoop, its employees and agents shall be free to use and employ their +general skills, know-how, and expertise, and to use, disclose, and employ any +generalized ideas, concepts, know-how, methods, techniques or skills gained or +learned during the Term or thereafter. +4. Ownership. StrongLoop shall retain all intellectual property and proprietary +rights in the Software, Documentation, and related works, including but not +limited to any derivative work of the foregoing and StrongLoop's licensors shall +retain all intellectual property and proprietary rights in any Third-Party +Software that may be provided with or as a part of the Software. Customer shall +do nothing inconsistent with StrongLoop's or its licensors' title to the +Software and the intellectual property rights embodied therein, including, but +not limited to, transferring, loaning, selling, assigning, pledging, or +otherwise disposing, encumbering, or suffering a lien or encumbrance upon or +against any interest in the Software. The Software (including any Third-Party +Software) contain copyrighted material, trade secrets and other proprietary +material of StrongLoop and/or its licensors. +5. Warranty Disclaimer. THE SOFTWARE (INCLUDING ANY THIRD-PARTY SOFTWARE) AND +DOCUMENTATION MADE AVAILABLE TO CUSTOMER ARE PROVIDED "AS-IS" AND STRONGLOOP, +ON BEHALF OF ITSELF AND ITS LICENSORS, EXPRESSLY DISCLAIMS ALL WARRANTIES OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE, +PERFORMANCE, AND ACCURACY AND ANY IMPLIED WARRANTIES ARISING FROM STATUTE, +COURSE OF DEALING, COURSE OF PERFORMANCE, OR USAGE OF TRADE. STRONGLOOP DOES +NOT WARRANT THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR +ERROR-FREE, THAT DEFECTS IN THE SOFTWARE WILL BE CORRECTED OR THAT THE SOFTWARE +WILL PROVIDE OR ENSURE ANY PARTICULAR RESULTS OR OUTCOME. NO ORAL OR WRITTEN +INFORMATION OR ADVICE GIVEN BY STRONGLOOP OR ITS AUTHORIZED REPRESENTATIVES +SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF THIS WARRANTY. +STRONGLOOP IS NOT OBLIGATED TO PROVIDE CUSTOMER WITH UPGRADES TO THE SOFTWARE, +BUT MAY ELECT TO DO SO IN ITS SOLE DISCRETION. SOME JURISDICTIONS DO NOT ALLOW +THE EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT APPLY TO +CUSTOMER.WITHOUT LIMITING THE GENERALITY OF THE FOREGOING DISCLAIMER, THE +SOFTWARE AND DOCUMENTATION ARE NOT DESIGNED, MANUFACTURED OR INTENDED FOR USE IN +THE PLANNING, CONSTRUCTION, MAINTENANCE, CONTROL, OR DIRECT OPERATION OF NUCLEAR +FACILITIES, AIRCRAFT NAVIGATION, CONTROL OR COMMUNICATION SYSTEMS, WEAPONS +SYSTEMS, OR DIRECT LIFE SUPPORT SYSTEMS. +6. Limitation of Liability. + 6.1 Exclusion of Liability. IN NO EVENT WILL STRONGLOOP OR ITS LICENSORS + BE LIABLE UNDER THIS AGREEMENT FOR ANY INDIRECT, RELIANCE, PUNITIVE, + CONSEQUENTIAL, SPECIAL, EXEMPLARY, OR INCIDENTAL DAMAGES OF ANY KIND AND + HOWEVER CAUSED (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF + BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION AND + THE LIKE), EVEN IF STRONGLOOP HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH + DAMAGES. CUSTOMER BEARS FULL RESPONSIBILITY FOR USE OF THE SOFTWARE AND + THE SUBSCRIPTION AND STRONGLOOP DOES NOT GUARANTEE THAT THE USE OF THE + SOFTWARE AND SUBSCRIPTION WILL ENSURE THAT CUSTOMER'S NETWORK WILL BE + AVAILABLE, SECURE, MONITORED OR PROTECTED AGAINST ANY DOWNTIME, DENIAL OF + SERVICE ATTACKS, SECUITY BREACHES, HACKERS AND THE LIKE. IN NO EVENT WILL + STRONGLOOP'S CUMULATIVE LIABILITY FOR ANY DAMAGES, LOSSES AND CAUSES OF + ACTION (WHETHER IN CONTRACT, TORT, INCLUDING NEGLIGENCE, OR OTHERWISE) + ARISING OUT OF OR RELATED TO THIS AGREEMENT EXCEED THE GREATER OF ONE + HUNDRED DOLLARS (US$100) OR THE TOTAL SUBSCRIPTION FEES PAID BY CUSTOMER + TO STRONGLOOP IN THE TWELVE (12) MONTHS PRECEDING THE DATE THE CLAIM + ARISES. + 6.2 Limitation of Damages. IN NO EVENT WILL STRONGLOOP'S LICENSORS HAVE + ANY LIABILITY FOR ANY CLAIM ARISING IN CONNECTION WITH THIS AGREEMENT. + THE PROVISIONS OF THIS SECTION 6 ALLOCATE RISKS UNDER THIS AGREEMENT + BETWEEN CUSTOMER, STRONGLOOP AND STRONGLOOP'S SUPPLIERS. THE FOREGOING + LIMITATIONS, EXCLUSIONS AND DISCLAIMERS APPLY TO THE MAXIMUM EXTENT + PERMITTED BY APPLICABLE LAW, EVEN IF ANY REMEDY FAILS IN ITS ESSENTIAL + PURPOSE. + 6.3 Failure of Essential Purpose. THE PARTIES AGREE THAT THESE + LIMITATIONS SHALL APPLY EVEN IF THIS AGREEMENT OR ANY LIMITED REMEDY + SPECIFIED HEREIN IS FOUND TO HAVE FAILED OF ITS ESSENTIAL PURPOSE. + 6.4 Allocation of Risk. The sections on limitation of liability and + disclaimer of warranties allocate the risks in the Agreement between the + parties. This allocation is an essential element of the basis of the + bargain between the parties. +7. Term and Termination. +7.1 This Agreement shall commence on the Effective Date and continue for so long +as Customer has a valid Subscription and is current on the payment of any +Subscription Fees required to be paid for that Subscription (the "Subscription +Term"). Either party may terminate this Agreement immediately upon written +notice to the other party, and the Subscription and licenses granted hereunder +automatically terminate upon the termination of this Agreement. This Agreement +will terminate immediately without notice from StrongLoop if Customer fails to +comply with or otherwise breaches any provision of this Agreement. +7.2 All Sections other than Section 1.1 (Subscriptions) and 1.2 (Licenses) shall +survive the expiration or termination of this Agreement. +8. Subscription Fees and Payments. StrongLoop, Customer agrees to pay +StrongLoop the Subscription Fees as described on the StrongLoop Site for the +Subscription purchased unless a different amount has been agreed to in a +separate agreement between Customer and StrongLoop. In addition, Customer shall +pay all sales, use, value added, withholding, excise taxes and other tax, duty, +custom and similar fees levied upon the delivery or use of the Software and the +Subscriptions described in this Agreement. Fees shall be invoiced in full upon +StrongLoop's acceptance of Customer's purchase order for the Subscription. All +invoices shall be paid in US dollars and are due upon receipt and shall be paid +within thirty (30) days. Payments shall be made without right of set-off or +chargeback. If Customer does not pay the invoices when due, StrongLoop may +charge interest at one percent (1%) per month or the highest rate permitted by +law, whichever is lower, on the unpaid balance from the original due date. If +Customer fails to pay fees in accordance with this Section, StrongLoop may +suspend fulfilling its obligations under this Agreement (including but not +limited to suspending the services under the Subscription) until payment is +received by StrongLoop. If any applicable law requires Customer to withhold +amounts from any payments to StrongLoop under this Agreement, (a) Customer shall +effect such withholding, remit such amounts to the appropriate taxing +authorities and promptly furnish StrongLoop with tax receipts evidencing the +payments of such amounts and (b) the sum payable by Customer upon which the +deduction or withholding is based shall be increased to the extent necessary to +ensure that, after such deduction or withholding, StrongLoop receives and +retains, free from liability for such deduction or withholding, a net amount +equal to the amount StrongLoop would have received and retained absent the +required deduction or withholding. +9. General. +9.1 Compliance with Laws. Customer shall abide by all local, state, federal and +international laws, rules, regulations and orders applying to Customer's use of +the Software, including, without limitation, the laws and regulations of the +United States that may restrict the export and re-export of certain commodities +and technical data of United States origin, including the Software. Customer +agrees that it will not export or re-export the Software without the appropriate +United States or foreign government licenses. +9.2 Entire Agreement. This Agreement constitutes the entire agreement between +the parties concerning the subject matter hereof. This Agreement supersedes all +prior or contemporaneous discussions, proposals and agreements between the +parties relating to the subject matter hereof. No amendment, modification or +waiver of any provision of this Agreement shall be effective unless in writing +and signed by both parties. Any additional or different terms on any purchase +orders issued by Customer to StrongLoop shall not be binding on either party, +are hereby rejected by StrongLoop and void. +9.3 Severability. If any provision of this Agreement is held to be invalid or +unenforceable, the remaining portions shall remain in full force and effect and +such provision shall be enforced to the maximum extent possible so as to effect +the intent of the parties and shall be reformed to the extent necessary to make +such provision valid and enforceable. +9.4 Waiver. No waiver of rights by either party may be implied from any actions +or failures to enforce rights under this Agreement. +9.5 Force Majeure. Neither party shall be liable to the other for any delay or +failure to perform due to causes beyond its reasonable control (excluding +payment of monies due). +9.6 No Third Party Beneficiaries. Unless otherwise specifically stated, the +terms of this Agreement are intended to be and are solely for the benefit of +StrongLoop and Customer and do not create any right in favor of any third party. +9.7 Governing Law and Jurisdiction. This Agreement shall be governed by the +laws of the State of California, without reference to the principles of +conflicts of law. The provisions of the Uniform Computerized Information +Transaction Act and United Nations Convention on Contracts for the International +Sale of Goods shall not apply to this Agreement. The parties shall attempt to +resolve any dispute related to this Agreement informally, initially through +their respective management, and then by non-binding mediation in San Francisco +County, California. Any litigation related to this Agreement shall be brought +in the state or federal courts located in San Francisco County, California, and +only in those courts and each party irrevocably waives any objections to such +venue. +9.8 Notices. All notices must be in writing and shall be effective three (3) +days after the date sent to the other party's headquarters, Attention Chief +Financial Officer. diff --git a/package.json b/package.json index 3879d61..cdb82f0 100644 --- a/package.json +++ b/package.json @@ -22,5 +22,8 @@ "type": "git", "url": "https://github.com/strongloop/loopback-storage-service.git" }, - "license": "MIT" + "license": { + "name": "Dual Artistic-2.0/StrongLoop", + "url": "https://github.com/strongloop/loopback-workspace/blob/master/LICENSE" + } } From 5ae0e43bc0747ac575e4cafed70262e295e39c1c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 17 Feb 2014 17:54:42 -0800 Subject: [PATCH 12/26] Fix license url --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cdb82f0..d95f99e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,6 @@ }, "license": { "name": "Dual Artistic-2.0/StrongLoop", - "url": "https://github.com/strongloop/loopback-workspace/blob/master/LICENSE" + "url": "https://github.com/strongloop/loopback-strorage-service/blob/master/LICENSE" } } From 0cc40b142132919a53e5eb91fa3d29d366a6eb86 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Tue, 4 Mar 2014 15:14:06 -0800 Subject: [PATCH 13/26] Create docs.json --- docs.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs.json diff --git a/docs.json b/docs.json new file mode 100644 index 0000000..317c754 --- /dev/null +++ b/docs.json @@ -0,0 +1,8 @@ +{ + "content": [ + { "title": "Container API", "depth": 2 }, + "lib/models/container.js", + { "title": "File API", "depth": 2 }, + "lib/models/file.js" + ] +} From 9f78564a9ca1497bac08bd82399086af15ee7e92 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Tue, 4 Mar 2014 15:18:02 -0800 Subject: [PATCH 14/26] Change Files to document --- docs.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs.json b/docs.json index 317c754..c4ecfa2 100644 --- a/docs.json +++ b/docs.json @@ -1,8 +1,8 @@ { "content": [ - { "title": "Container API", "depth": 2 }, - "lib/models/container.js", - { "title": "File API", "depth": 2 }, - "lib/models/file.js" + { "title": "LoopBack Storage Service", "depth": 2 }, + "lib/storage-service.js", + { "title": "Container API", "depth": 3 }, + "lib/storage-handler.js" ] } From 7eb910a3455d5e5f353dc546c53d6dd124b2726f Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Tue, 4 Mar 2014 15:18:38 -0800 Subject: [PATCH 15/26] Fix title --- docs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs.json b/docs.json index c4ecfa2..32cef15 100644 --- a/docs.json +++ b/docs.json @@ -2,7 +2,7 @@ "content": [ { "title": "LoopBack Storage Service", "depth": 2 }, "lib/storage-service.js", - { "title": "Container API", "depth": 3 }, + { "title": "Storage Handler API", "depth": 3 }, "lib/storage-handler.js" ] } From 64f3f6964413925746de366a7d49d566ed84b92c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 4 Mar 2014 16:06:22 -0800 Subject: [PATCH 16/26] Update README --- README.md | 154 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 98 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 4e5f2da..4fd2c04 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,60 @@ # loopback-storage-service -LoopBack Storage Service +LoopBack storage service provides Node.js and REST APIs to manage binary contents +using pluggable storage providers, such as local file systems, Amazon S3, or +Rackspace cloud files. We use [pkgcloud](https://github.com/pkgcloud/pkgcloud) to support the cloud based +storage services including: -## Storage +- Amazon +- Rackspace +- Openstack +- Azure -The `loopback-storage-service` module is designed to make it easy to upload and download files to various infrastructure providers. +The binary artifacts are organized with containers and files. A container is the +collection of files. Each file will belong to a container. -To get started with a `loopback-storage-service` provider just create one: +## Define a model with the loopback-storage-service connector -``` js - var storageService = require('loopback-storage-service')({ - // - // The name of the provider (e.g. "file") - // - provider: 'provider-name', - - // - // ... Provider specific credentials - // - }); -``` +LoopBack exposes the APIs using a model that is attached to a data source configured +with the loopback-storage-service connector. -Each compute provider takes different credentials to authenticate; these details about each specific provider can be found below: + var ds = loopback.createDataSource({ + connector: require('loopback-storage-service'), + provider: 'filesystem', + root: path.join(__dirname, 'storage') + }); + + var container = ds.createModel('container'); + +The following methods are mixed into the model class: + +- getContainers(cb): List all containers +- createContainer(options, cb): Create a new container +- destroyContainer(container, cb): Destroy an existing container +- getContainer(container, cb): Look up a container by name + +- uploadStream(container, file, options, cb): Get the stream for uploading +- downloadStream(container, file, options, cb): Get the stream for downloading + +- getFiles(container, download, cb): List all files within the given container +- getFile(container, file, cb): Look up a file by name within the given container +- removeFile(container, file, cb): Remove a file by name within the given container + +- upload(req, res, cb): Handle the file upload at the server side +- download(container, file, res, cb): Handle the file download at the server side + +## Configure the storage providers + +Each storage provider takes different settings; these details about each specific +provider can be found below: + +* Local File System + + + { + provider: 'filesystem', + root: '/tmp/storage' + } * Amazon @@ -41,53 +74,62 @@ Each compute provider takes different credentials to authenticate; these details apiKey: '...' } -* Azure - -* Local File System +* OpenStack { - provider: 'filesystem', - root: '/tmp/storage' + provider: 'openstack', + username: 'your-user-name', + password: 'your-password', + authUrl: 'https://your-identity-service' } -Each instance of `storage.Client` returned from `storage.createClient` has a set of uniform APIs: +* Azure -### Container -* `storageService.getContainers(function (err, containers) { })` -* `storageService.createContainer(options, function (err, container) { })` -* `storageService.destroyContainer(containerName, function (err) { })` -* `storageService.getContainer(containerName, function (err, container) { })` -### File -* `storageService.upload(options, function (err) { })` -* `storageService.download(options, function (err) { })` -* `storageService.getFiles(container, function (err, files) { })` -* `storageService.getFile(container, file, function (err, server) { })` -* `storageService.removeFile(container, file, function (err) { })` + { + provider: 'azure', + storageAccount: "test-storage-account", // Name of your storage account + storageAccessKey: "test-storage-access-key" // Access key for storage account + } -Both the `.upload(options)` and `.download(options)` have had **careful attention paid to make sure they are pipe and stream capable:** -### Upload a File -``` js - var storage = require('loopback-storage-service'), - fs = require('fs'); - - var storageService = storage({ /* ... */ }); - - fs.createReadStream('a-file.txt').pipe(storageService.uploadStream('a-container','remote-file-name.txt')); -``` +## REST APIs -### Download a File -``` js - var storage = require('loopback-storage-service'), - fs = require('fs'); - - var storageService = storage({ /* ... */ }); - - storageService.downloadStream({ - container: 'a-container', - remote: 'remote-file-name.txt' - }).pipe(fs.createWriteStream('a-file.txt')); -``` +- GET /api/containers +List all containers + +- GET /api/containers/:container + +Get information about a container by name + +- POST /api/containers + +Create a new container + +- DELETE /api/containers/:container + +Delete an existing container by name + +- GET /api/containers/:container/files + +List all files within a given container by name + +- GET /api/containers/:container/files/:file + +Get information for a file within a given container by name + +- DELETE /api/containers/:container/files/:file + +Delete a file within a given container by name + +- POST /api/containers/:container/upload + +Upload one or more files into the given container by name. The request body should +use [multipart/form-data](https://www.ietf.org/rfc/rfc2388.txt) which the file input +type for HTML uses. + +- GET /api/containers/:container/download/:file + +Download a file within a given container by name \ No newline at end of file From e13146bc31b03268ae60564a6289dc6e4dd34d25 Mon Sep 17 00:00:00 2001 From: crandmck Date: Thu, 20 Mar 2014 14:03:27 -0700 Subject: [PATCH 17/26] Add JSDoc comments --- lib/storage-service.js | 58 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/lib/storage-service.js b/lib/storage-service.js index aa26d91..f4b3a98 100644 --- a/lib/storage-service.js +++ b/lib/storage-service.js @@ -9,9 +9,14 @@ var File = require('./models/file'); module.exports = StorageService; /** - * @param options The options to create a provider - * @returns {StorageService} - * @constructor + * Storage service constructor. Properties of options object depend on the storage service provider. + * + * + * @options {Object} options The options to create a provider; see below; + * @prop {Object} connector + * @prop {String} provider Use 'filesystem' for local file system. Other supported values are: 'amazon', 'rackspace', 'azure', and 'openstack'. + * @prop {String} root With 'filesystem' provider, the path to the root of storage directory. + * @class */ function StorageService(options) { if (!(this instanceof StorageService)) { @@ -42,6 +47,11 @@ function map(obj) { */ } +/** + * List all storage service containers. + * @param {Function} callback Callback function; parameters: err - error message, containers - object holding all containers. + */ + StorageService.prototype.getContainers = function (cb) { this.client.getContainers(function (err, containers) { if (err) { @@ -54,6 +64,17 @@ StorageService.prototype.getContainers = function (cb) { }); }; +/** + * Create a new storage service container. Other option properties depend on the provider. + * + * @options {Object} options The options to create a provider; see below; + * @prop {Object} connector + * @prop {String} provider Storage service provider. Use 'filesystem' for local file system. Other supported values are: 'amazon', 'rackspace', 'azure', and 'openstack'. + * @prop {String} root With 'filesystem' provider, the path to the root of storage directory. + * @prop {String} + * @param {Function} callback Callback function. + */ + StorageService.prototype.createContainer = function (options, cb) { options = options || {}; if ('object' === typeof options && !(options instanceof storage.Container)) { @@ -65,17 +86,33 @@ StorageService.prototype.createContainer = function (options, cb) { }); }; +/** + * Destroy an existing storage service container. + * @param {Object} container Container object. + * @param {Function} callback Callback function. + */ StorageService.prototype.destroyContainer = function (container, cb) { return this.client.destroyContainer(container, cb); }; +/** + * Look up a container by name. + * @param {Object} container Container object. + * @param {Function} callback Callback function. + */ StorageService.prototype.getContainer = function (container, cb) { return this.client.getContainer(container, function (err, container) { return cb(err, map(container)); }); }; -// File related functions +/** + * Get the stream for uploading + * @param {Object} container Container object. + * @param {String} file IS THIS A FILE? + * @options options See below. + * @param callback Callback function + */ StorageService.prototype.uploadStream = function (container, file, options, cb) { if (!cb && typeof options === 'function') { cb = options; @@ -88,6 +125,13 @@ StorageService.prototype.uploadStream = function (container, file, options, cb) return this.client.upload(options, cb); }; +/** + * Get the stream for downloading. + * @param {Object} container Container object. + * @param {String} file Path to file. + * @options {Object} options See below. + * @param {Function} callback Callback function + */ StorageService.prototype.downloadStream = function (container, file, options, cb) { if (!cb && typeof options === 'function') { cb = options; @@ -100,6 +144,12 @@ StorageService.prototype.downloadStream = function (container, file, options, cb return this.client.download(options, cb); }; +/** + * List all files within the given container. + * @param {Object} container Container object. + * @param {Function} download + * @param {Function} callback Callback function + */ StorageService.prototype.getFiles = function (container, download, cb) { return this.client.getFiles(container, download, function (err, files) { if (err) { From dc145edc28b5b22c6e42669aaf94f830c959accc Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 21 Mar 2014 17:28:57 -0700 Subject: [PATCH 18/26] Add a HTML5/Angular based file upload demo --- example/app.js | 7 +- example/public/angular-file-upload.js | 685 +++++++++++++++++++++ example/public/angular-file-upload.min.js | 6 + example/public/angular-file-upload.min.map | 1 + example/public/controllers.js | 97 +++ example/public/index.html | 202 ++++++ 6 files changed, 996 insertions(+), 2 deletions(-) create mode 100644 example/public/angular-file-upload.js create mode 100644 example/public/angular-file-upload.min.js create mode 100644 example/public/angular-file-upload.min.map create mode 100644 example/public/controllers.js create mode 100644 example/public/index.html diff --git a/example/app.js b/example/app.js index 56c91df..7fce5f8 100644 --- a/example/app.js +++ b/example/app.js @@ -6,7 +6,10 @@ var path = require('path'); app.use(app.router); // expose a rest api -app.use(loopback.rest()); +app.use('/api', loopback.rest()); + +app.use(loopback.static(path.join(__dirname, 'public'))); + app.configure(function () { app.set('port', process.env.PORT || 3000); @@ -25,7 +28,7 @@ app.model(container); app.get('/', function (req, res, next) { res.setHeader('Content-Type', 'text/html'); var form = "

Storage Service Demo

" + - "List all containers

" + + "List all containers

" + "Upload to container c1:

" + "

" + "File to upload:
" diff --git a/example/public/angular-file-upload.js b/example/public/angular-file-upload.js new file mode 100644 index 0000000..a73fd4c --- /dev/null +++ b/example/public/angular-file-upload.js @@ -0,0 +1,685 @@ +/* + Angular File Upload v0.3.3.1 + https://github.com/nervgh/angular-file-upload +*/ +(function(angular, factory) { + if (typeof define === 'function' && define.amd) { + define('angular-file-upload', ['angular'], function(angular) { + return factory(angular); + }); + } else { + return factory(angular); + } +}(angular || null, function(angular) { +var app = angular.module('angularFileUpload', []); + +// It is attached to an element that catches the event drop file +app.directive('ngFileDrop', [ '$fileUploader', function ($fileUploader) { + 'use strict'; + + return { + // don't use drag-n-drop files in IE9, because not File API support + link: !$fileUploader.isHTML5 ? angular.noop : function (scope, element, attributes) { + element + .bind('drop', function (event) { + var dataTransfer = event.dataTransfer ? + event.dataTransfer : + event.originalEvent.dataTransfer; // jQuery fix; + if (!dataTransfer) return; + event.preventDefault(); + event.stopPropagation(); + scope.$broadcast('file:removeoverclass'); + scope.$emit('file:add', dataTransfer.files, scope.$eval(attributes.ngFileDrop)); + }) + .bind('dragover', function (event) { + var dataTransfer = event.dataTransfer ? + event.dataTransfer : + event.originalEvent.dataTransfer; // jQuery fix; + + event.preventDefault(); + event.stopPropagation(); + dataTransfer.dropEffect = 'copy'; + scope.$broadcast('file:addoverclass'); + }) + .bind('dragleave', function () { + scope.$broadcast('file:removeoverclass'); + }); + } + }; +}]) +// It is attached to an element which will be assigned to a class "ng-file-over" or ng-file-over="className" +app.directive('ngFileOver', function () { + 'use strict'; + + return { + link: function (scope, element, attributes) { + scope.$on('file:addoverclass', function () { + element.addClass(attributes.ngFileOver || 'ng-file-over'); + }); + scope.$on('file:removeoverclass', function () { + element.removeClass(attributes.ngFileOver || 'ng-file-over'); + }); + } + }; +}); +// It is attached to element like +app.directive('ngFileSelect', [ '$fileUploader', function ($fileUploader) { + 'use strict'; + + return { + link: function (scope, element, attributes) { + $fileUploader.isHTML5 || element.removeAttr('multiple'); + + element.bind('change', function () { + scope.$emit('file:add', $fileUploader.isHTML5 ? this.files : this, scope.$eval(attributes.ngFileSelect)); + ($fileUploader.isHTML5 && element.attr('multiple')) && element.prop('value', null); + }); + + element.prop('value', null); // FF fix + } + }; +}]); +app.factory('$fileUploader', [ '$compile', '$rootScope', '$http', '$window', function ($compile, $rootScope, $http, $window) { + 'use strict'; + + /** + * Creates a uploader + * @param {Object} params + * @constructor + */ + function Uploader(params) { + angular.extend(this, { + scope: $rootScope, + url: '/', + alias: 'file', + queue: [], + headers: {}, + progress: null, + autoUpload: false, + removeAfterUpload: false, + method: 'POST', + filters: [], + formData: [], + isUploading: false, + _nextIndex: 0, + _timestamp: Date.now() + }, params); + + // add the base filter + this.filters.unshift(this._filter); + + this.scope.$on('file:add', function (event, items, options) { + event.stopPropagation(); + this.addToQueue(items, options); + }.bind(this)); + + this.bind('beforeupload', Item.prototype._beforeupload); + this.bind('in:progress', Item.prototype._progress); + this.bind('in:success', Item.prototype._success); + this.bind('in:cancel', Item.prototype._cancel); + this.bind('in:error', Item.prototype._error); + this.bind('in:complete', Item.prototype._complete); + this.bind('in:progress', this._progress); + this.bind('in:complete', this._complete); + } + + Uploader.prototype = { + /** + * Link to the constructor + */ + constructor: Uploader, + + /** + * The base filter. If returns "true" an item will be added to the queue + * @param {File|Input} item + * @returns {boolean} + * @private + */ + _filter: function (item) { + return angular.isElement(item) ? true : !!item.size; + }, + + /** + * Registers a event handler + * @param {String} event + * @param {Function} handler + * @return {Function} unsubscribe function + */ + bind: function (event, handler) { + return this.scope.$on(this._timestamp + ':' + event, handler.bind(this)); + }, + + /** + * Triggers events + * @param {String} event + * @param {...*} [some] + */ + trigger: function (event, some) { + arguments[ 0 ] = this._timestamp + ':' + event; + this.scope.$broadcast.apply(this.scope, arguments); + }, + + /** + * Checks a support the html5 uploader + * @returns {Boolean} + * @readonly + */ + isHTML5: !!($window.File && $window.FormData), + + /** + * Adds items to the queue + * @param {FileList|File|HTMLInputElement} items + * @param {Object} [options] + */ + addToQueue: function (items, options) { + var length = this.queue.length; + var list = 'length' in items ? items : [items]; + + angular.forEach(list, function (file) { + // check a [File|HTMLInputElement] + var isValid = !this.filters.length ? true : this.filters.every(function (filter) { + return filter.call(this, file); + }, this); + + // create new item + var item = new Item(angular.extend({ + url: this.url, + alias: this.alias, + headers: angular.copy(this.headers), + formData: angular.copy(this.formData), + removeAfterUpload: this.removeAfterUpload, + method: this.method, + uploader: this, + file: file + }, options)); + + if (isValid) { + this.queue.push(item); + this.trigger('afteraddingfile', item); + } else { + this.trigger('whenaddingfilefailed', item); + } + }, this); + + if (this.queue.length !== length) { + this.trigger('afteraddingall', this.queue); + this.progress = this._getTotalProgress(); + } + + this._render(); + this.autoUpload && this.uploadAll(); + }, + + /** + * Remove items from the queue. Remove last: index = -1 + * @param {Item|Number} value + */ + removeFromQueue: function (value) { + var index = this.getIndexOfItem(value); + var item = this.queue[ index ]; + item.isUploading && item.cancel(); + this.queue.splice(index, 1); + item._destroy(); + this.progress = this._getTotalProgress(); + }, + + /** + * Clears the queue + */ + clearQueue: function () { + this.queue.forEach(function (item) { + item.isUploading && item.cancel(); + item._destroy(); + }, this); + this.queue.length = 0; + this.progress = 0; + }, + + /** + * Returns a index of item from the queue + * @param {Item|Number} value + * @returns {Number} + */ + getIndexOfItem: function (value) { + return angular.isObject(value) ? this.queue.indexOf(value) : value; + }, + + /** + * Returns not uploaded items + * @returns {Array} + */ + getNotUploadedItems: function () { + return this.queue.filter(function (item) { + return !item.isUploaded; + }); + }, + + /** + * Returns items ready for upload + * @returns {Array} + */ + getReadyItems: function() { + return this.queue + .filter(function(item) { + return item.isReady && !item.isUploading; + }) + .sort(function(item1, item2) { + return item1.index - item2.index; + }); + }, + + /** + * Uploads a item from the queue + * @param {Item|Number} value + */ + uploadItem: function (value) { + var index = this.getIndexOfItem(value); + var item = this.queue[ index ]; + var transport = this.isHTML5 ? '_xhrTransport' : '_iframeTransport'; + + item.index = item.index || this._nextIndex++; + item.isReady = true; + + if (this.isUploading) { + return; + } + + this.isUploading = true; + this[ transport ](item); + }, + + /** + * Cancels uploading of item from the queue + * @param {Item|Number} value + */ + cancelItem: function(value) { + var index = this.getIndexOfItem(value); + var item = this.queue[ index ]; + var prop = this.isHTML5 ? '_xhr' : '_form'; + item[prop] && item[prop].abort(); + }, + + /** + * Uploads all not uploaded items of queue + */ + uploadAll: function () { + var items = this.getNotUploadedItems().filter(function(item) { + return !item.isUploading; + }); + items.forEach(function(item) { + item.index = item.index || this._nextIndex++; + item.isReady = true; + }, this); + items.length && this.uploadItem(items[ 0 ]); + }, + + /** + * Cancels all uploads + */ + cancelAll: function() { + this.getNotUploadedItems().forEach(function(item) { + this.cancelItem(item); + }, this); + }, + + /** + * Updates angular scope + * @private + */ + _render: function() { + this.scope.$$phase || this.scope.$digest(); + }, + + /** + * Returns the total progress + * @param {Number} [value] + * @returns {Number} + * @private + */ + _getTotalProgress: function (value) { + if (this.removeAfterUpload) { + return value || 0; + } + + var notUploaded = this.getNotUploadedItems().length; + var uploaded = notUploaded ? this.queue.length - notUploaded : this.queue.length; + var ratio = 100 / this.queue.length; + var current = (value || 0) * ratio / 100; + + return Math.round(uploaded * ratio + current); + }, + + /** + * The 'in:progress' handler + * @private + */ + _progress: function (event, item, progress) { + var result = this._getTotalProgress(progress); + this.trigger('progressall', result); + this.progress = result; + this._render(); + }, + + /** + * The 'in:complete' handler + * @private + */ + _complete: function () { + var item = this.getReadyItems()[ 0 ]; + this.isUploading = false; + + if (angular.isDefined(item)) { + this.uploadItem(item); + return; + } + + this.trigger('completeall', this.queue); + this.progress = this._getTotalProgress(); + this._render(); + }, + + /** + * The XMLHttpRequest transport + * @private + */ + _xhrTransport: function (item) { + var xhr = item._xhr = new XMLHttpRequest(); + var form = new FormData(); + var that = this; + + this.trigger('beforeupload', item); + + item.formData.forEach(function(obj) { + angular.forEach(obj, function(value, key) { + form.append(key, value); + }); + }); + + form.append(item.alias, item.file); + + xhr.upload.onprogress = function (event) { + var progress = event.lengthComputable ? event.loaded * 100 / event.total : 0; + that.trigger('in:progress', item, Math.round(progress)); + }; + + xhr.onload = function () { + var response = that._transformResponse(xhr.response); + var event = that._isSuccessCode(xhr.status) ? 'success' : 'error'; + that.trigger('in:' + event, xhr, item, response); + that.trigger('in:complete', xhr, item, response); + }; + + xhr.onerror = function () { + that.trigger('in:error', xhr, item); + that.trigger('in:complete', xhr, item); + }; + + xhr.onabort = function () { + that.trigger('in:cancel', xhr, item); + that.trigger('in:complete', xhr, item); + }; + + xhr.open(item.method, item.url, true); + + angular.forEach(item.headers, function (value, name) { + xhr.setRequestHeader(name, value); + }); + + xhr.send(form); + }, + + /** + * The IFrame transport + * @private + */ + _iframeTransport: function (item) { + var form = angular.element(''); + var iframe = angular.element('