├── .gitignore ├── .travis.yml ├── README.md ├── lib ├── index.d.ts ├── index.js ├── index.js.map ├── index.test.d.ts ├── index.test.js └── index.test.js.map ├── package-lock.json ├── package.json ├── src ├── index.test.ts └── index.ts ├── test_client ├── Views │ └── index.pug ├── package-lock.json ├── package.json ├── readme.md ├── src │ ├── Profiles │ │ ├── Controllers │ │ │ ├── profiles.controller.spec.ts │ │ │ └── profiles.controller.ts │ │ ├── Models │ │ │ └── profiles.models.ts │ │ ├── Routes │ │ │ └── profiles.routes.ts │ │ └── Services │ │ │ └── upload.service.ts │ ├── config │ │ └── config.ts │ └── index.ts └── tsconfig.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .DS_Store 61 | 62 | #istanbul 63 | .nyc_output 64 | coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: false 7 | node_js: 8 | - '7' 9 | - '6' 10 | - '4' 11 | before_script: 12 | - npm prune 13 | script: 14 | - npm test 15 | after_success: 16 | - npm run semantic-release 17 | branches: 18 | except: 19 | - /^v\d+\.\d+\.\d+$/ 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multer-google-storage 2 | [](https://travis-ci.org/ARozar/multer-google-storage/) 3 | 4 | This is a multer storage engine for google's file storage. 5 | 6 | ## Installation 7 | npm install multer-google-storage --save 8 | 9 | or 10 | 11 | yarn add multer-google-storage 12 | 13 | 14 | ## Usage 15 | ### ES6 16 | 17 | import * as multer from 'multer'; 18 | import * as express from 'express'; 19 | import MulterGoogleCloudStorage from 'multer-google-storage'; 20 | 21 | const app = express(); 22 | 23 | const uploadHandler = multer({ 24 | storage: new MulterGoogleCloudStorage() 25 | }); 26 | 27 | app.post('/upload', uploadHandler.any(), (req, res) => { 28 | console.log(req.files); 29 | res.json(req.files); 30 | }); 31 | 32 | ### ES5 / Common.js imports 33 | 34 | var multer = require("multer"); 35 | var express = require("express"); 36 | var multerGoogleStorage = require("multer-google-storage"); 37 | var app = express(); 38 | var uploadHandler = multer({ 39 | storage: multerGoogleStorage.storageEngine() 40 | }); 41 | app.post('/upload', uploadHandler.any(), function (req, res) { 42 | console.log(req.files); 43 | res.json(req.files); 44 | }); 45 | 46 | NB: This package is written to work with es5 or higher. If you have an editor or IDE that can understand d.ts (typescript) type definitions you will get additional support from your tooling though you do not need to be using typescript to use this package. 47 | 48 | ## Google Cloud 49 | ### Creating a storage bucket 50 | For instructions on how to create a storage bucket see the following [documentation from google](https://cloud.google.com/storage/docs/creating-buckets#storage-create-bucket-console). 51 | 52 | ### Obtaining credentials 53 | For instructions on how to obtain the JSON keyfile as well a "projectId" (contained in the key file) please refer to the following [documentation from google](https://cloud.google.com/docs/authentication/getting-started) 54 | ### Credentials 55 | #### Default method 56 | If using the MulterGoogleCloudStorage class without passing in any configuration options then the following environment variables will need to be set: 57 | 1. GCS_BUCKET, the name of the bucket to save to. 58 | 2. GCLOUD_PROJECT, this is your projectId. It can be found in the json credentials that you generated. 59 | 3. GCS_KEYFILE, this is the path to the json key that you generated. 60 | 61 | #### Explicit method 62 | The constructor of the MulterGoogleCloudStorage class can be passed an optional configuration object. 63 | 64 | | Parameter Name | Type | Sample Value | 65 | |---|---|---| 66 | |`autoRetry`|`boolean`| `true`| 67 | |`email`|`string`|`"test@test.com"`| 68 | |`keyFilename`|`string`|`"./key.json"`| 69 | |`maxRetries`|`number`|`2`| 70 | |`projectId`|`string`|`"test-prj-1234"`| 71 | |`filename`| `function`|`(request, file, callback): void`| 72 | |`bucket`|`string`|`"mybucketname"`| 73 | |`contentType`|`function`|`(request, file): string`| 74 | |`acl`|`string`|`"publicread"`| 75 | 76 | #### Custom file naming 77 | If you need to customize the naming of files then you are able to provide a function that will be called before uploading the file. The third argument of the function must be a standard node callback so pass any error in the first argument (or null on sucess) and the string name of the file on success. 78 | 79 | getFilename(req, file, cb) { 80 | cb(null,`${uuid()}_${file.originalname}`); 81 | } 82 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as multer from 'multer'; 3 | import { ConfigurationObject } from '@google-cloud/storage'; 4 | import { Request } from 'express'; 5 | export default class MulterGoogleCloudStorage implements multer.StorageEngine { 6 | private gcobj; 7 | private gcsBucket; 8 | private options; 9 | getFilename(req: any, file: any, cb: any): void; 10 | getDestination(req: any, file: any, cb: any): void; 11 | getContentType: ContentTypeFunction; 12 | constructor(opts?: ConfigurationObject & { 13 | filename?: any; 14 | bucket?: string; 15 | contentType?: ContentTypeFunction; 16 | }); 17 | _handleFile: (req: any, file: any, cb: any) => void; 18 | _removeFile: (req: any, file: any, cb: any) => void; 19 | } 20 | export declare function storageEngine(opts?: ConfigurationObject & { 21 | filename?: any; 22 | bucket?: string; 23 | }): MulterGoogleCloudStorage; 24 | export declare type ContentTypeFunction = (req: Request, file: Express.Multer.File) => string | undefined; 25 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var uuid = require("uuid/v1"); 4 | var storage = require('@google-cloud/storage'); 5 | var MulterGoogleCloudStorage = /** @class */ (function () { 6 | function MulterGoogleCloudStorage(opts) { 7 | var _this = this; 8 | this.getContentType = function (req, file) { 9 | return undefined; 10 | }; 11 | this._handleFile = function (req, file, cb) { 12 | _this.getDestination(req, file, function (err, destination) { 13 | if (err) { 14 | return cb(err); 15 | } 16 | _this.getFilename(req, file, function (err, filename) { 17 | if (err) { 18 | return cb(err); 19 | } 20 | var gcFile = _this.gcsBucket.file(filename); 21 | var streamOpts = { 22 | predefinedAcl: _this.options.acl || 'private' 23 | }; 24 | var contentType = _this.getContentType(req, file); 25 | if (contentType) { 26 | streamOpts.metadata = { contentType: contentType }; 27 | } 28 | file.stream.pipe(gcFile.createWriteStream(streamOpts)) 29 | .on('error', function (err) { return cb(err); }) 30 | .on('finish', function (file) { return cb(null, { 31 | path: "https://" + _this.options.bucket + ".storage.googleapis.com/" + filename, 32 | filename: filename 33 | }); }); 34 | }); 35 | }); 36 | }; 37 | this._removeFile = function (req, file, cb) { 38 | var gcFile = _this.gcsBucket.file(file.filename); 39 | gcFile.delete(); 40 | }; 41 | opts = opts || {}; 42 | this.getFilename = (opts.filename || this.getFilename); 43 | this.getContentType = (opts.contentType || this.getContentType); 44 | opts.bucket = (opts.bucket || process.env.GCS_BUCKET || null); 45 | opts.projectId = opts.projectId || process.env.GCLOUD_PROJECT || null; 46 | opts.keyFilename = opts.keyFilename || process.env.GCS_KEYFILE || null; 47 | if (!opts.bucket) { 48 | throw new Error('You have to specify bucket for Google Cloud Storage to work.'); 49 | } 50 | if (!opts.projectId) { 51 | throw new Error('You have to specify project id for Google Cloud Storage to work.'); 52 | } 53 | if (!opts.keyFilename) { 54 | throw new Error('You have to specify credentials key file for Google Cloud Storage to work.'); 55 | } 56 | this.gcobj = storage({ 57 | projectId: opts.projectId, 58 | keyFilename: opts.keyFilename 59 | }); 60 | this.gcsBucket = this.gcobj.bucket(opts.bucket); 61 | this.options = opts; 62 | } 63 | MulterGoogleCloudStorage.prototype.getFilename = function (req, file, cb) { 64 | cb(null, uuid() + "_" + file.originalname); 65 | }; 66 | MulterGoogleCloudStorage.prototype.getDestination = function (req, file, cb) { 67 | cb(null, ''); 68 | }; 69 | return MulterGoogleCloudStorage; 70 | }()); 71 | exports.default = MulterGoogleCloudStorage; 72 | function storageEngine(opts) { 73 | return new MulterGoogleCloudStorage(opts); 74 | } 75 | exports.storageEngine = storageEngine; 76 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /lib/index.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/index.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var chai_1 = require("chai"); 4 | var mockery = require("mockery"); 5 | describe('multer-google-storage', function () { 6 | before(function () { 7 | mockery.enable(); 8 | var storageMock = function () { return { bucket: function () { } }; }; 9 | mockery.registerMock('@google-cloud/storage', storageMock); 10 | process.env.GCS_BUCKET = 'test'; 11 | process.env.GCLOUD_PROJECT = 'test'; 12 | process.env.GCS_KEYFILE = './test'; 13 | }); 14 | it('should define multer storage engine interface', function () { 15 | var MulterGoogleCloudStorage = require('./index').default; 16 | var cloudStorage = new MulterGoogleCloudStorage(); 17 | chai_1.expect(cloudStorage._handleFile).to.be.a('function'); 18 | chai_1.expect(cloudStorage._removeFile).to.be.a('function'); 19 | chai_1.expect(cloudStorage.getDestination).to.be.a('function'); 20 | }); 21 | after(function () { return mockery.disable(); }); 22 | }); 23 | //# sourceMappingURL=index.test.js.map -------------------------------------------------------------------------------- /lib/index.test.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":";;AAAA,6BAA8B;AAC9B,iCAAoC;AAGpC,QAAQ,CAAC,uBAAuB,EAAE;IAC9B,MAAM,CAAC;QACH,OAAO,CAAC,MAAM,EAAE,CAAC;QACjB,IAAM,WAAW,GAAG,cAAQ,OAAO,EAAC,MAAM,EAAE,cAAO,CAAC,EAAC,CAAA,CAAA,CAAC,CAAC;QACvD,OAAO,CAAC,YAAY,CAAC,uBAAuB,EAAE,WAAW,CAAC,CAAC;QAE3D,OAAO,CAAC,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,MAAM,CAAC;QACpC,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,QAAQ,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAC;QAE/C,IAAM,wBAAwB,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC;QAC5D,IAAM,YAAY,GAAG,IAAI,wBAAwB,EAAE,CAAC;QAEpD,aAAM,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACrD,aAAM,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACrD,aAAM,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,cAAM,OAAA,OAAO,CAAC,OAAO,EAAE,EAAjB,CAAiB,CAAC,CAAC;AACnC,CAAC,CAAC,CAAA"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multer-google-storage", 3 | "version": "0.0.0-development", 4 | "description": "Streaming multer storage engine for Google Cloud Storage", 5 | "main": "lib/index.js", 6 | "typings": "lib/index", 7 | "scripts": { 8 | "test": "npm run build && nyc --reporter=html --reporter=text mocha lib/index.test.js", 9 | "build": "npm run clean && tsc", 10 | "clean": "rimraf lib", 11 | "precommit": "npm test", 12 | "prepush": "npm test", 13 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 14 | "commit": "git-cz" 15 | }, 16 | "keywords": [ 17 | "multer", 18 | "node", 19 | "gcs", 20 | "cloud", 21 | "storage" 22 | ], 23 | "author": "Andrew de Rozario (https://justintimecoder.com/)", 24 | "license": "MIT", 25 | "dependencies": { 26 | "@google-cloud/storage": "^1.1.1", 27 | "@types/express": "^4.16.1", 28 | "@types/google-cloud__storage": "^1.1.1", 29 | "multer": "^1.3.0", 30 | "uuid": "^3.1.0" 31 | }, 32 | "devDependencies": { 33 | "@types/chai": "^4.0.1", 34 | "@types/mocha": "^2.2.41", 35 | "@types/mockery": "^1.4.29", 36 | "@types/multer": "^1.3.2", 37 | "@types/node": "^8.0.7", 38 | "chai": "^4.0.2", 39 | "commitizen": "^2.9.6", 40 | "cz-conventional-changelog": "^2.0.0", 41 | "husky": "^0.14.3", 42 | "mocha": "^3.4.2", 43 | "mockery": "^2.1.0", 44 | "nyc": "^11.0.3", 45 | "rimraf": "^2.6.1", 46 | "semantic-release": "^6.3.6", 47 | "typescript": "^2.4.1" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "https://github.com/ARozar/multer-google-storage.git" 52 | }, 53 | "czConfig": { 54 | "path": "node_modules/cz-conventional-changelog" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as mockery from 'mockery'; 3 | 4 | 5 | describe('multer-google-storage', () => { 6 | before(() => { 7 | mockery.enable(); 8 | const storageMock = () => { return {bucket: () => {}}}; 9 | mockery.registerMock('@google-cloud/storage', storageMock); 10 | 11 | process.env.GCS_BUCKET = 'test'; 12 | process.env.GCLOUD_PROJECT = 'test'; 13 | process.env.GCS_KEYFILE = './test'; 14 | }); 15 | 16 | it('should define multer storage engine interface',() => { 17 | 18 | const MulterGoogleCloudStorage = require('./index').default; 19 | const cloudStorage = new MulterGoogleCloudStorage(); 20 | 21 | expect(cloudStorage._handleFile).to.be.a('function'); 22 | expect(cloudStorage._removeFile).to.be.a('function'); 23 | expect(cloudStorage.getDestination).to.be.a('function'); 24 | }); 25 | 26 | after(() => mockery.disable()); 27 | }) 28 | 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as multer from 'multer'; 2 | import * as Storage from '@google-cloud/storage'; 3 | import { Bucket, ConfigurationObject } from '@google-cloud/storage'; 4 | import * as uuid from 'uuid/v1'; 5 | import { Request } from 'express'; 6 | const storage: (options?:ConfigurationObject)=>Storage = require('@google-cloud/storage'); 7 | 8 | export default class MulterGoogleCloudStorage implements multer.StorageEngine { 9 | 10 | private gcobj: Storage; 11 | private gcsBucket: Bucket; 12 | private options: ConfigurationObject & { acl?: string, bucket?: string, contentType?: ContentTypeFunction }; 13 | 14 | getFilename(req, file, cb) { 15 | cb(null,`${uuid()}_${file.originalname}`); 16 | } 17 | getDestination( req, file, cb ) { 18 | cb( null, '' ); 19 | } 20 | 21 | public getContentType: ContentTypeFunction = (req, file) => { 22 | return undefined; 23 | } 24 | 25 | constructor(opts?: ConfigurationObject & { filename?: any, bucket?:string, contentType?: ContentTypeFunction }) { 26 | opts = opts || {}; 27 | 28 | this.getFilename = (opts.filename || this.getFilename); 29 | this.getContentType = (opts.contentType || this.getContentType); 30 | 31 | opts.bucket = (opts.bucket || process.env.GCS_BUCKET || null); 32 | opts.projectId = opts.projectId || process.env.GCLOUD_PROJECT || null; 33 | opts.keyFilename = opts.keyFilename || process.env.GCS_KEYFILE || null; 34 | 35 | if (!opts.bucket) { 36 | throw new Error('You have to specify bucket for Google Cloud Storage to work.'); 37 | } 38 | 39 | if (!opts.projectId) { 40 | throw new Error('You have to specify project id for Google Cloud Storage to work.'); 41 | } 42 | 43 | if (!opts.keyFilename) { 44 | throw new Error('You have to specify credentials key file for Google Cloud Storage to work.'); 45 | } 46 | 47 | this.gcobj = storage({ 48 | projectId: opts.projectId, 49 | keyFilename: opts.keyFilename 50 | }); 51 | 52 | this.gcsBucket = this.gcobj.bucket(opts.bucket); 53 | 54 | this.options = opts; 55 | } 56 | 57 | _handleFile = (req, file, cb) => { 58 | this.getDestination(req, file, (err, destination) => { 59 | 60 | if (err) { 61 | return cb(err); 62 | } 63 | 64 | this.getFilename(req, file, (err, filename) => { 65 | if (err) { 66 | return cb(err); 67 | } 68 | var gcFile = this.gcsBucket.file(filename); 69 | 70 | const streamOpts: Storage.WriteStreamOptions = { 71 | predefinedAcl: this.options.acl || 'private' 72 | }; 73 | 74 | const contentType = this.getContentType(req, file); 75 | 76 | if (contentType) { 77 | streamOpts.metadata = {contentType}; 78 | } 79 | 80 | file.stream.pipe( 81 | gcFile.createWriteStream(streamOpts)) 82 | .on('error', (err) => cb(err)) 83 | .on('finish', (file) => cb(null, { 84 | path: `https://${this.options.bucket}.storage.googleapis.com/${filename}`, 85 | filename: filename 86 | }) 87 | ); 88 | }); 89 | 90 | }); 91 | } 92 | _removeFile = (req, file, cb) => { 93 | var gcFile = this.gcsBucket.file(file.filename); 94 | gcFile.delete(); 95 | }; 96 | } 97 | 98 | export function storageEngine(opts?: ConfigurationObject & { filename?: any, bucket?:string }){ 99 | 100 | return new MulterGoogleCloudStorage(opts); 101 | } 102 | 103 | export type ContentTypeFunction = (req: Request, file: Express.Multer.File) => string | undefined; 104 | -------------------------------------------------------------------------------- /test_client/Views/index.pug: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Create Application 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Upload 16 | 17 | 18 | Title 19 | 20 | 21 | 22 | Description 23 | 24 | 25 | 26 | File input 27 | 28 | 29 | 30 | Submit 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test_client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-ts-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run compile && node ./dist/index.js", 8 | "compile": "npm run clean && tsc && copyfiles -u 1 ./src/**/*.handlebars ./dist", 9 | "debug": "npm run compile && node --inspect-brk ./dist/index.js", 10 | "clean": "rimraf ./dist", 11 | "test": "npm run compile && nyc mocha ./dist/**/*.spec.js", 12 | "test:debug": "npm run compile && mocha --inspect-brk ./dist/**/*.spec.js" 13 | }, 14 | "author": "Andrew de Rozario (https://justintimecoder.com/)", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/chai": "^4.0.1", 18 | "@types/chalk": "^0.4.31", 19 | "@types/express": "^4.0.36", 20 | "@types/express-handlebars": "0.0.29", 21 | "@types/google-cloud__storage": "^1.1.1", 22 | "@types/mocha": "^2.2.41", 23 | "@types/mongoose": "^4.7.18", 24 | "@types/multer": "^1.3.2", 25 | "@types/sinon": "^2.3.2", 26 | "copyfiles": "^1.2.0", 27 | "nyc": "^11.0.3", 28 | "rimraf": "^2.6.1", 29 | "typescript": "^3.0.0", 30 | "chai": "^4.1.0", 31 | "mocha": "^3.4.2", 32 | "sinon": "^2.3.8" 33 | }, 34 | "dependencies": { 35 | "chalk": "^3.0.0", 36 | "express": "^4.15.3", 37 | "express-handlebars": "^3.0.0", 38 | "mongodb-memory-server": "^6.6.7", 39 | "mongoose": "^4.11.1", 40 | "multer": "^1.3.0", 41 | "multer-google-storage": "^1.2.0", 42 | "pug": "^3.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test_client/readme.md: -------------------------------------------------------------------------------- 1 | This is a basic Express.js application built using TypeScript. 2 | 3 | It is configured on the assumption that you have Node.js > v7. 4 | 5 | To run the application use the following command line: 6 | 7 | npm install && npm start -------------------------------------------------------------------------------- /test_client/src/Profiles/Controllers/profiles.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import * as sinon from 'sinon'; 4 | import * as mongoose from 'mongoose'; 5 | import { Response, Request, Express } from 'express'; 6 | 7 | import { createProfile, uploadProfile, viewProfiles } from '../Controllers/profiles.controller'; 8 | import { Profile, ProfileRecord } from '../Models/profiles.models'; 9 | 10 | describe('profiles controller create', function () { 11 | 12 | it('returns index view', async function () { 13 | 14 | let file = { path: 'test' }; 15 | let body = { title:'test title', description: 'test description' }; 16 | let req: Partial = {}; 17 | 18 | let res: Partial = { 19 | render: sinon.stub() 20 | }; 21 | 22 | createProfile(req, res); 23 | 24 | sinon.assert.calledWith(res.render as sinon.SinonStub, 'index',{ layout: false , title: 'Please upload your application'}); 25 | }); 26 | }); 27 | 28 | describe('profiles controller',async function() { 29 | let { ProfileRecord } = await import('../Models/profiles.models'); 30 | 31 | beforeEach(function() { 32 | sinon.stub(ProfileRecord, 'find'); 33 | }); 34 | 35 | afterEach(function() { 36 | (ProfileRecord.find as sinon.SinonStub).restore(); 37 | }); 38 | 39 | it('should return expected models', async function() { 40 | 41 | var expectedModels = [{}, {}]; 42 | (ProfileRecord.find as sinon.SinonStub).resolves(expectedModels); 43 | var req: Partial = { }; 44 | var res: Partial = { 45 | json: sinon.stub() 46 | }; 47 | 48 | await viewProfiles(req, res); 49 | 50 | sinon.assert.calledWith(res.json as sinon.SinonStub, expectedModels); 51 | }); 52 | }); 53 | 54 | describe('profiles controller upload', async function () { 55 | let { ProfileRecord } = await import('../Models/profiles.models'); 56 | 57 | const ProfilePrototype: mongoose.Document = ProfileRecord.prototype; 58 | 59 | beforeEach(function () { 60 | sinon.stub(ProfileRecord.prototype, 'save'); 61 | 62 | (ProfilePrototype.save as sinon.SinonStub).callsFake(function (this: Profile) { 63 | let currentRecord = this; 64 | 65 | return Promise.resolve(currentRecord); 66 | }); 67 | }); 68 | 69 | afterEach(function () { 70 | (ProfilePrototype.save as sinon.SinonStub).restore(); 71 | }); 72 | 73 | it('should call save ', async function () { 74 | 75 | let file: Partial = { path: 'test' }; 76 | let body = { title:'test title', description: 'test description' }; 77 | let req: Partial = { file: file as Express.Multer.File, body }; 78 | 79 | let createdModels: Partial = {}; 80 | let res: Partial = { 81 | json: (data: any) => createdModels = data 82 | }; 83 | 84 | await uploadProfile(req, res); 85 | 86 | sinon.assert.called(ProfileRecord.prototype.save); 87 | }); 88 | 89 | it('should create, save and return Profile', async function () { 90 | 91 | let file: Partial = { path: 'test' }; 92 | let body = { title:'test title', description: 'test description' }; 93 | let req: Partial = { file: file as Express.Multer.File, body }; 94 | 95 | let createdModel: Partial = {}; 96 | let res: Partial = { 97 | json: (data: any) => createdModel = data 98 | }; 99 | 100 | await uploadProfile(req, res); 101 | 102 | expect(createdModel.fileName).to.equal(file.path); 103 | expect(createdModel.title).to.equal(body.title); 104 | expect(createdModel.description).to.equal(body.description); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test_client/src/Profiles/Controllers/profiles.controller.ts: -------------------------------------------------------------------------------- 1 | import { Router, RequestHandler, Response, Request, Express } from 'express'; 2 | import * as path from 'path'; 3 | import { ProfileRecord, Profile } from '../Models/profiles.models'; 4 | 5 | export const createProfile = (req: Request, res: Response) => { 6 | res.render('index', { layout: false , title: 'Please upload your application' }); 7 | } 8 | 9 | export const uploadProfile = async (req:Request, res: Response) => { 10 | 11 | const fileName = req.file.path; 12 | 13 | const { title, description } = req.body as Profile; 14 | 15 | const newProfile: Profile = Object.assign(new ProfileRecord(), { fileName, title, description }); 16 | 17 | const savedProfile = await newProfile.save(); 18 | 19 | res.json(savedProfile); 20 | } 21 | 22 | export const viewProfiles = async (req: Request, res: Response) => { 23 | 24 | const profiles = await ProfileRecord.find({}); 25 | 26 | res.json(profiles); 27 | } -------------------------------------------------------------------------------- /test_client/src/Profiles/Models/profiles.models.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | 3 | export interface Profile extends mongoose.Document{ 4 | title:string; 5 | description:string; 6 | fileName: string; 7 | } 8 | var profileSchema = new mongoose.Schema({ 9 | title: String, 10 | description: String, 11 | fileName: String 12 | }); 13 | 14 | export const ProfileRecord = mongoose.model('Profile', profileSchema); 15 | -------------------------------------------------------------------------------- /test_client/src/Profiles/Routes/profiles.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router, Express } from 'express'; 2 | import { createProfile, uploadProfile, viewProfiles } from '../Controllers/profiles.controller'; 3 | import { createUploadHandler } from '../Services/upload.service'; 4 | import * as path from 'path'; 5 | 6 | const router = Router(); 7 | 8 | router.get('/', createProfile); 9 | 10 | router.post('/upload', createUploadHandler.single('image') , uploadProfile); 11 | 12 | router.get('/profiles', viewProfiles); 13 | 14 | 15 | const profiles = (app: Express) => { 16 | app.use('/', router); 17 | 18 | return path.resolve(__dirname,'../Views') 19 | } 20 | 21 | export default profiles; -------------------------------------------------------------------------------- /test_client/src/Profiles/Services/upload.service.ts: -------------------------------------------------------------------------------- 1 | import * as multer from 'multer'; 2 | 3 | import MulterGoogleCloudStorage from 'multer-google-storage'; 4 | 5 | const createUploadHandler = multer({ 6 | storage: new MulterGoogleCloudStorage() 7 | }); 8 | 9 | export { 10 | createUploadHandler 11 | } -------------------------------------------------------------------------------- /test_client/src/config/config.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import { MongoMemoryServer } from 'mongodb-memory-server'; 3 | import * as chalk from 'chalk'; 4 | 5 | const connString = process.env.MONGO_CONNECTION ||''; 6 | const mongod = new MongoMemoryServer(); 7 | 8 | export const dbConfig: mongoose.ConnectionOptions = { 9 | useMongoClient: true 10 | } 11 | 12 | export async function connectDb (appStart: ()=>void ) { 13 | //as we should be using node > 7 we should have native promises 14 | (mongoose as any).Promise = global.Promise; 15 | 16 | const uri = await mongod.getConnectionString(); 17 | 18 | const mongooseOpts = { 19 | useNewUrlParser: true, 20 | autoReconnect: true, 21 | reconnectTries: Number.MAX_VALUE, 22 | reconnectInterval: 1000, 23 | useMongoClient: true 24 | }; 25 | 26 | try { 27 | //try to connect using our configuration 28 | const db = await mongoose.connect(uri, mongooseOpts); 29 | //so we can see what is running as we develop 30 | mongoose.set('debug', true); 31 | 32 | if(appStart)//if we get this far launch the app 33 | appStart(); 34 | 35 | } catch (error) { 36 | //using chalk to give any errors a forboding red colour 37 | console.error(chalk.red('Could not connect to MongoDB!', error)); 38 | } 39 | }; -------------------------------------------------------------------------------- /test_client/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { connectDb } from './config/config'; 3 | import * as chalk from 'chalk'; 4 | import profiles from './Profiles/Routes/profiles.routes'; 5 | 6 | //declare out start up logic 7 | const appStart = () => { 8 | 9 | const app: express.Express = express(); 10 | //setup our features (of which there is one) 11 | const profilesViews = profiles(app); 12 | //setup our view engine 13 | app.set('view engine', 'pug') 14 | //start the app 15 | app.listen('3002', () => console.log(chalk.green('Server listening on port 3002'))); 16 | } 17 | 18 | appStart(); 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test_client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "lib": ["es2015"], /* Specify library files to be included in the compilation: */ 6 | "sourceMap": true, /* Generates corresponding '.map' file. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 9 | "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ 10 | "outDir": "./dist" 11 | } 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "noImplicitAny": false, 7 | "sourceMap": true, 8 | "lib": [ 9 | "es2015" 10 | ], 11 | "outDir": "./lib", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "declaration": true 16 | }, 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } --------------------------------------------------------------------------------