├── .gitignore ├── .travis.yml ├── README.md ├── gulpfile.coffee ├── gulpfile.js ├── lib └── index.js ├── package.json ├── source └── index.coffee └── test ├── connector.coffee ├── files └── item.png └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '4.8' 5 | - '6.12' 6 | 7 | cache: 8 | directories: 9 | - node_modules 10 | 11 | services: mongodb 12 | 13 | notifications: 14 | email: 15 | - jeremie.drouet@gmail.com 16 | 17 | before_script: 18 | - npm install 19 | - npm run build 20 | 21 | script: 22 | - npm test 23 | 24 | after_script: 25 | - cat coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-component-storage-mongo 2 | [![Build Status](https://travis-ci.org/jdrouet/loopback-component-storage-mongo.svg)](https://travis-ci.org/jdrouet/loopback-component-storage-mongo) 3 | [![codecov.io](https://codecov.io/github/jdrouet/loopback-component-storage-mongo/coverage.svg?branch=master)](https://codecov.io/github/jdrouet/loopback-component-storage-mongo?branch=master) 4 | [![Dependency Status](https://david-dm.org/jdrouet/loopback-component-storage-mongo.svg)](https://david-dm.org/jdrouet/loopback-component-storage-mongo) 5 | 6 | ![codecov.io](https://codecov.io/github/jdrouet/loopback-component-storage-mongo/branch.svg?branch=master) 7 | 8 | LoopBack storage mongo component provides Node.js and REST APIs to manage binary contents using Mongodb gridfs 9 | 10 | ## Installation 11 | 12 | Install the storage component as usual for a Node package: 13 | 14 | ```bash 15 | npm install --save loopback-component-storage-mongo 16 | ``` 17 | 18 | ## Using it 19 | 20 | Edit you datasources.json and add the following part 21 | 22 | ```javascript 23 | "gridfs": { 24 | "name": "gridfs", 25 | "connector": "loopback-component-storage-mongo", 26 | "host": "localhost", 27 | "port": 27017, 28 | "database": "test" 29 | } 30 | ``` 31 | 32 | And the you can use it as a datasource of your model. 33 | 34 | ## API 35 | 36 | Description | Container model method | REST URI 37 | --------------------------------------------------------------|-------------------------------------------|-------------------------------------------- 38 | List all containers | getContainers(callback) | GET /api/ 39 | Get information about specified container | getContainer(container, callback) | GET /api//:container 40 | Create a new container | createContainer(options, callback) | PORT /api/ 41 | Delete specified container | destroyContainer(options, callback) | DELETE /api//:container 42 | List all files within specified container | getFiles(container, callback) | GET /api//:container/files 43 | Get information for specified file within specified container | getFile(container, file, callback) | GET /api//:container/files/:file 44 | Delete a file within a given container by name | removeFile(container, file, callback) | DELETE /api//:container/files/:file 45 | Upload one or more files into the specified container | upload(container, req, res, callback) | POST /api//:container/upload 46 | Download a file within specified container | download(container, file, res, callback) | GET /api//:container/download/:file 47 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | coffee = require 'gulp-coffee' 2 | gulp = require 'gulp' 3 | mocha = require 'gulp-mocha' 4 | plumber = require 'gulp-plumber' 5 | 6 | gulp.task 'build', -> 7 | gulp.src './source/{,**/}*.coffee' 8 | .pipe plumber() 9 | .pipe coffee bare: true 10 | .pipe plumber.stop() 11 | .pipe gulp.dest './lib/' 12 | return 13 | 14 | gulp.task 'test', -> 15 | gulp.src './test/{,**/}*.coffee', read: false 16 | .pipe plumber() 17 | .pipe mocha 18 | reporter: 'spec' 19 | .pipe plumber.stop() 20 | return 21 | 22 | gulp.task 'default', ['build'] 23 | 24 | gulp.task 'watch', ['build', 'test'], -> 25 | gulp.watch ['{source,test}/{,**/}*.coffee'], ['build', 'test'] 26 | gulp.watch ['test/{,**/}*.coffee'], ['test'] 27 | 28 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('coffee-script/register'); 2 | require('./gulpfile.coffee'); 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var Busboy, DataSource, Grid, GridFS, MongoStorage, ObjectID, Promise, _, async, debug, generateUrl, mongodb; 2 | 3 | _ = require('lodash'); 4 | 5 | async = require('async'); 6 | 7 | Busboy = require('busboy'); 8 | 9 | DataSource = require('loopback-datasource-juggler').DataSource; 10 | 11 | debug = require('debug')('loopback:storage:mongo'); 12 | 13 | Grid = require('gridfs-stream'); 14 | 15 | mongodb = require('mongodb'); 16 | 17 | Promise = require('bluebird'); 18 | 19 | GridFS = mongodb.GridFS; 20 | 21 | ObjectID = mongodb.ObjectID; 22 | 23 | generateUrl = function(options) { 24 | var database, host, port; 25 | host = options.host || options.hostname || 'localhost'; 26 | port = options.port || 27017; 27 | database = options.database || 'test'; 28 | if (options.username && options.password) { 29 | return "mongodb://" + options.username + ":" + options.password + "@" + host + ":" + port + "/" + database; 30 | } else { 31 | return "mongodb://" + host + ":" + port + "/" + database; 32 | } 33 | }; 34 | 35 | MongoStorage = (function() { 36 | function MongoStorage(settings1) { 37 | this.settings = settings1; 38 | if (!this.settings.url) { 39 | this.settings.url = generateUrl(this.settings); 40 | } 41 | } 42 | 43 | MongoStorage.prototype.connect = function(callback) { 44 | var self; 45 | self = this; 46 | if (this.db) { 47 | return process.nextTick(function() { 48 | if (callback) { 49 | return callback(null, self.db); 50 | } 51 | }); 52 | } else { 53 | return mongodb.MongoClient.connect(this.settings.url, this.settings, function(err, db) { 54 | if (!err) { 55 | debug('Mongo connection established: ' + self.settings.url); 56 | self.db = db; 57 | } 58 | if (callback) { 59 | return callback(err, db); 60 | } 61 | }); 62 | } 63 | }; 64 | 65 | MongoStorage.prototype.getContainers = function(callback) { 66 | return this.db.collection('fs.files').find({ 67 | 'metadata.mongo-storage': true 68 | }).toArray(function(err, files) { 69 | var list; 70 | if (err) { 71 | return callback(err); 72 | } 73 | list = _(files).map('metadata').flatten().map('container').uniq().map(function(item) { 74 | return { 75 | container: item 76 | }; 77 | }).value(); 78 | return callback(null, list); 79 | }); 80 | }; 81 | 82 | MongoStorage.prototype.getContainer = function(name, callback) { 83 | return this.db.collection('fs.files').find({ 84 | 'metadata.mongo-storage': true, 85 | 'metadata.container': name 86 | }).toArray(function(err, files) { 87 | if (err) { 88 | return callback(err); 89 | } 90 | return callback(null, { 91 | container: name, 92 | files: files 93 | }); 94 | }); 95 | }; 96 | 97 | MongoStorage.prototype.destroyContainer = function(name, callback) { 98 | var self; 99 | self = this; 100 | return self.getFiles(name, function(err, files) { 101 | if (err) { 102 | return callback(err); 103 | } 104 | return async.each(files, function(file, done) { 105 | return self.removeFileById(file._id, done); 106 | }, callback); 107 | }); 108 | }; 109 | 110 | MongoStorage.prototype.upload = function(container, req, res, callback) { 111 | var busboy, promises, self; 112 | self = this; 113 | busboy = new Busboy({ 114 | headers: req.headers 115 | }); 116 | promises = []; 117 | busboy.on('file', function(fieldname, file, filename, encoding, mimetype) { 118 | return promises.push(new Promise(function(resolve, reject) { 119 | var options; 120 | options = { 121 | filename: filename, 122 | metadata: { 123 | 'mongo-storage': true, 124 | container: container, 125 | filename: filename, 126 | mimetype: mimetype 127 | } 128 | }; 129 | return self.uploadFile(container, file, options, function(err, res) { 130 | if (err) { 131 | return reject(err); 132 | } 133 | return resolve(res); 134 | }); 135 | })); 136 | }); 137 | busboy.on('finish', function() { 138 | return Promise.all(promises).then(function(res) { 139 | return callback(null, res); 140 | })["catch"](callback); 141 | }); 142 | return req.pipe(busboy); 143 | }; 144 | 145 | MongoStorage.prototype.uploadFile = function(container, file, options, callback) { 146 | var gfs, stream; 147 | if (callback == null) { 148 | callback = (function() {}); 149 | } 150 | options._id = new ObjectID(); 151 | options.mode = 'w'; 152 | gfs = Grid(this.db, mongodb); 153 | stream = gfs.createWriteStream(options); 154 | stream.on('close', function(metaData) { 155 | return callback(null, metaData); 156 | }); 157 | stream.on('error', callback); 158 | return file.pipe(stream); 159 | }; 160 | 161 | MongoStorage.prototype.getFiles = function(container, callback) { 162 | return this.db.collection('fs.files').find({ 163 | 'metadata.mongo-storage': true, 164 | 'metadata.container': container 165 | }).toArray(callback); 166 | }; 167 | 168 | MongoStorage.prototype.removeFile = function(container, filename, callback) { 169 | var self; 170 | self = this; 171 | return self.getFile(container, filename, function(err, file) { 172 | if (err) { 173 | return callback(err); 174 | } 175 | return self.removeFileById(file._id, callback); 176 | }); 177 | }; 178 | 179 | MongoStorage.prototype.removeFileById = function(id, callback) { 180 | var self; 181 | self = this; 182 | return async.parallel([ 183 | function(done) { 184 | return self.db.collection('fs.chunks').remove({ 185 | files_id: id 186 | }, done); 187 | }, function(done) { 188 | return self.db.collection('fs.files').remove({ 189 | _id: id 190 | }, done); 191 | } 192 | ], callback); 193 | }; 194 | 195 | MongoStorage.prototype.__getFile = function(query, callback) { 196 | return this.db.collection('fs.files').findOne(query, function(err, file) { 197 | if (err) { 198 | return callback(err); 199 | } 200 | if (!file) { 201 | err = new Error('File not found'); 202 | err.status = 404; 203 | return callback(err); 204 | } 205 | return callback(null, file); 206 | }); 207 | }; 208 | 209 | MongoStorage.prototype.getFile = function(container, filename, callback) { 210 | return this.__getFile({ 211 | 'metadata.mongo-storage': true, 212 | 'metadata.container': container, 213 | 'metadata.filename': filename 214 | }, callback); 215 | }; 216 | 217 | MongoStorage.prototype.getFileById = function(id, callback) { 218 | return this.__getFile({ 219 | _id: id 220 | }, callback); 221 | }; 222 | 223 | MongoStorage.prototype.__download = function(file, res, callback) { 224 | var gfs, read; 225 | if (callback == null) { 226 | callback = (function() {}); 227 | } 228 | gfs = Grid(this.db, mongodb); 229 | read = gfs.createReadStream({ 230 | _id: file._id 231 | }); 232 | res.attachment(file.filename); 233 | res.set('Content-Type', file.metadata.mimetype); 234 | res.set('Content-Length', file.length); 235 | return read.pipe(res); 236 | }; 237 | 238 | MongoStorage.prototype.downloadById = function(id, res, callback) { 239 | var self; 240 | if (callback == null) { 241 | callback = (function() {}); 242 | } 243 | self = this; 244 | return this.getFileById(id, function(err, file) { 245 | if (err) { 246 | return callback(err); 247 | } 248 | return self.__download(file, res, callback); 249 | }); 250 | }; 251 | 252 | MongoStorage.prototype.download = function(container, filename, res, callback) { 253 | var self; 254 | if (callback == null) { 255 | callback = (function() {}); 256 | } 257 | self = this; 258 | return this.getFile(container, filename, function(err, file) { 259 | if (err) { 260 | return callback(err); 261 | } 262 | return self.__download(file, res, callback); 263 | }); 264 | }; 265 | 266 | return MongoStorage; 267 | 268 | })(); 269 | 270 | MongoStorage.modelName = 'storage'; 271 | 272 | MongoStorage.prototype.getContainers.shared = true; 273 | 274 | MongoStorage.prototype.getContainers.accepts = []; 275 | 276 | MongoStorage.prototype.getContainers.returns = { 277 | arg: 'containers', 278 | type: 'array', 279 | root: true 280 | }; 281 | 282 | MongoStorage.prototype.getContainers.http = { 283 | verb: 'get', 284 | path: '/' 285 | }; 286 | 287 | MongoStorage.prototype.getContainer.shared = true; 288 | 289 | MongoStorage.prototype.getContainer.accepts = [ 290 | { 291 | arg: 'container', 292 | type: 'string' 293 | } 294 | ]; 295 | 296 | MongoStorage.prototype.getContainer.returns = { 297 | arg: 'containers', 298 | type: 'object', 299 | root: true 300 | }; 301 | 302 | MongoStorage.prototype.getContainer.http = { 303 | verb: 'get', 304 | path: '/:container' 305 | }; 306 | 307 | MongoStorage.prototype.destroyContainer.shared = true; 308 | 309 | MongoStorage.prototype.destroyContainer.accepts = [ 310 | { 311 | arg: 'container', 312 | type: 'string' 313 | } 314 | ]; 315 | 316 | MongoStorage.prototype.destroyContainer.returns = {}; 317 | 318 | MongoStorage.prototype.destroyContainer.http = { 319 | verb: 'delete', 320 | path: '/:container' 321 | }; 322 | 323 | MongoStorage.prototype.upload.shared = true; 324 | 325 | MongoStorage.prototype.upload.accepts = [ 326 | { 327 | arg: 'container', 328 | type: 'string' 329 | }, { 330 | arg: 'req', 331 | type: 'object', 332 | http: { 333 | source: 'req' 334 | } 335 | }, { 336 | arg: 'res', 337 | type: 'object', 338 | http: { 339 | source: 'res' 340 | } 341 | } 342 | ]; 343 | 344 | MongoStorage.prototype.upload.returns = { 345 | arg: 'result', 346 | type: 'object' 347 | }; 348 | 349 | MongoStorage.prototype.upload.http = { 350 | verb: 'post', 351 | path: '/:container/upload' 352 | }; 353 | 354 | MongoStorage.prototype.getFiles.shared = true; 355 | 356 | MongoStorage.prototype.getFiles.accepts = [ 357 | { 358 | arg: 'container', 359 | type: 'string' 360 | } 361 | ]; 362 | 363 | MongoStorage.prototype.getFiles.returns = { 364 | arg: 'file', 365 | type: 'array', 366 | root: true 367 | }; 368 | 369 | MongoStorage.prototype.getFiles.http = { 370 | verb: 'get', 371 | path: '/:container/files' 372 | }; 373 | 374 | MongoStorage.prototype.getFile.shared = true; 375 | 376 | MongoStorage.prototype.getFile.accepts = [ 377 | { 378 | arg: 'container', 379 | type: 'string' 380 | }, { 381 | arg: 'file', 382 | type: 'string' 383 | } 384 | ]; 385 | 386 | MongoStorage.prototype.getFile.returns = { 387 | arg: 'file', 388 | type: 'object', 389 | root: true 390 | }; 391 | 392 | MongoStorage.prototype.getFile.http = { 393 | verb: 'get', 394 | path: '/:container/files/:file' 395 | }; 396 | 397 | MongoStorage.prototype.removeFile.shared = true; 398 | 399 | MongoStorage.prototype.removeFile.accepts = [ 400 | { 401 | arg: 'container', 402 | type: 'string' 403 | }, { 404 | arg: 'file', 405 | type: 'string' 406 | } 407 | ]; 408 | 409 | MongoStorage.prototype.removeFile.returns = {}; 410 | 411 | MongoStorage.prototype.removeFile.http = { 412 | verb: 'delete', 413 | path: '/:container/files/:file' 414 | }; 415 | 416 | MongoStorage.prototype.download.shared = true; 417 | 418 | MongoStorage.prototype.download.accepts = [ 419 | { 420 | arg: 'container', 421 | type: 'string' 422 | }, { 423 | arg: 'file', 424 | type: 'string' 425 | }, { 426 | arg: 'res', 427 | type: 'object', 428 | http: { 429 | source: 'res' 430 | } 431 | } 432 | ]; 433 | 434 | MongoStorage.prototype.download.http = { 435 | verb: 'get', 436 | path: '/:container/download/:file' 437 | }; 438 | 439 | exports.initialize = function(dataSource, callback) { 440 | var connector, k, m, method, opt, ref, settings; 441 | settings = dataSource.settings || {}; 442 | connector = new MongoStorage(settings); 443 | dataSource.connector = connector; 444 | dataSource.connector.dataSource = dataSource; 445 | connector.DataAccessObject = function() {}; 446 | ref = MongoStorage.prototype; 447 | for (m in ref) { 448 | method = ref[m]; 449 | if (_.isFunction(method)) { 450 | connector.DataAccessObject[m] = method.bind(connector); 451 | for (k in method) { 452 | opt = method[k]; 453 | connector.DataAccessObject[m][k] = opt; 454 | } 455 | } 456 | } 457 | connector.define = function(model, properties, settings) {}; 458 | if (callback) { 459 | dataSource.connector.connect(callback); 460 | } 461 | }; 462 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-component-storage-mongo", 3 | "version": "1.5.1", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "pretest": "mongo admin --eval 'db.createUser({\"user\":\"username\", \"pwd\":\"password\",\"roles\":[{\"role\":\"readWrite\",\"db\":\"test_authenticated\"}]});'; echo", 8 | "test": "COFFEECOV_INIT_ALL=false mocha 'test/{,**/}*.coffee'", 9 | "posttest": "istanbul report", 10 | "build": "gulp", 11 | "watch": "gulp watch" 12 | }, 13 | "keywords": [ 14 | "loopback", 15 | "mongodb", 16 | "mongo", 17 | "gridfs", 18 | "connector", 19 | "storage" 20 | ], 21 | "author": "Jérémie Drouet ", 22 | "license": "ISC", 23 | "dependencies": { 24 | "async": "^2.0.0-rc.2", 25 | "bluebird": "^3.3.4", 26 | "busboy": "^0.2.13", 27 | "debug": "^2.2.0", 28 | "formidable": "^1.0.17", 29 | "gridfs-stream": "^1.1.1", 30 | "lodash": "^4.6.1", 31 | "loopback-connector": "^2.3.0", 32 | "mongodb": "^2.1.11", 33 | "string_decoder": "^0.10.31" 34 | }, 35 | "devDependencies": { 36 | "chai": "^3.5.0", 37 | "codecov.io": "^0.1.6", 38 | "coffee-coverage": "^1.0.1", 39 | "coffee-script": "^1.10.0", 40 | "compression": "^1.6.1", 41 | "cors": "^2.7.1", 42 | "gulp": "^3.9.1", 43 | "gulp-coffee": "^2.3.1", 44 | "gulp-mocha": "^2.2.0", 45 | "gulp-plumber": "^1.1.0", 46 | "gulp-util": "^3.0.7", 47 | "istanbul": "^0.4.2", 48 | "jshint": "^2.9.1", 49 | "loopback": "^2.27.0", 50 | "loopback-boot": "^2.17.0", 51 | "loopback-component-explorer": "^2.4.0", 52 | "loopback-datasource-juggler": "^2.45.2", 53 | "mocha": "^2.4.5", 54 | "mongodb": "^2.1.11", 55 | "serve-favicon": "^2.3.0", 56 | "superagent": "^1.8.2", 57 | "supertest": "^1.2.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /source/index.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'lodash' 2 | async = require 'async' 3 | Busboy = require 'busboy' 4 | DataSource = require('loopback-datasource-juggler').DataSource 5 | debug = require('debug') 'loopback:storage:mongo' 6 | Grid = require 'gridfs-stream' 7 | mongodb = require 'mongodb' 8 | Promise = require 'bluebird' 9 | 10 | GridFS = mongodb.GridFS 11 | ObjectID = mongodb.ObjectID 12 | 13 | generateUrl = (options) -> 14 | host = options.host or options.hostname or 'localhost' 15 | port = options.port or 27017 16 | database = options.database or 'test' 17 | if options.username and options.password 18 | return "mongodb://#{options.username}:#{options.password}@#{host}:#{port}/#{database}" 19 | else 20 | return "mongodb://#{host}:#{port}/#{database}" 21 | 22 | class MongoStorage 23 | constructor: (@settings) -> 24 | if not @settings.url 25 | @settings.url = generateUrl @settings 26 | 27 | connect: (callback) -> 28 | self = @ 29 | if @db 30 | process.nextTick -> 31 | if callback 32 | callback null, self.db 33 | else 34 | mongodb.MongoClient.connect @settings.url, @settings, (err, db) -> 35 | if not err 36 | debug 'Mongo connection established: ' + self.settings.url 37 | self.db = db 38 | if callback 39 | callback err, db 40 | 41 | getContainers: (callback) -> 42 | @db.collection 'fs.files' 43 | .find 44 | 'metadata.mongo-storage': true 45 | .toArray (err, files) -> 46 | return callback err if err 47 | list = _(files) 48 | .map 'metadata' 49 | .flatten() 50 | .map 'container' 51 | .uniq() 52 | .map (item) -> 53 | container: item 54 | .value() 55 | callback null, list 56 | 57 | getContainer: (name, callback) -> 58 | @db.collection 'fs.files' 59 | .find 60 | 'metadata.mongo-storage': true 61 | 'metadata.container': name 62 | .toArray (err, files) -> 63 | return callback err if err 64 | callback null, 65 | container: name 66 | files: files 67 | 68 | destroyContainer: (name, callback) -> 69 | self = @ 70 | self.getFiles name, (err, files) -> 71 | return callback err if err 72 | async.each files, (file, done) -> 73 | self.removeFileById file._id, done 74 | , callback 75 | 76 | upload: (container, req, res, callback) -> 77 | self = @ 78 | busboy = new Busboy headers: req.headers 79 | promises = [] 80 | busboy.on 'file', (fieldname, file, filename, encoding, mimetype) -> 81 | promises.push new Promise (resolve, reject) -> 82 | options = 83 | filename: filename 84 | metadata: 85 | 'mongo-storage': true 86 | container: container 87 | filename: filename 88 | mimetype: mimetype 89 | self.uploadFile container, file, options, (err, res) -> 90 | return reject err if err 91 | resolve res 92 | busboy.on 'finish', -> 93 | Promise.all promises 94 | .then (res) -> 95 | return callback null, res 96 | .catch callback 97 | req.pipe busboy 98 | 99 | uploadFile: (container, file, options, callback = (-> return)) -> 100 | options._id = new ObjectID() 101 | options.mode = 'w' 102 | gfs = Grid @db, mongodb 103 | stream = gfs.createWriteStream options 104 | stream.on 'close', (metaData) -> 105 | callback null, metaData 106 | stream.on 'error', callback 107 | file.pipe stream 108 | 109 | getFiles: (container, callback) -> 110 | @db.collection 'fs.files' 111 | .find 112 | 'metadata.mongo-storage': true 113 | 'metadata.container': container 114 | .toArray callback 115 | 116 | removeFile: (container, filename, callback) -> 117 | self = @ 118 | self.getFile container, filename, (err, file) -> 119 | return callback err if err 120 | self.removeFileById file._id, callback 121 | 122 | removeFileById: (id, callback) -> 123 | self = @ 124 | async.parallel [ 125 | (done) -> 126 | self.db.collection 'fs.chunks' 127 | .remove 128 | files_id: id 129 | , done 130 | (done) -> 131 | self.db.collection 'fs.files' 132 | .remove 133 | _id: id 134 | , done 135 | ], callback 136 | 137 | __getFile: (query, callback) -> 138 | @db.collection 'fs.files' 139 | .findOne query 140 | , (err, file) -> 141 | return callback err if err 142 | if not file 143 | err = new Error 'File not found' 144 | err.status = 404 145 | return callback err 146 | callback null, file 147 | 148 | getFile: (container, filename, callback) -> 149 | @__getFile 150 | 'metadata.mongo-storage': true 151 | 'metadata.container': container 152 | 'metadata.filename': filename 153 | , callback 154 | 155 | getFileById: (id, callback) -> 156 | @__getFile _id: id, callback 157 | 158 | __download: (file, res, callback = (-> return)) -> 159 | gfs = Grid @db, mongodb 160 | read = gfs.createReadStream 161 | _id: file._id 162 | res.attachment file.filename 163 | res.set 'Content-Type', file.metadata.mimetype 164 | res.set 'Content-Length', file.length 165 | read.pipe res 166 | 167 | downloadById: (id, res, callback = (-> return)) -> 168 | self = @ 169 | @getFileById id, (err, file) -> 170 | return callback err if err 171 | self.__download file, res, callback 172 | 173 | download: (container, filename, res, callback = (-> return)) -> 174 | self = @ 175 | @getFile container, filename, (err, file) -> 176 | return callback err if err 177 | self.__download file, res, callback 178 | 179 | MongoStorage.modelName = 'storage' 180 | 181 | MongoStorage.prototype.getContainers.shared = true 182 | MongoStorage.prototype.getContainers.accepts = [] 183 | MongoStorage.prototype.getContainers.returns = {arg: 'containers', type: 'array', root: true} 184 | MongoStorage.prototype.getContainers.http = {verb: 'get', path: '/'} 185 | 186 | MongoStorage.prototype.getContainer.shared = true 187 | MongoStorage.prototype.getContainer.accepts = [{arg: 'container', type: 'string'}] 188 | MongoStorage.prototype.getContainer.returns = {arg: 'containers', type: 'object', root: true} 189 | MongoStorage.prototype.getContainer.http = {verb: 'get', path: '/:container'} 190 | 191 | MongoStorage.prototype.destroyContainer.shared = true 192 | MongoStorage.prototype.destroyContainer.accepts = [{arg: 'container', type: 'string'}] 193 | MongoStorage.prototype.destroyContainer.returns = {} 194 | MongoStorage.prototype.destroyContainer.http = {verb: 'delete', path: '/:container'} 195 | 196 | MongoStorage.prototype.upload.shared = true 197 | MongoStorage.prototype.upload.accepts = [ 198 | {arg: 'container', type: 'string'} 199 | {arg: 'req', type: 'object', http: {source: 'req'}} 200 | {arg: 'res', type: 'object', http: {source: 'res'}} 201 | ] 202 | MongoStorage.prototype.upload.returns = {arg: 'result', type: 'object'} 203 | MongoStorage.prototype.upload.http = {verb: 'post', path: '/:container/upload'} 204 | 205 | MongoStorage.prototype.getFiles.shared = true 206 | MongoStorage.prototype.getFiles.accepts = [ 207 | {arg: 'container', type: 'string'} 208 | ] 209 | MongoStorage.prototype.getFiles.returns = {arg: 'file', type: 'array', root: true} 210 | MongoStorage.prototype.getFiles.http = {verb: 'get', path: '/:container/files'} 211 | 212 | MongoStorage.prototype.getFile.shared = true 213 | MongoStorage.prototype.getFile.accepts = [ 214 | {arg: 'container', type: 'string'} 215 | {arg: 'file', type: 'string'} 216 | ] 217 | MongoStorage.prototype.getFile.returns = {arg: 'file', type: 'object', root: true} 218 | MongoStorage.prototype.getFile.http = {verb: 'get', path: '/:container/files/:file'} 219 | 220 | MongoStorage.prototype.removeFile.shared = true 221 | MongoStorage.prototype.removeFile.accepts = [ 222 | {arg: 'container', type: 'string'} 223 | {arg: 'file', type: 'string'} 224 | ] 225 | MongoStorage.prototype.removeFile.returns = {} 226 | MongoStorage.prototype.removeFile.http = {verb: 'delete', path: '/:container/files/:file'} 227 | 228 | MongoStorage.prototype.download.shared = true 229 | MongoStorage.prototype.download.accepts = [ 230 | {arg: 'container', type: 'string'} 231 | {arg: 'file', type: 'string'} 232 | {arg: 'res', type: 'object', http: {source: 'res'}} 233 | ] 234 | MongoStorage.prototype.download.http = {verb: 'get', path: '/:container/download/:file'} 235 | 236 | exports.initialize = (dataSource, callback) -> 237 | settings = dataSource.settings or {} 238 | connector = new MongoStorage settings 239 | dataSource.connector = connector 240 | dataSource.connector.dataSource = dataSource 241 | connector.DataAccessObject = -> return 242 | for m, method of MongoStorage.prototype 243 | if _.isFunction method 244 | connector.DataAccessObject[m] = method.bind connector 245 | for k, opt of method 246 | connector.DataAccessObject[m][k] = opt 247 | connector.define = (model, properties, settings) -> return 248 | if callback 249 | dataSource.connector.connect callback 250 | return 251 | -------------------------------------------------------------------------------- /test/connector.coffee: -------------------------------------------------------------------------------- 1 | expect = require('chai').expect 2 | fs = require 'fs' 3 | Grid = require 'gridfs-stream' 4 | GridStore = require('mongodb').GridStore 5 | ObjectID = require('mongodb').ObjectID 6 | loopback = require 'loopback' 7 | mongo = require 'mongodb' 8 | path = require 'path' 9 | StorageService = require '../source' 10 | request = require 'supertest' 11 | 12 | insertTestFile = (ds, container, done) -> 13 | options = 14 | filename: 'item.png' 15 | mode: 'w' 16 | metadata: 17 | 'mongo-storage': true 18 | container: container 19 | filename: 'item.png' 20 | gfs = Grid(ds.connector.db, mongo) 21 | write = gfs.createWriteStream options 22 | read = fs.createReadStream path.join __dirname, 'files', 'item.png' 23 | read.pipe write 24 | write.on 'close', -> done() 25 | 26 | describe 'mongo gridfs connector', -> 27 | 28 | agent = null 29 | app = null 30 | datasource = null 31 | server = null 32 | 33 | describe 'datasource', -> 34 | 35 | it 'should exist', -> 36 | expect(StorageService).to.exist 37 | 38 | describe 'default configuration', -> 39 | 40 | before (done) -> 41 | datasource = loopback.createDataSource 42 | connector: StorageService 43 | hostname: '127.0.0.1' 44 | port: 27017 45 | setTimeout done, 200 46 | 47 | it 'should create the datasource', -> 48 | expect(datasource).to.exist 49 | expect(datasource.connector).to.exist 50 | expect(datasource.settings).to.exist 51 | 52 | it 'should create the url', -> 53 | expect(datasource.settings.url).to.exist 54 | expect(datasource.settings.url).to.eql "mongodb://127.0.0.1:27017/test" 55 | 56 | it 'should be connected', -> 57 | expect(datasource.connected).to.eql true 58 | 59 | describe 'model usage', -> 60 | 61 | model = null 62 | 63 | before (done) -> 64 | datasource = loopback.createDataSource 65 | connector: StorageService 66 | hostname: '127.0.0.1' 67 | port: 27017 68 | model = datasource.createModel 'MyModel' 69 | setTimeout done, 200 70 | 71 | it 'should create the model', -> 72 | expect(model).to.exist 73 | 74 | describe 'getContainers function', -> 75 | 76 | it 'should exist', -> 77 | expect(model.getContainers).to.exist 78 | 79 | it 'should return an empty list', (done) -> 80 | model.getContainers (err, list) -> 81 | expect(Array.isArray list).to.eql true 82 | done() 83 | 84 | describe 'getContainer function', -> 85 | 86 | it 'should exist', -> 87 | expect(model.getContainer).to.exist 88 | 89 | describe 'upload function', -> 90 | 91 | it 'should exist', -> 92 | expect(model.upload).to.exist 93 | 94 | describe 'application usage', -> 95 | 96 | app = null 97 | ds = null 98 | server = null 99 | 100 | before (done) -> 101 | app = loopback() 102 | app.set 'port', 5000 103 | app.set 'url', '127.0.0.1' 104 | app.set 'legacyExplorer', false 105 | app.use loopback.rest() 106 | ds = loopback.createDataSource 107 | connector: StorageService 108 | hostname: '127.0.0.1' 109 | port: 27017 110 | model = ds.createModel 'MyModel', {}, 111 | base: 'Model' 112 | plural: 'my-model' 113 | app.model model 114 | setTimeout done, 200 115 | 116 | before (done) -> 117 | ds.connector.db.collection('fs.files').remove {}, done 118 | 119 | before (done) -> 120 | server = app.listen done 121 | 122 | after -> 123 | server.close() 124 | 125 | describe 'getContainers', -> 126 | 127 | describe 'without data', -> 128 | 129 | it 'should return an array', (done) -> 130 | request 'http://127.0.0.1:5000' 131 | .get '/my-model' 132 | .end (err, res) -> 133 | expect(res.status).to.equal 200 134 | expect(Array.isArray res.body).to.equal true 135 | expect(res.body.length).to.equal 0 136 | done() 137 | 138 | describe 'with data', -> 139 | 140 | before (done) -> 141 | insertTestFile ds, 'my-cats', done 142 | 143 | it 'should return an array', (done) -> 144 | request 'http://127.0.0.1:5000' 145 | .get '/my-model' 146 | .end (err, res) -> 147 | expect(res.status).to.equal 200 148 | expect(Array.isArray res.body).to.equal true 149 | expect(res.body.length).to.equal 1 150 | expect(res.body[0].container).to.equal 'my-cats' 151 | done() 152 | 153 | describe 'getContainer', -> 154 | 155 | describe 'without data', -> 156 | 157 | it 'should return an array', (done) -> 158 | request 'http://127.0.0.1:5000' 159 | .get '/my-model/fake-container' 160 | .end (err, res) -> 161 | expect(res.status).to.equal 200 162 | expect(res.body.container).to.equal 'fake-container' 163 | expect(Array.isArray res.body.files).to.equal true 164 | expect(res.body.files.length).to.equal 0 165 | done() 166 | 167 | describe 'with data', -> 168 | 169 | before (done) -> 170 | insertTestFile ds, 'my-cats-1', done 171 | 172 | it 'should return an array', (done) -> 173 | request 'http://127.0.0.1:5000' 174 | .get '/my-model/my-cats-1' 175 | .end (err, res) -> 176 | expect(res.status).to.equal 200 177 | expect(res.body.container).to.equal 'my-cats-1' 178 | expect(Array.isArray res.body.files).to.equal true 179 | expect(res.body.files.length).to.equal 1 180 | done() 181 | 182 | describe 'upload', -> 183 | 184 | it 'should return 20x', (done) -> 185 | request 'http://127.0.0.1:5000' 186 | .post '/my-model/my-cats/upload' 187 | .attach 'file', path.join(__dirname, 'files', 'item.png') 188 | .end (err, res) -> 189 | expect(res.status).to.equal 200 190 | done() 191 | 192 | describe 'download', -> 193 | 194 | before (done) -> 195 | ds.connector.db.collection('fs.files').remove {}, done 196 | 197 | before (done) -> 198 | insertTestFile ds, 'my-cats', done 199 | 200 | it 'should return the file', (done) -> 201 | request 'http://127.0.0.1:5000' 202 | .get '/my-model/my-cats/download/item.png' 203 | .end (err, res) -> 204 | expect(res.status).to.equal 200 205 | done() 206 | 207 | describe 'removeFile', -> 208 | 209 | before (done) -> 210 | ds.connector.db.collection('fs.files').remove {}, done 211 | 212 | before (done) -> 213 | insertTestFile ds, 'my-cats', done 214 | 215 | it 'should return the file', (done) -> 216 | request 'http://127.0.0.1:5000' 217 | .delete '/my-model/my-cats/files/item.png' 218 | .end (err, res) -> 219 | expect(res.status).to.equal 200 220 | done() 221 | 222 | describe 'destroyContainer', -> 223 | 224 | before (done) -> 225 | ds.connector.db.collection('fs.files').remove {}, done 226 | 227 | before (done) -> 228 | insertTestFile ds, 'my-cats', done 229 | 230 | it 'should return the file', (done) -> 231 | request 'http://127.0.0.1:5000' 232 | .delete '/my-model/my-cats' 233 | .end (err, res) -> 234 | expect(res.status).to.equal 200 235 | done() 236 | -------------------------------------------------------------------------------- /test/files/item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdrouet/loopback-component-storage-mongo/0726986d60be0080652848bc331e7d46a8747e0a/test/files/item.png -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require coffee-script/register 2 | --require coffee-coverage/register-istanbul 3 | --------------------------------------------------------------------------------