diff --git a/node/wikibooks-nodejs-microservice/.gitignore b/node/wikibooks-nodejs-microservice/.gitignore new file mode 100644 index 0000000000..25c8fdbaba --- /dev/null +++ b/node/wikibooks-nodejs-microservice/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/1-express/app1.js b/node/wikibooks-nodejs-microservice/2/1-express/app1.js new file mode 100644 index 0000000000..22bf988205 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/1-express/app1.js @@ -0,0 +1,8 @@ +let express = require('express'); +let app = express(); + +app.get('/', (req, res) => { + res.send('Hello World'); +}); + +app.listen(3000); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/1-express/app2.js b/node/wikibooks-nodejs-microservice/2/1-express/app2.js new file mode 100644 index 0000000000..e770685104 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/1-express/app2.js @@ -0,0 +1,34 @@ +let express = require('express'); +let app = express(); +let stack = []; + +app.post('/stack', (req, res, next) => { + let buffer = ''; + + req.on('data', (data) => { + buffer += data; + }); + + req.on('end', () => { + stack.push(buffer); + return next(); + }); +}); + +app.delete('/stack', (req, res, next) => { + stack.pop(); + return next(); +}); + +app.get('/stack/:index', (req, res) => { + if (req.params.index >= 0 && req.params.index < stack.length) { + return res.end('' + stack[req.params.index]); + } + res.status(404).end(); +}); + +app.use('/stack', (req, res) => { + res.send(stack); +}); + +app.listen(3000); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/1-express/app3.js b/node/wikibooks-nodejs-microservice/2/1-express/app3.js new file mode 100644 index 0000000000..ddb0aad496 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/1-express/app3.js @@ -0,0 +1,33 @@ +let express = require('express'); +let body = require('body-parser'); +let route = express.Router(); +let app = express(); +let stack = []; + +app.use(body.text({ type: '*/*' })); + +route.post('/', (req, res, next) => { + stack.push(req.body); + + return next(); +}); + +route.delete('/', (req, res, next) => { + stack.pop(); + return next(); +}); + +route.get('/:index', (req, res) => { + if (req.params.index >= 0 && req.params.index < stack.length) { + return res.end('' + stack[req.params.index]); + } + res.status(404).end(); +}); + +route.use('/', (req, res) => { + res.send(stack); +}); + +app.use('/stack', route); + +app.listen(3000); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/1-express/package.json b/node/wikibooks-nodejs-microservice/2/1-express/package.json new file mode 100644 index 0000000000..3deab32423 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/1-express/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "body-parser": "^1.19.0", + "express": "^4.17.1" + } +} diff --git a/node/wikibooks-nodejs-microservice/2/2-micro/app.js b/node/wikibooks-nodejs-microservice/2/2-micro/app.js new file mode 100644 index 0000000000..415217cd3d --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/2-micro/app.js @@ -0,0 +1,3 @@ +module.exports = (req, res) => { + res.end('Hello World'); +}; \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/2-micro/package.json b/node/wikibooks-nodejs-microservice/2/2-micro/package.json new file mode 100644 index 0000000000..3288245443 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/2-micro/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "micro": "^9.3.4" + }, + "scripts": { + "start": "micro app" + } +} \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/3-seneca/.gitignore b/node/wikibooks-nodejs-microservice/2/3-seneca/.gitignore new file mode 100644 index 0000000000..6320cd248d --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/3-seneca/.gitignore @@ -0,0 +1 @@ +data \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/3-seneca/app1.js b/node/wikibooks-nodejs-microservice/2/3-seneca/app1.js new file mode 100644 index 0000000000..20541e7e32 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/3-seneca/app1.js @@ -0,0 +1,14 @@ +const seneca = require('seneca'); +const service = seneca({ log: 'silent' }); + +service.add({ math: 'sum' }, (msg, next) => { + next(null, { + sum: msg.values.reduce((total, value) => (total + value), 0) + }); +}); + +service.act({ math: 'sum', values: [1, 2, 3] }, (err, msg) => { + if (err) return console.error(err); + + console.log('sum = %s', msg.sum); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/3-seneca/app2.js b/node/wikibooks-nodejs-microservice/2/3-seneca/app2.js new file mode 100644 index 0000000000..2e23fa3da9 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/3-seneca/app2.js @@ -0,0 +1,25 @@ +const seneca = require('seneca'); +const service = seneca({ log: 'silent' }); + +const stack = []; + +// http://localhost:3000/act?stack=push&value=one +service.add('stack:push,value:*', (msg, next) => { + stack.push(msg.value); + + next(null, stack); +}); + +// http://localhost:3000/act?stack=pop +service.add('stack:pop', (msg, next) => { + stack.pop(); + + next(null, stack); +}); + +// http://localhost:3000/act?stack=get +service.add('stack:get', (msg, next) => { + next(null, stack); +}); + +service.listen(3000); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/3-seneca/app3.js b/node/wikibooks-nodejs-microservice/2/3-seneca/app3.js new file mode 100644 index 0000000000..65d54b0802 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/3-seneca/app3.js @@ -0,0 +1,41 @@ +const async = require('async'); +const seneca = require('seneca'); +const service = seneca({ log: 'silent' }); + +service.use('basic'); +service.use('entity'); +service.use('jsonfile-store', { folder: 'data' }); + +const stack = service.make$('stack'); + +stack.load$((err) => { + if (err) throw err; + + service.add('stack:push,value:*', (msg, next) => { + stack.make$().save$({ value: msg.value }, (err) => { + return next(err, { value: msg.value }); + }); + }); + + service.add('stack:pop,value:*', (msg, next) => { + stack.list$({ value: msg.value }, (err, items) => { + async.each(items, (item, next) => { + item.remove$(next); + }, (err) => { + if (err) return next(err); + + return next(err, { remove: items.length }); + }); + }); + }); + + service.add('stack:get', (msg, next) => { + stack.list$((err, items) => { + if (err) return next(err); + + return next(null, items.map((item) => (item.value))); + }); + }); + + service.listen(3000); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/3-seneca/package.json b/node/wikibooks-nodejs-microservice/2/3-seneca/package.json new file mode 100644 index 0000000000..96b60e028d --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/3-seneca/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "async": "^3.1.0", + "seneca": "^3.14.1", + "seneca-basic": "^0.6.0", + "seneca-entity": "^4.1.0", + "seneca-jsonfile-store": "^1.1.0" + } +} diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/description.md b/node/wikibooks-nodejs-microservice/2/4-hydra/description.md new file mode 100644 index 0000000000..2a9d5269e3 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/description.md @@ -0,0 +1,21 @@ +## 2 - 4 히드라(Hydra) + +- `npm i -g yo generator-fwsp-hydra hydra-cli` + +- `hydra-cli config local` + - `redisUrl: 127.0.0.1 redisPort: 6379 redisDb: 15` + +- `yo fwsp-hydra` + - ? Name of the service (`-service` will be appended automatically) hello + - ? Your full name? + - ? Your email address? + - ? Your organization or username? (used to tag docker images) + - ? Host the service runs on? + - ? Port the service runs on? 0 + - ? What does this service do? + - ? Does this service need auth? No + - ? Is this a hydra-express service? Yes + - ? Set up a view engine? No + - ? Set up logging? No + - ? Enable CORS on serverResponses? No + - ? Run npm install? No \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.editorconfig b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.editorconfig new file mode 100644 index 0000000000..c0f2cb2c8b --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.editorconfig @@ -0,0 +1,10 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.eslintrc b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.eslintrc new file mode 100644 index 0000000000..e498908ea5 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.eslintrc @@ -0,0 +1,97 @@ +{ + "plugins": ["jasmine"], + "extends": ["eslint:recommended", "plugin:jasmine/recommended"], + "parserOptions": { + "ecmaVersion": 6, + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "rules": { + "valid-jsdoc": [2, { + "requireReturn": false + }], + "comma-dangle": 0, + "curly": 2, + "semi": [2, "always"], + "no-console": 0, + "no-debugger": 2, + "no-extra-semi": 2, + "no-constant-condition": 2, + "no-alert": 2, + "no-unused-vars": ["error", { "argsIgnorePattern": "^(?:reject|_)" }], + "one-var-declaration-per-line": 2, + "operator-linebreak": [ + 2, + "after" + ], + "max-len": [ + 2, + 240 + ], + "indent": [ + 2, + 2, + { + "SwitchCase": 1 + } + ], + "quotes": [ + 2, + "single", + { + "avoidEscape": true + } + ], + "no-multi-str": 2, + "no-mixed-spaces-and-tabs": 2, + "no-trailing-spaces": 2, + "space-unary-ops": [ + 2, + { + "nonwords": false, + "overrides": {} + } + ], + "one-var": [ + 2, + { + "uninitialized": "always", + "initialized": "never" + } + ], + "keyword-spacing": [ + 2, + {} + ], + "space-infix-ops": 2, + "space-before-blocks": [ + 2, + "always" + ], + "eol-last": 2, + "space-in-parens": [ + 2, + "never" + ], + "no-multiple-empty-lines": 2, + "no-multi-spaces": 2, + "key-spacing": [ + 2, + { + "beforeColon": false, + "afterColon": true + } + ] + }, + "env": { + "browser": true, + "node": true, + "es6": true, + "jasmine": true + }, + "globals": { + "-": 0 + } +} diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.gitattributes b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.gitattributes new file mode 100644 index 0000000000..a3f05d060c --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.gitattributes @@ -0,0 +1,2 @@ +*.apib linguist-documentation + diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.gitignore b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.gitignore new file mode 100644 index 0000000000..0cf3873046 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +*.log +*.pem +*.rsa +node_modules +config/config.json +*/public diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.nvmrc b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.nvmrc new file mode 100644 index 0000000000..3410944e13 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/.nvmrc @@ -0,0 +1 @@ +v6.2.1 \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/config/sample-config.json b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/config/sample-config.json new file mode 100644 index 0000000000..abfc25e0b1 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/config/sample-config.json @@ -0,0 +1,15 @@ +{ + "environment": "development", + "hydra": { + "serviceName": "hello-service", + "serviceIP": "", + "servicePort": 0, + "serviceType": "hello", + "serviceDescription": "", + "redis": { + "url": "127.0.0.1", + "port": 6379, + "db": 15 + } + } +} diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/hello-service.js b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/hello-service.js new file mode 100644 index 0000000000..18e97613e0 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/hello-service.js @@ -0,0 +1,28 @@ +/** +* @name Hello +* @summary Hello Hydra Express service entry point +* @description +*/ +'use strict'; + +const version = require('./package.json').version; +const hydraExpress = require('hydra-express'); + + + +let config = require('fwsp-config'); + +/** +* Load configuration file and initialize hydraExpress app +*/ +config.init('./config/config.json') + .then(() => { + config.version = version; + return hydraExpress.init(config.getObject(), version, () => { + hydraExpress.registerRoutes({ + '/v1/hello': require('./routes/hello-v1-routes') + }); + }); + }) + .then(serviceInfo => console.log('serviceInfo', serviceInfo)) + .catch(err => console.log('err', err)); diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/package.json b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/package.json new file mode 100644 index 0000000000..6950b6b283 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/package.json @@ -0,0 +1,26 @@ +{ + "name": "hello-service", + "version": "0.0.1", + "author": " <>", + "private": true, + "scripts": { + "start": "node hello-service.js", + "debug": "node-debug hello-service.js --debug-brk", + "test": "mocha specs --reporter spec", + "docker": "node scripts/docker.js" + }, + "engines": { + "node": ">=6.2.1" + }, + "dependencies": { + "fwsp-config": "1.1.5", + "hydra-express": "1.7.1", + "fwsp-server-response": "2.2.6" + }, + "devDependencies": { + "chai": "3.5.0", + "eslint": "3.16.0", + "mocha": "3.2.0", + "superagent": "3.5.2" + } +} diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/routes/hello-v1-routes.js b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/routes/hello-v1-routes.js new file mode 100644 index 0000000000..89adcd82fa --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/routes/hello-v1-routes.js @@ -0,0 +1,27 @@ +/** + * @name hello-v1-api + * @description This module packages the Hello API. + */ +'use strict'; + +const hydraExpress = require('hydra-express'); +const hydra = hydraExpress.getHydra(); +const express = hydraExpress.getExpress(); +const ServerResponse = require('fwsp-server-response'); + +let serverResponse = new ServerResponse(); +express.response.sendError = function(err) { + serverResponse.sendServerError(this, {result: {error: err}}); +}; +express.response.sendOk = function(result) { + serverResponse.sendOk(this, {result}); +}; + +let api = express.Router(); + +api.get('/', +(req, res) => { + res.sendOk({greeting: 'Welcome to Hydra Express!'}); +}); + +module.exports = api; diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/scripts/docker.js b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/scripts/docker.js new file mode 100644 index 0000000000..5e0850194e --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/scripts/docker.js @@ -0,0 +1,64 @@ +const config = require('fwsp-config'), + fs = require('fs'), + spawn = require('child_process').spawn, + tag = `/${process.env.npm_package_name}:${process.env.npm_package_version}`; +modes = { + build: ['docker', ['build', '-t', tag, process.cwd()], { stdio: 'inherit' }], + run: ['docker', ['run', '-it', tag], { stdio: 'inherit' }], + up: ['docker', ['run', '-d', tag], { detached: true }] +}, + getDockerfile = (exposePort, logger = false) => ` + FROM node:6.3 + MAINTAINER + EXPOSE ${exposePort} + ARG NPM_TOKEN + RUN mkdir -p /usr/src/app + WORKDIR /usr/src/app + ADD . /usr/src/app + RUN echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc + ${logger ? 'RUN npm install pino-elasticsearch -g' : ''} + RUN npm install --production + RUN rm -f .npmrc + CMD ["npm", "start"] + `, + run = (mode) => { + console.log(`Running '${[modes[mode][0], ...modes[mode][1]].join(' ')}'`); + let docker = spawn(...modes[mode]); + docker.on('close', code => console.log(`docker ${mode} exited with code ${code}`)); + }; + +let mode = process.argv[2]; +if (!modes[mode]) { + console.log(`No such mode '${mode}'. Available modes: ${Object.keys(modes).join(', ')}.`); + return; +} +if (mode === 'build') { + if (!fs.existsSync('config/config.json')) { + console.log('config/config.json must exist to build docker image'); + process.exit(1); + } + if (fs.existsSync('Dockerfile')) { + run(mode); + } else { + console.log('No Dockerfile found, loading config.json and generating one...'); + config.init('./config/config.json') + .then(() => { + let Dockerfile = getDockerfile( + config.hydra.servicePort, + config.hydra.plugins && config.hydra.plugins.logger ? true : false + ).split(/\n/).map(v => v.trim()).filter(v => v.length).join('\n'); + console.log(Dockerfile); + fs.writeFile('Dockerfile', Dockerfile, err => { + if (err) { + console.log('Error writing Dockerfile', err); + process.exit(1); + } else { + console.log('Wrote Dockerfile'); + run(mode); + } + }); + }); + } +} else { + run(mode); +} diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/specs/helpers/chai.js b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/specs/helpers/chai.js new file mode 100644 index 0000000000..ae951a6e67 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/specs/helpers/chai.js @@ -0,0 +1,10 @@ +'use strict'; + +var chai = require('chai'); + +chai.config.includeStack = true; + +global.expect = chai.expect; +global.AssertionError = chai.AssertionError; +global.Assertion = chai.Assertion; +global.assert = chai.assert; diff --git a/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/specs/test.js b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/specs/test.js new file mode 100644 index 0000000000..e3013e9ab0 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/2/4-hydra/hello-service/specs/test.js @@ -0,0 +1,10 @@ +'use strict'; + +/** +* Change into specs folder so that file loading works. +*/ +process.chdir('./specs'); + +require('./helpers/chai.js'); + +// Tests go here. diff --git a/node/wikibooks-nodejs-microservice/3/1-express/.gitignore b/node/wikibooks-nodejs-microservice/3/1-express/.gitignore new file mode 100644 index 0000000000..95a48fbcd4 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/1-express/.gitignore @@ -0,0 +1 @@ +uploads \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/3/1-express/app.js b/node/wikibooks-nodejs-microservice/3/1-express/app.js new file mode 100644 index 0000000000..c352ff3b05 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/1-express/app.js @@ -0,0 +1,135 @@ +const express = require('express'); +const sharp = require('sharp'); +const bodyparser = require('body-parser'); +const path = require('path'); +const fs = require('fs'); +const app = express(); + +app.param('image', (req, res, next, image) => { + if (!image.match(/\.(png|jpg)$/i)) { + return res.status(req.method === 'POST' ? 403 : 404).end(); + } + + req.image = image; + + req.localpath = path.join(__dirname, 'uploads', req.image); + + return next(); +}); + +app.post('/uploads/:image', bodyparser.raw({ + limit: '10mb', + type: 'image/*' +}), (req, res) => { + let fd = fs.createWriteStream(req.localpath, { + flags: 'w+', + encoding: 'binary' + }); + + fd.write(req.body); + fd.end(); + + fd.on('close', () => { + res.send({ status: 'ok', size: req.body.length }); + }); +}); + +app.head('/uploads/:image', (req, res) => { + fs.access( + req.localpath, + fs.constants.R_OK, + (err) => { + res.status(err ? 404 : 200).end(); + }, + ); +}); + +app.get("/uploads/:image", (req, res) => { + fs.access(req.localpath, fs.constants.R_OK, (err) => { + if (err) return res.status(404).end(); + + let image = sharp(req.localpath); + + let width = +req.query.width; + let height = +req.query.height; + let blur = +req.query.blur; + let sharpen = +req.query.sharpen; + let greyscale = ['y', 'yes', '1', 'on'].includes(req.query.greyscale); + let flip = ['y', 'yes', '1', 'on'].includes(req.query.flip); + let flop = ['y', 'yes', '1', 'on'].includes(req.query.flop); + + if (width > 0 && height > 0) { + image.ignoreAspectRatio(); + } + + if (width > 0 || height > 0) { + image.resize(width || null, height || null); + } + + if (flip) image.flip(); + if (flop) image.flop(); + if (blur > 0) image.blur(blur); + if (sharpen > 0) image.sharpen(sharpen); + if (greyscale) image.greyscale(); + + res.setHeader('Content-Type', 'image/' + path.extname(req.image).substr(1)); + + image.pipe(res); + }); +}); + +app.get(/\/thumbnail\.(jpg|png)/, (req, res, next) => { + let format = (req.params[0] === 'png' ? 'png' : 'jpeg'); + let width = +req.query.width || 300; + let height = +req.query.height || 200; + let border = +req.query.border || 5; + let bgcolor = req.query.bgcolor || '#fcfcfc'; + let fgcolor = req.query.fgcolor || '#ddd'; + let textcolor = req.query.textcolor || '#aaa'; + let textsize = +req.query.textsize || 24; + let image = sharp({ + create: { + width, + height, + channels: 4, + background: { r: 0, g: 0, b: 0 }, + }, + }); + + const thumbnail = Buffer.from(` + + + + + + + + ${width} x ${height} + + + `); + + image.overlayWith(thumbnail)[format]().pipe(res); +}); + +app.listen(3000, () => { + console.log('ready'); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/3/1-express/description.md b/node/wikibooks-nodejs-microservice/3/1-express/description.md new file mode 100644 index 0000000000..88dafb6cae --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/1-express/description.md @@ -0,0 +1,7 @@ +## 3 - 1 - 1 이미지 업로드 + +- `curl -X POST -H 'Content-Type: image/png' --data-binary @example.png http://localhost:3000/uploads/example.png` + +## 3 - 1 - 2 폴더에 이미지가 있는지 확인 + +- `curl --head 'http://localhost:3000/uploads/example.png'` \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/3/1-express/example.png b/node/wikibooks-nodejs-microservice/3/1-express/example.png new file mode 100644 index 0000000000..fd4089b315 Binary files /dev/null and b/node/wikibooks-nodejs-microservice/3/1-express/example.png differ diff --git a/node/wikibooks-nodejs-microservice/3/1-express/package.json b/node/wikibooks-nodejs-microservice/3/1-express/package.json new file mode 100644 index 0000000000..382e9c9b43 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/1-express/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "body-parser": "^1.19.0", + "express": "^4.17.1", + "sharp": "^0.19.0" + } +} diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/description.md b/node/wikibooks-nodejs-microservice/3/2-hydra/description.md new file mode 100644 index 0000000000..389ef581fe --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/description.md @@ -0,0 +1,18 @@ +## 3 - 2 히드라 사용 + +- `yo fwsp-hydra` + - ? Name of the service (`-service` will be appended automatically) imagini + - ? Your full name? + - ? Your email address? + - ? Your organization or username? (used to tag docker images) + - ? Host the service runs on? + - ? Port the service runs on? 3000 + - ? What does this service do? + - ? Does this service need auth? No + - ? Is this a hydra-express service? Yes + - ? Set up a view engine? No + - ? Set up logging? No + - ? Enable CORS on serverResponses? No + - ? Run npm install? No + +- `curl -X POST -H 'Content-Type: image/png' --data-binary @example.png http://localhost:3000/v1/imagini/example.png` \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/example.png b/node/wikibooks-nodejs-microservice/3/2-hydra/example.png new file mode 100644 index 0000000000..fd4089b315 Binary files /dev/null and b/node/wikibooks-nodejs-microservice/3/2-hydra/example.png differ diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.editorconfig b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.editorconfig new file mode 100644 index 0000000000..c0f2cb2c8b --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.editorconfig @@ -0,0 +1,10 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.eslintrc b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.eslintrc new file mode 100644 index 0000000000..e498908ea5 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.eslintrc @@ -0,0 +1,97 @@ +{ + "plugins": ["jasmine"], + "extends": ["eslint:recommended", "plugin:jasmine/recommended"], + "parserOptions": { + "ecmaVersion": 6, + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "rules": { + "valid-jsdoc": [2, { + "requireReturn": false + }], + "comma-dangle": 0, + "curly": 2, + "semi": [2, "always"], + "no-console": 0, + "no-debugger": 2, + "no-extra-semi": 2, + "no-constant-condition": 2, + "no-alert": 2, + "no-unused-vars": ["error", { "argsIgnorePattern": "^(?:reject|_)" }], + "one-var-declaration-per-line": 2, + "operator-linebreak": [ + 2, + "after" + ], + "max-len": [ + 2, + 240 + ], + "indent": [ + 2, + 2, + { + "SwitchCase": 1 + } + ], + "quotes": [ + 2, + "single", + { + "avoidEscape": true + } + ], + "no-multi-str": 2, + "no-mixed-spaces-and-tabs": 2, + "no-trailing-spaces": 2, + "space-unary-ops": [ + 2, + { + "nonwords": false, + "overrides": {} + } + ], + "one-var": [ + 2, + { + "uninitialized": "always", + "initialized": "never" + } + ], + "keyword-spacing": [ + 2, + {} + ], + "space-infix-ops": 2, + "space-before-blocks": [ + 2, + "always" + ], + "eol-last": 2, + "space-in-parens": [ + 2, + "never" + ], + "no-multiple-empty-lines": 2, + "no-multi-spaces": 2, + "key-spacing": [ + 2, + { + "beforeColon": false, + "afterColon": true + } + ] + }, + "env": { + "browser": true, + "node": true, + "es6": true, + "jasmine": true + }, + "globals": { + "-": 0 + } +} diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.gitattributes b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.gitattributes new file mode 100644 index 0000000000..a3f05d060c --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.gitattributes @@ -0,0 +1,2 @@ +*.apib linguist-documentation + diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.gitignore b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.gitignore new file mode 100644 index 0000000000..527ac902b9 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +*.log +*.pem +*.rsa +node_modules +config/config.json +*/public + +uploads diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.nvmrc b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.nvmrc new file mode 100644 index 0000000000..3410944e13 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/.nvmrc @@ -0,0 +1 @@ +v6.2.1 \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/config/sample-config.json b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/config/sample-config.json new file mode 100644 index 0000000000..9bbf186114 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/config/sample-config.json @@ -0,0 +1,15 @@ +{ + "environment": "development", + "hydra": { + "serviceName": "imagini-service", + "serviceIP": "", + "servicePort": 3000, + "serviceType": "imagini", + "serviceDescription": "", + "redis": { + "url": "127.0.0.1", + "port": 6379, + "db": 15 + } + } +} diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/imagini-service.js b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/imagini-service.js new file mode 100644 index 0000000000..5867fd8750 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/imagini-service.js @@ -0,0 +1,28 @@ +/** +* @name Imagini +* @summary Imagini Hydra Express service entry point +* @description +*/ +'use strict'; + +const version = require('./package.json').version; +const hydraExpress = require('hydra-express'); + + + +let config = require('fwsp-config'); + +/** +* Load configuration file and initialize hydraExpress app +*/ +config.init('./config/config.json') + .then(() => { + config.version = version; + return hydraExpress.init(config.getObject(), version, () => { + hydraExpress.registerRoutes({ + '/v1/imagini': require('./routes/imagini-v1-routes') + }); + }); + }) + .then(serviceInfo => console.log('serviceInfo', serviceInfo)) + .catch(err => console.log('err', err)); diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/package.json b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/package.json new file mode 100644 index 0000000000..c334baca2a --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/package.json @@ -0,0 +1,28 @@ +{ + "name": "imagini-service", + "version": "0.0.1", + "author": " <>", + "private": true, + "scripts": { + "start": "node imagini-service.js", + "debug": "node-debug imagini-service.js --debug-brk", + "test": "mocha specs --reporter spec", + "docker": "node scripts/docker.js" + }, + "engines": { + "node": ">=6.2.1" + }, + "dependencies": { + "body-parser": "^1.19.0", + "fwsp-config": "1.1.5", + "fwsp-server-response": "2.2.6", + "hydra-express": "1.7.1", + "sharp": "^0.23.0" + }, + "devDependencies": { + "chai": "3.5.0", + "eslint": "3.16.0", + "mocha": "3.2.0", + "superagent": "3.5.2" + } +} diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/routes/imagini-v1-routes.js b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/routes/imagini-v1-routes.js new file mode 100644 index 0000000000..e3258302ad --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/routes/imagini-v1-routes.js @@ -0,0 +1,100 @@ +/** + * @name imagini-v1-api + * @description This module packages the Imagini API. + */ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const sharp = require('sharp'); +const bodyparser = require('body-parser'); +const hydraExpress = require('hydra-express'); +const ServerResponse = require('fwsp-server-response'); +const hydra = hydraExpress.getHydra(); +const express = hydraExpress.getExpress(); + +let serverResponse = new ServerResponse(); + +express.response.sendError = function (err) { + serverResponse.sendServerError(this, { result: { error: err } }); +}; +express.response.sendOk = function (result) { + serverResponse.sendOk(this, { result }); +}; + +let api = express.Router(); + +api.param('image', (req, res, next, image) => { + if (!image.match(/\.(png|jpg)$/i)) { + return res.sendError('invalid image type/extension'); + } + + req.image = image; + req.localpath = path.join(__dirname, '../uploads', req.image); + + return next(); +}); + +api.post('/:image', bodyparser.raw({ + limit: '10mb', + type: 'image/*' +}), (req, res) => { + let fd = fs.createWriteStream(req.localpath, { + flags: 'w+', + encoding: 'binary' + }); + + fd.end(req.body); + + fd.on('close', () => { + res.sendOk({ size: req.body.length }); + }); +}); + +api.head('/:image', (req, res) => { + fs.access(req.localpath, fs.constants.R_OK, (err) => { + if (err) { + return res.sendError('image not found'); + } + + return res.sendOk(); + }); +}); + +api.get('/:image', (req, res) => { + fs.access(req.localpath, fs.constants.R_OK, (err) => { + if (err) { + return res.sendError('image not found'); + } + + let image = sharp(req.localpath); + + let width = +req.query.width; + let height = +req.query.height; + let blur = +req.query.blur; + let sharpen = +req.query.sharpen; + let greyscale = ['y', 'yes', '1', 'on'].includes(req.query.greyscale); + let flip = ['y', 'yes', '1', 'on'].includes(req.query.flip); + let flop = ['y', 'yes', '1', 'on'].includes(req.query.flop); + + if (width > 0 && height > 0) { + image.ignoreAspectRatio(); + } + + if (width > 0 || height > 0) { + image.resize(width || null, height || null); + } + + if (flip) image.flip(); + if (flop) image.flop(); + if (blur > 0) image.blur(blur); + if (sharpen > 0) image.sharpen(sharpen); + if (greyscale) image.greyscale(); + + res.setHeader('Content-Type', 'image/' + path.extname(req.image).substr(1)); + + image.pipe(res); + }); +}) + +module.exports = api; diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/scripts/docker.js b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/scripts/docker.js new file mode 100644 index 0000000000..5e0850194e --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/scripts/docker.js @@ -0,0 +1,64 @@ +const config = require('fwsp-config'), + fs = require('fs'), + spawn = require('child_process').spawn, + tag = `/${process.env.npm_package_name}:${process.env.npm_package_version}`; +modes = { + build: ['docker', ['build', '-t', tag, process.cwd()], { stdio: 'inherit' }], + run: ['docker', ['run', '-it', tag], { stdio: 'inherit' }], + up: ['docker', ['run', '-d', tag], { detached: true }] +}, + getDockerfile = (exposePort, logger = false) => ` + FROM node:6.3 + MAINTAINER + EXPOSE ${exposePort} + ARG NPM_TOKEN + RUN mkdir -p /usr/src/app + WORKDIR /usr/src/app + ADD . /usr/src/app + RUN echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc + ${logger ? 'RUN npm install pino-elasticsearch -g' : ''} + RUN npm install --production + RUN rm -f .npmrc + CMD ["npm", "start"] + `, + run = (mode) => { + console.log(`Running '${[modes[mode][0], ...modes[mode][1]].join(' ')}'`); + let docker = spawn(...modes[mode]); + docker.on('close', code => console.log(`docker ${mode} exited with code ${code}`)); + }; + +let mode = process.argv[2]; +if (!modes[mode]) { + console.log(`No such mode '${mode}'. Available modes: ${Object.keys(modes).join(', ')}.`); + return; +} +if (mode === 'build') { + if (!fs.existsSync('config/config.json')) { + console.log('config/config.json must exist to build docker image'); + process.exit(1); + } + if (fs.existsSync('Dockerfile')) { + run(mode); + } else { + console.log('No Dockerfile found, loading config.json and generating one...'); + config.init('./config/config.json') + .then(() => { + let Dockerfile = getDockerfile( + config.hydra.servicePort, + config.hydra.plugins && config.hydra.plugins.logger ? true : false + ).split(/\n/).map(v => v.trim()).filter(v => v.length).join('\n'); + console.log(Dockerfile); + fs.writeFile('Dockerfile', Dockerfile, err => { + if (err) { + console.log('Error writing Dockerfile', err); + process.exit(1); + } else { + console.log('Wrote Dockerfile'); + run(mode); + } + }); + }); + } +} else { + run(mode); +} diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/specs/helpers/chai.js b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/specs/helpers/chai.js new file mode 100644 index 0000000000..ae951a6e67 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/specs/helpers/chai.js @@ -0,0 +1,10 @@ +'use strict'; + +var chai = require('chai'); + +chai.config.includeStack = true; + +global.expect = chai.expect; +global.AssertionError = chai.AssertionError; +global.Assertion = chai.Assertion; +global.assert = chai.assert; diff --git a/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/specs/test.js b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/specs/test.js new file mode 100644 index 0000000000..e3013e9ab0 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/2-hydra/imagini-service/specs/test.js @@ -0,0 +1,10 @@ +'use strict'; + +/** +* Change into specs folder so that file loading works. +*/ +process.chdir('./specs'); + +require('./helpers/chai.js'); + +// Tests go here. diff --git a/node/wikibooks-nodejs-microservice/3/3-seneca/.gitignore b/node/wikibooks-nodejs-microservice/3/3-seneca/.gitignore new file mode 100644 index 0000000000..95a48fbcd4 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/3-seneca/.gitignore @@ -0,0 +1 @@ +uploads \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/3/3-seneca/description.md b/node/wikibooks-nodejs-microservice/3/3-seneca/description.md new file mode 100644 index 0000000000..d7569a1ece --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/3-seneca/description.md @@ -0,0 +1,7 @@ +## 3 - 3 세네카 사용 + +- `curl -H 'Content-Type: application/json' --data '{"role":"upload","image":"example.png","data":"'"$(base64 example.png)"'"}' http://localhost:3000/act` + +- `curl -H 'Content-Type: application/json' --data '{"role":"check", "image": "example.png"}' http://localhost:3000/act` + +- `curl -H 'Content-Type: application/json' --data '{"role":"download", "image": "example.png","greyscale":true,"height":100}' http://localhost:3000/act` \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/3/3-seneca/example.png b/node/wikibooks-nodejs-microservice/3/3-seneca/example.png new file mode 100644 index 0000000000..fd4089b315 Binary files /dev/null and b/node/wikibooks-nodejs-microservice/3/3-seneca/example.png differ diff --git a/node/wikibooks-nodejs-microservice/3/3-seneca/imagini.js b/node/wikibooks-nodejs-microservice/3/3-seneca/imagini.js new file mode 100644 index 0000000000..cdee4e4d98 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/3-seneca/imagini.js @@ -0,0 +1,48 @@ +const sharp = require('sharp'); +const path = require('path'); +const fs = require('fs'); + +module.exports = function (settings = { path: 'uploads' }) { + const localpath = (image) => (path.join(settings.path, image)); + + const access = (filename, next) => { + fs.access(filename, fs.constants.R_OK, (err) => { + return next(!err, filename); + }); + }; + + this.add('role:check,image:*', function (msg, next) { + access(localpath(msg.image), (exists) => (next(null, { exists }))); + }); + + this.add('role:upload,image:*,data:*', (msg, next) => { + let data = Buffer.from(msg.data, 'base64'); + + fs.writeFile(localpath(msg.image), data, (err) => { + return next(err, { size: data.length }); + }); + }); + + this.add('role:download,image:*', (msg, next) => { + access(localpath(msg.image), (exists, filename) => { + if (!exists) return next(new Error('image not found')); + + let image = sharp(filename); + + let width = +msg.width || null; + let height = +msg.height || null; + + if (width && height) image.ignoreAspectRatio(); + if (width || height) image.resize(width || null, height || null); + if (msg.flip) image.flip(); + if (msg.flop) image.flop(); + if (msg.blur > 0) image.blur(msg.blur); + if (msg.sharpen > 0) image.sharpen(msg.sharpen); + if (msg.greyscale) image.greyscale(); + + image.toBuffer().then((data) => { + return next(null, { data: data.toString('base64') }); + }); + }) + }) +}; \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/3/3-seneca/package.json b/node/wikibooks-nodejs-microservice/3/3-seneca/package.json new file mode 100644 index 0000000000..1fa47cbcf4 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/3-seneca/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "seneca": "^3.14.1", + "sharp": "^0.23.0" + } +} diff --git a/node/wikibooks-nodejs-microservice/3/3-seneca/seneca.js b/node/wikibooks-nodejs-microservice/3/3-seneca/seneca.js new file mode 100644 index 0000000000..2bb704af40 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/3/3-seneca/seneca.js @@ -0,0 +1,6 @@ +const seneca = require('seneca'); +const service = seneca({ log: 'silent' }); + +service.use('./imagini.js', { path: __dirname + '/uploads' }); + +service.listen(3000); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/4/mysql/description.md b/node/wikibooks-nodejs-microservice/4/mysql/description.md new file mode 100644 index 0000000000..05f236acb4 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/4/mysql/description.md @@ -0,0 +1,7 @@ +## 4 상태와 보안 - 1 상태 - 2 MySQL + +- `curl -X POST -H 'Content-Type: image/png' --data-binary @example.png http://localhost:3000/uploads/test.png` + +- `curl --head http://localhost:3000/uploads/test.png` + +- `curl -v -X DELETE http://localhost:3000/uploads/test.png` \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/4/mysql/example.png b/node/wikibooks-nodejs-microservice/4/mysql/example.png new file mode 100644 index 0000000000..fd4089b315 Binary files /dev/null and b/node/wikibooks-nodejs-microservice/4/mysql/example.png differ diff --git a/node/wikibooks-nodejs-microservice/4/mysql/imagini.js b/node/wikibooks-nodejs-microservice/4/mysql/imagini.js new file mode 100644 index 0000000000..fb0a5efe1a --- /dev/null +++ b/node/wikibooks-nodejs-microservice/4/mysql/imagini.js @@ -0,0 +1,133 @@ +const settings = require('./settings'); +const mysql = require('mysql'); +const db = mysql.createConnection(settings.db); +const express = require('express'); +const sharp = require('sharp'); +const bodyparser = require('body-parser'); +const path = require('path'); +const app = express(); + +db.connect((err) => { + if (err) throw err; + + console.log('db: ready'); + + db.query(` + CREATE TABLE IF NOT EXISTS images + ( + id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, + date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_used TIMESTAMP NULL DEFAULT NULL, + name VARCHAR(300) NOT NULL, + size INT(11) UNSIGNED NOT NULL, + data LONGBLOB NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY name (name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + `); + + setInterval(() => { + db.query(` + DELETE FROM images + WHERE (date_created < UTC_TIMESTAMP - INTERVAL 1 WEEK AND date_used IS NULL) + OR (date_used < UTC_TIMESTAMP - INTERVAL 1 MONTH) + `); + }, 3600 * 1000); + + app.param('image', (req, res, next, image) => { + if (!image.match(/\.(png|jpg)$/i)) { + return res.status(403).end(); + } + + db.query("SELECT * FROM images WHERE name = ?", [image], (err, images) => { + if (err || !images.length) { + return res.status(404).end(); + } + + req.image = images[0]; + + return next(); + }); + }); + + app.post('/uploads/:name', bodyparser.raw({ + limit: '10mb', + type: 'image/*' + }), (req, res) => { + db.query("INSERT INTO images SET ?", { + name: req.params.name, + size: req.body.length, + data: req.body + }, (err) => { + if (err) { + return res.send({ status: 'error', code: err.code }); + } + + res.send({ status: 'ok', size: req.body.length }); + }); + }); + + app.head('/uploads/:image', (req, res) => { + return res.status(200).end(); + }); + + app.get("/uploads/:image", (req, res) => { + let image = sharp(req.image.data); + + let width = +req.query.width; + let height = +req.query.height; + let blur = +req.query.blur; + let sharpen = +req.query.sharpen; + let greyscale = ['y', 'yes', '1', 'on'].includes(req.query.greyscale); + let flip = ['y', 'yes', '1', 'on'].includes(req.query.flip); + let flop = ['y', 'yes', '1', 'on'].includes(req.query.flop); + + if (width > 0 && height > 0) { + image.ignoreAspectRatio(); + } + + if (width > 0 || height > 0) { + image.resize(width || null, height || null); + } + + if (flip) image.flip(); + if (flop) image.flop(); + if (blur > 0) image.blur(blur); + if (sharpen > 0) image.sharpen(sharpen); + if (greyscale) image.greyscale(); + + db.query("UPDATE images SET date_used = UTC_TIMESTAMP WHERE id = ?", [req.image.id]); + + res.setHeader('Content-Type', 'image/' + path.extname(req.image.name).substr(1)); + + image.pipe(res); + }); + + app.delete('/uploads/:image', (req, res) => { + db.query('DELETE FROM images WHERE id = ?', [req.image.id], (err) => { + return res.status(err ? 500 : 200).end(); + }); + }); + + app.get('/stats', (req, res) => { + db.query(` + SELECT + COUNT(*) total, + SUM(size) size, + MAX(date_created) last_created + FROM images + `, (err, rows) => { + if (err) { + return res.status(500).end(); + } + + rows[0].uptime = process.uptime(); + + return res.send(rows[0]); + }); + }) + + app.listen(3000, () => { + console.log('app: ready'); + }); +}) \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/4/mysql/package.json b/node/wikibooks-nodejs-microservice/4/mysql/package.json new file mode 100644 index 0000000000..b73601350b --- /dev/null +++ b/node/wikibooks-nodejs-microservice/4/mysql/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "body-parser": "^1.19.0", + "express": "^4.17.1", + "mysql": "^2.17.1", + "sharp": "^0.19.0" + } +} diff --git a/node/wikibooks-nodejs-microservice/4/mysql/settings.json b/node/wikibooks-nodejs-microservice/4/mysql/settings.json new file mode 100644 index 0000000000..dc44d360b6 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/4/mysql/settings.json @@ -0,0 +1,3 @@ +{ + "db": "mysql://root@localhost/imagini" +} \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/4/redis/example.png b/node/wikibooks-nodejs-microservice/4/redis/example.png new file mode 100644 index 0000000000..fd4089b315 Binary files /dev/null and b/node/wikibooks-nodejs-microservice/4/redis/example.png differ diff --git a/node/wikibooks-nodejs-microservice/4/redis/imagini.js b/node/wikibooks-nodejs-microservice/4/redis/imagini.js new file mode 100644 index 0000000000..a0032272a7 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/4/redis/imagini.js @@ -0,0 +1,88 @@ +const express = require('express'); +const sharp = require('sharp'); +const bodyparser = require('body-parser'); +const path = require('path'); +const redis = require('redis'); +const db = redis.createClient(); +const app = express(); + +db.on('connect', () => { + console.log('db: ready'); + + app.post('/uploads/:name', bodyparser.raw({ + limit: '10mb', + type: 'image/*' + }), (req, res) => { + db.hmset(req.params.name, { + size: req.body.length, + data: req.body.toString('base64') + }, (err) => { + if (err) { + return res.send({ status: 'error', code: err.code }); + } + + res.send({ status: 'ok', size: req.body.length }); + }); + }); + + app.param('image', (req, res, next, name) => { + if (!name.match(/\.(png|jpg)$/i)) { + return res.status(req.method === 'POST' ? 403 : 404).end(); + } + + db.hgetall(name, (err, image) => { + if (err || !image) return res.status(404).end(); + + req.image = image; + req.image.name = name; + + return next(); + }); + }); + + app.head('/uploads/:image', (req, res) => { + return res.status(200).end(); + }); + + app.get("/uploads/:image", (req, res) => { + let image = sharp(Buffer.from(req.image.data, 'base64')); + + let width = +req.query.width; + let height = +req.query.height; + let blur = +req.query.blur; + let sharpen = +req.query.sharpen; + let greyscale = ['y', 'yes', '1', 'on'].includes(req.query.greyscale); + let flip = ['y', 'yes', '1', 'on'].includes(req.query.flip); + let flop = ['y', 'yes', '1', 'on'].includes(req.query.flop); + + if (width > 0 && height > 0) { + image.ignoreAspectRatio(); + } + + if (width > 0 || height > 0) { + image.resize(width || null, height || null); + } + + if (flip) image.flip(); + if (flop) image.flop(); + if (blur > 0) image.blur(blur); + if (sharpen > 0) image.sharpen(sharpen); + if (greyscale) image.greyscale(); + + db.hset(req.image.name, 'date_used', Date.now()); + + res.setHeader('Content-Type', 'image/' + path.extname(req.image.name).substr(1)); + + image.pipe(res); + }); + + app.delete('/uploads/:image', (req, res) => { + db.del(req.image.name, (err) => { + return res.status(err ? 500 : 200).end(); + }) + }); + + app.listen(3000, () => { + console.log('app: ready'); + }); +}); diff --git a/node/wikibooks-nodejs-microservice/4/redis/package.json b/node/wikibooks-nodejs-microservice/4/redis/package.json new file mode 100644 index 0000000000..7b0c2fe046 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/4/redis/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "body-parser": "^1.19.0", + "express": "^4.17.1", + "redis": "^2.8.0", + "sharp": "^0.19.0" + } +} diff --git a/node/wikibooks-nodejs-microservice/4/rethinkdb/example.png b/node/wikibooks-nodejs-microservice/4/rethinkdb/example.png new file mode 100644 index 0000000000..fd4089b315 Binary files /dev/null and b/node/wikibooks-nodejs-microservice/4/rethinkdb/example.png differ diff --git a/node/wikibooks-nodejs-microservice/4/rethinkdb/imagini.js b/node/wikibooks-nodejs-microservice/4/rethinkdb/imagini.js new file mode 100644 index 0000000000..ba2df15b1c --- /dev/null +++ b/node/wikibooks-nodejs-microservice/4/rethinkdb/imagini.js @@ -0,0 +1,133 @@ +const settings = require('./settings'); +const express = require('express'); +const sharp = require('sharp'); +const bodyparser = require('body-parser'); +const path = require('path'); +const rethinkdb = require('rethinkdb'); +const app = express(); + +rethinkdb.connect(settings.db, (err, db) => { + if (err) throw err; + + console.log('db: ready'); + + rethinkdb.tableList().run(db, (err, tables) => { + if (err) throw err; + + if (!tables.includes('images')) { + rethinkdb.tableCreate('images').run(db); + } + }); + + setInterval(() => { + let expiration = Date.now() - (30 * 86400 * 1000); + + rethinkdb.table('images').filter((image) => { + return image('date_used').lt(expiration); + }).delete().run(db); + }, 3600 * 1000); + + app.param('image', (req, res, next, image) => { + if (!image.match(/\.(png|jpg)$/i)) { + return res.status(403).end(); + } + + rethinkdb.table('images').filter({ + name: image + }).limit(1).run(db, (err, images) => { + if (err) throw res.status(404).end(); + + images.toArray((err, images) => { + if (err) return res.status(500).end(); + if (!images.length) return res.status(404).end(); + + req.image = images[0]; + + return next(); + }); + }); + }); + + app.post('/uploads/:name', bodyparser.raw({ + limit: '10mb', + type: 'image/*' + }), (req, res) => { + rethinkdb.table('images').insert({ + name: req.params.name, + size: req.body.length, + data: req.body + }).run(db, (err) => { + if (err) { + return res.send({ status: 'error', code: err.code }); + } + + res.send({ status: 'ok', size: req.body.length }); + }); + }); + + app.head('/uploads/:image', (req, res) => { + return res.status(200).end(); + }); + + app.get("/uploads/:image", (req, res) => { + let image = sharp(req.image.data); + + let width = +req.query.width; + let height = +req.query.height; + let blur = +req.query.blur; + let sharpen = +req.query.sharpen; + let greyscale = ['y', 'yes', '1', 'on'].includes(req.query.greyscale); + let flip = ['y', 'yes', '1', 'on'].includes(req.query.flip); + let flop = ['y', 'yes', '1', 'on'].includes(req.query.flop); + + if (width > 0 && height > 0) { + image.ignoreAspectRatio(); + } + + if (width > 0 || height > 0) { + image.resize(width || null, height || null); + } + + if (flip) image.flip(); + if (flop) image.flop(); + if (blur > 0) image.blur(blur); + if (sharpen > 0) image.sharpen(sharpen); + if (greyscale) image.greyscale(); + + rethinkdb.table('images').get(req.image.id).update({ date_used: Date.now() }).run(db); + + res.setHeader('Content-Type', 'image/' + path.extname(req.image.name).substr(1)); + + image.pipe(res); + }); + + app.delete('/uploads/:image', (req, res) => { + rethinkdb.table('images').get(req.image.id).delete().run(db, (err) => { + return res.status(err ? 500 : 200).end(); + }); + }); + + app.get('/stats', (req, res) => { + let uptime = process.uptime(); + + rethinkdb.table('images').count().run(db, (err, total) => { + if (err) return res.status(500).end(); + + rethinkdb.table('images').sum('size').run(db, (err, size) => { + if (err) return res.status(500).end(); + + rethinkdb.table('images').max('date_used').run(db, (err, last_used) => { + if (err) return res.status(500).end(); + + last_used = (last_used ? new Date(last_used.date_used) : null); + + return res.send({ total, size, last_used, uptime }); + }); + }); + }); + }); + + app.listen(3000, () => { + console.log('app: ready'); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/4/rethinkdb/package.json b/node/wikibooks-nodejs-microservice/4/rethinkdb/package.json new file mode 100644 index 0000000000..77f2d973d2 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/4/rethinkdb/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "body-parser": "^1.19.0", + "express": "^4.17.1", + "rethinkdb": "^2.3.3", + "sharp": "^0.19.0" + } +} diff --git a/node/wikibooks-nodejs-microservice/4/rethinkdb/settings.json b/node/wikibooks-nodejs-microservice/4/rethinkdb/settings.json new file mode 100644 index 0000000000..559c40c0c0 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/4/rethinkdb/settings.json @@ -0,0 +1,6 @@ +{ + "db": { + "host": "localhost", + "db": "imagini" + } +} \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/.gitignore b/node/wikibooks-nodejs-microservice/5/.gitignore new file mode 100644 index 0000000000..2b7cecf776 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/.gitignore @@ -0,0 +1,2 @@ +.nyc_output +coverage \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/description.md b/node/wikibooks-nodejs-microservice/5/description.md new file mode 100644 index 0000000000..ad766b28fe --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/description.md @@ -0,0 +1,5 @@ +## 5 - 3 - 2 코드 커버리지 추가 + +- `npx nyc npm test` + +- `npx nyc report --reporter=html` \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/imagini.js b/node/wikibooks-nodejs-microservice/5/imagini.js new file mode 100644 index 0000000000..05bcd60aee --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/imagini.js @@ -0,0 +1,141 @@ +const settings = require('./settings'); +const mysql = require('mysql'); +const db = mysql.createConnection(settings.db); +const express = require('express'); +const sharp = require('sharp'); +const bodyparser = require('body-parser'); +const path = require('path'); +const app = express(); + +app.db = db; + +db.connect((err) => { + if (err) throw err; + + db.query(` + CREATE TABLE IF NOT EXISTS images + ( + id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, + date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_used TIMESTAMP NULL DEFAULT NULL, + name VARCHAR(300) NOT NULL, + size INT(11) UNSIGNED NOT NULL, + data LONGBLOB NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY name (name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + `); + + setInterval(() => { + db.query(` + DELETE FROM images + WHERE (date_created < UTC_TIMESTAMP - INTERVAL 1 WEEK AND date_used IS NULL) + OR (date_used < UTC_TIMESTAMP - INTERVAL 1 MONTH) + `); + }, 3600 * 1000); + + app.param('image', (req, res, next, image) => { + if (!image.match(/\.(png|jpg)$/i)) { + return res.status(403).end(); + } + + db.query("SELECT * FROM images WHERE name = ?", [image], (err, images) => { + if (err || !images.length) { + return res.status(404).end(); + } + + req.image = images[0]; + + return next(); + }); + }); + + app.post('/uploads/:name', bodyparser.raw({ + limit: '10mb', + type: 'image/*' + }), (req, res) => { + db.query("INSERT INTO images SET ?", { + name: req.params.name, + size: req.body.length, + data: req.body + }, (err) => { + if (err) { + return res.send({ status: 'error', code: err.code }); + } + + res.send({ status: 'ok', size: req.body.length }); + }); + }); + + app.head('/uploads/:image', (req, res) => { + return res.status(200).end(); + }); + + app.get("/uploads/:image", (req, res) => { + if (Object.keys(req.query).length === 0) { + db.query("UPDATE images SET date_used = UTC_TIMESTAMP WHERE id = ?", [req.image.id]); + + res.setHeader("Content-Type", "image/" + path.extname(req.image.name).substr(1)); + + return res.end(req.image.data); + } + + let image = sharp(req.image.data); + + let width = +req.query.width; + let height = +req.query.height; + let blur = +req.query.blur; + let sharpen = +req.query.sharpen; + let greyscale = ['y', 'yes', '1', 'on'].includes(req.query.greyscale); + let flip = ['y', 'yes', '1', 'on'].includes(req.query.flip); + let flop = ['y', 'yes', '1', 'on'].includes(req.query.flop); + + if (width > 0 && height > 0) { + image.ignoreAspectRatio(); + } + + if (width > 0 || height > 0) { + image.resize(width || null, height || null); + } + + if (flip) image.flip(); + if (flop) image.flop(); + if (blur > 0) image.blur(blur); + if (sharpen > 0) image.sharpen(sharpen); + if (greyscale) image.greyscale(); + + db.query("UPDATE images SET date_used = UTC_TIMESTAMP WHERE id = ?", [req.image.id]); + + res.setHeader('Content-Type', 'image/' + path.extname(req.image.name).substr(1)); + + image.pipe(res); + }); + + app.delete('/uploads/:image', (req, res) => { + db.query('DELETE FROM images WHERE id = ?', [req.image.id], (err) => { + return res.status(err ? 500 : 200).end(); + }); + }); + + app.get('/stats', (req, res) => { + db.query(` + SELECT + COUNT(*) total, + SUM(size) size, + MAX(date_used) last_used + FROM images + `, (err, rows) => { + if (err) { + return res.status(500).end(); + } + + rows[0].uptime = process.uptime(); + + return res.send(rows[0]); + }); + }) + + app.listen(3000); +}) + +module.exports = app; \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/package.json b/node/wikibooks-nodejs-microservice/5/package.json new file mode 100644 index 0000000000..bfc367cc9e --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/package.json @@ -0,0 +1,17 @@ +{ + "dependencies": { + "body-parser": "^1.19.0", + "express": "^4.17.1", + "mysql": "^2.17.1", + "sharp": "^0.19.0" + }, + "devDependencies": { + "chai": "^4.2.0", + "chai-http": "^4.3.0", + "mocha": "^6.2.0", + "sinon": "^7.4.2" + }, + "scripts": { + "test": "node test/run" + } +} diff --git a/node/wikibooks-nodejs-microservice/5/settings.json b/node/wikibooks-nodejs-microservice/5/settings.json new file mode 100644 index 0000000000..88afb918a1 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/settings.json @@ -0,0 +1,3 @@ +{ + "db": "mysql://root@localhost/imagini" +} \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/test/integration/image-check.js b/node/wikibooks-nodejs-microservice/5/test/integration/image-check.js new file mode 100644 index 0000000000..bd77bc72da --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/test/integration/image-check.js @@ -0,0 +1,48 @@ +const chai = require('chai'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Checking image', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_check.png') + .end(() => { + return done(); + }); + }); + + it("should return 404 if it doesn't exist", (done) => { + chai + .request(tools.service) + .head('/uploads/test_image_check.png') + .end((err, res) => { + chai.expect(res).to.have.status(404); + + return done(); + }); + }); + + it('should return 200 if it exists', (done) => { + chai + .request(tools.service) + .post('/uploads/test_image_check.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + chai + .request(tools.service) + .head('/uploads/test_image_check.png') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + return done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/test/integration/image-delete-old.js b/node/wikibooks-nodejs-microservice/5/test/integration/image-delete-old.js new file mode 100644 index 0000000000..1c7b55bfdf --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/test/integration/image-delete-old.js @@ -0,0 +1,24 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Deleting older image', () => { + let clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); + + it('should run every hour', (done) => { + chai + .request(tools.service) + .get('/stats') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + clock.tick(3600 * 1000); + clock.restore(); + + return done(); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/test/integration/image-delete.js b/node/wikibooks-nodejs-microservice/5/test/integration/image-delete.js new file mode 100644 index 0000000000..659ee556de --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/test/integration/image-delete.js @@ -0,0 +1,70 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Deleting image', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_delete.png') + .end(() => { + return done(); + }); + }); + + it('should return 200 if it exists', (done) => { + chai + .request(tools.service) + .post('/uploads/test_image_delete.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + chai + .request(tools.service) + .delete('/uploads/test_image_delete.png') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + return done(); + }); + }); + }); + + it('should return 500 if it exists', (done) => { + chai + .request(tools.service) + .post('/uploads/test_image_delete.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + let query = sinon.stub(tools.service.db, 'query'); + + query + .withArgs('DELETE FROM images WHERE id = ?') + .callsArgWithAsync(2, new Error('Fake')); + + query + .callThrough(); + + chai + .request(tools.service) + .delete('/uploads/test_image_delete.png') + .end((err, res) => { + chai.expect(res).to.have.status(500); + + query.restore(); + + return done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/test/integration/image-download.js b/node/wikibooks-nodejs-microservice/5/test/integration/image-download.js new file mode 100644 index 0000000000..fddeba2d78 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/test/integration/image-download.js @@ -0,0 +1,109 @@ +const chai = require('chai'); +const http = require('chai-http'); +const tools = require('../tools'); +const sharp = require('sharp'); + +chai.use(http); + +describe('Downloading image', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_download.png') + .end(() => { + chai + .request(tools.service) + .post('/uploads/test_image_download.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + return done(); + }); + }); + }); + + it('should return the original image size if no parameters given', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_download.png') + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.length(tools.sample.length); + + return done(); + }); + }); + + it('should be able to resize the image as we request', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_download.png?width=200&height=100') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + + let image = sharp(res.body); + + image + .metadata() + .then((metadata) => { + chai.expect(metadata).to.have.property('width', 200); + chai.expect(metadata).to.have.property('height', 100); + + return done(); + }); + }); + }); + + it("should be able to resize the image width as we request", (done) => { + chai + .request(tools.service) + .get("/uploads/test_image_download.png?width=200") + .end((err, res) => { + chai.expect(res).to.have.status(200); + + let image = sharp(res.body); + + image + .metadata() + .then((metadata) => { + chai.expect(metadata).to.have.property("width", 200); + + return done(); + }); + }); + }); + + it("should be able to resize the image height as we request", (done) => { + chai + .request(tools.service) + .get("/uploads/test_image_download.png?height=100") + .end((err, res) => { + chai.expect(res).to.have.status(200); + + let image = sharp(res.body); + + image + .metadata() + .then((metadata) => { + chai.expect(metadata).to.have.property("height", 100); + + return done(); + }); + }); + }); + + it('should be able add image effects as we request', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_download.png?flip=y&flop=y&greyscale=y&blur=10&sharpen=10') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + return done(); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/test/integration/image-parameter.js b/node/wikibooks-nodejs-microservice/5/test/integration/image-parameter.js new file mode 100644 index 0000000000..55754bdd1f --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/test/integration/image-parameter.js @@ -0,0 +1,38 @@ +const chai = require('chai'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('The image parameter', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_upload.png') + .end(() => { + return done(); + }); + }); + + it('should reply 403 fro non image extension', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_paramter.txt') + .end((err, res) => { + chai.expect(res).to.have.status(403); + + return done(); + }); + }); + + it('should reply 404 for non image existence', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_parameter.png') + .end((err, res) => { + chai.expect(res).to.have.status(404); + + return done(); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/test/integration/image-stats.js b/node/wikibooks-nodejs-microservice/5/test/integration/image-stats.js new file mode 100644 index 0000000000..80e0c106a1 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/test/integration/image-stats.js @@ -0,0 +1,51 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Statistics', () => { + it('should return an object with total, size, last_used and uptime', (done) => { + chai + .request(tools.service) + .get('/stats') + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.property('total'); + chai.expect(res.body).to.have.property('size'); + chai.expect(res.body).to.have.property('last_used'); + chai.expect(res.body).to.have.property('uptime'); + + return done(); + }); + }); + + it('should return 500 if a database error happens', (done) => { + let query = sinon.stub(tools.service.db, 'query'); + + query + .withArgs(` + SELECT + COUNT(*) total, + SUM(size) size, + MAX(date_used) last_used + FROM images + `) + .callsArgWithAsync(1, new Error('Fake')); + + query + .callThrough(); + + chai + .request(tools.service) + .get('/stats') + .end((err, res) => { + chai.expect(res).to.have.status(500); + + query.restore(); + + return done(); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/test/integration/image-upload.js b/node/wikibooks-nodejs-microservice/5/test/integration/image-upload.js new file mode 100644 index 0000000000..5eb1f0dc05 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/test/integration/image-upload.js @@ -0,0 +1,56 @@ +const chai = require('chai'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Uploading image', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_upload.png') + .end(() => { + return done(); + }); + }); + + it('should accept a PNG images', function (done) { + chai + .request(tools.service) + .post('/uploads/test_image_upload.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + return done(); + }); + }); + + it('should deny duplicated images', (done) => { + chai + .request(tools.service) + .post('/uploads/test_image_upload.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + chai + .request(tools.service) + .post('/uploads/test_image_upload.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('error'); + chai.expect(res.body).to.have.property('code', 'ER_DUP_ENTRY'); + + return done(); + }); + + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/test/run.js b/node/wikibooks-nodejs-microservice/5/test/run.js new file mode 100644 index 0000000000..5a97128391 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/test/run.js @@ -0,0 +1,17 @@ +const fs = require('fs'); +const path = require('path'); +const mocha = require('mocha'); +const suite = new mocha(); + +fs.readdir(path.join(__dirname, 'integration'), (err, files) => { + if (err) throw err; + + files.filter((filename) => (filename.match(/\.js$/))) + .map((filename) => { + suite.addFile(path.join(__dirname, 'integration', filename)); + }); + + suite.run((failures) => { + process.exit(failures); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/5/test/sample.png b/node/wikibooks-nodejs-microservice/5/test/sample.png new file mode 100644 index 0000000000..fd4089b315 Binary files /dev/null and b/node/wikibooks-nodejs-microservice/5/test/sample.png differ diff --git a/node/wikibooks-nodejs-microservice/5/test/tools.js b/node/wikibooks-nodejs-microservice/5/test/tools.js new file mode 100644 index 0000000000..cdc9096b3c --- /dev/null +++ b/node/wikibooks-nodejs-microservice/5/test/tools.js @@ -0,0 +1,5 @@ +const fs = require('fs'); +const path = require('path'); + +exports.service = require('../imagini.js'); +exports.sample = fs.readFileSync(path.join(__dirname, 'sample.png')); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/.gitignore b/node/wikibooks-nodejs-microservice/6/.gitignore new file mode 100644 index 0000000000..0ac6e33b12 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/.gitignore @@ -0,0 +1,3 @@ +.nyc_output +coverage +mysql \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/Dockerfile b/node/wikibooks-nodejs-microservice/6/Dockerfile new file mode 100644 index 0000000000..87bfd51b41 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/Dockerfile @@ -0,0 +1,13 @@ +FROM node:10 +MAINTAINER neverlish + +ADD imagini/imagini.js /opt/app/imagini.js +ADD imagini/package.json /opt/app/package.json +ADD imagini/settings.json /opt/app/settings.json + +WORKDIR /opt/app +RUN npm i + +EXPOSE 3000 + +CMD ["node", "/opt/app/imagini"] \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/description.md b/node/wikibooks-nodejs-microservice/6/description.md new file mode 100644 index 0000000000..c2ab5dcdc1 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/description.md @@ -0,0 +1,30 @@ +## 6 - 3 도커를 사용한 배포 - 2 도커파일 정의 + +- `docker pull node:alpine` +- `docker build -t imagini:0.0.1 .` +- `docker run -i -t imagini:0.0.1 sh` +- `docker run -v $(pwd)/settings.json:/opt/app/settings.json imagini:0.0.1` + +## 6 - 3 - 3 컨테이너 관리 +- `time docker stop CONTAINER_ID` +- `docker run -d -p 80:3000 -v $(pwd)/settings.json:/opt/app/settings.json imagini:0.0.1` + +## 6 - 3 - 4 컨테이너 정리 +- `docker rm $(docker ps -qa)` +- `docker rmi $(docker images -q)` + +## 6 - 4 MySQL 배포 +- `docker network create imagini` +- `docker network ls` +- `docker run --name imagini-database --network imagini -v $(pwd)/mysql:/var/lib/mysql -e MYSQL_DATABASE=imagini -e MYSQL_ROOT_PASSWORD=secret -d mysql:5.7` +- `docker run --rm -t -i --network imagini node:latest bash` + - `# ping imagini-database -c 5` +- `docker run --name imagini-service --network imagini -p 80:3000 -d -v $(pwd)/settings.json:/opt/app/settings.json imagini:0.0.1` + +## 6 - 5 도커 컴포즈 사용 +- `docker-compose up -d` + +## 6 - 5 - 1 도커 컴포즈 고급 활용 +- `docker-compose logs service` +- `docker-compose ps` +- `docker-compose down` \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/docker-compose.yml b/node/wikibooks-nodejs-microservice/6/docker-compose.yml new file mode 100644 index 0000000000..2feb24f7b7 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3" +networks: + imagini: +services: + database: + image: mysql:5.7 + networks: + - imagini + volumes: + - ${PWD}/mysql:/var/lib/mysql + environment: + MYSQL_DATABASE: imagini + MYSQL_ROOT_PASSWORD: secret + service: + image: imagini:0.0.1 + networks: + - imagini + volumes: + - ${PWD}/settings.json:/opt/app/settings.json + ports: + - "80:3000" + restart: on-failure diff --git a/node/wikibooks-nodejs-microservice/6/imagini/description.md b/node/wikibooks-nodejs-microservice/6/imagini/description.md new file mode 100644 index 0000000000..ad766b28fe --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/description.md @@ -0,0 +1,5 @@ +## 5 - 3 - 2 코드 커버리지 추가 + +- `npx nyc npm test` + +- `npx nyc report --reporter=html` \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/imagini.js b/node/wikibooks-nodejs-microservice/6/imagini/imagini.js new file mode 100644 index 0000000000..a4f68e2840 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/imagini.js @@ -0,0 +1,149 @@ +const settings = require('./settings'); +const mysql = require('mysql'); +const db = mysql.createConnection(settings.db); +const express = require('express'); +const sharp = require('sharp'); +const bodyparser = require('body-parser'); +const path = require('path'); +const app = express(); + +app.db = db; + +db.connect((err) => { + if (err) throw err; + + db.query(` + CREATE TABLE IF NOT EXISTS images + ( + id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, + date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_used TIMESTAMP NULL DEFAULT NULL, + name VARCHAR(300) NOT NULL, + size INT(11) UNSIGNED NOT NULL, + data LONGBLOB NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY name (name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + `); + + setInterval(() => { + db.query(` + DELETE FROM images + WHERE (date_created < UTC_TIMESTAMP - INTERVAL 1 WEEK AND date_used IS NULL) + OR (date_used < UTC_TIMESTAMP - INTERVAL 1 MONTH) + `); + }, 3600 * 1000); + + app.param('image', (req, res, next, image) => { + if (!image.match(/\.(png|jpg)$/i)) { + return res.status(403).end(); + } + + db.query("SELECT * FROM images WHERE name = ?", [image], (err, images) => { + if (err || !images.length) { + return res.status(404).end(); + } + + req.image = images[0]; + + return next(); + }); + }); + + app.post('/uploads/:name', bodyparser.raw({ + limit: '10mb', + type: 'image/*' + }), (req, res) => { + db.query("INSERT INTO images SET ?", { + name: req.params.name, + size: req.body.length, + data: req.body + }, (err) => { + if (err) { + return res.send({ status: 'error', code: err.code }); + } + + res.send({ status: 'ok', size: req.body.length }); + }); + }); + + app.head('/uploads/:image', (req, res) => { + return res.status(200).end(); + }); + + app.get("/uploads/:image", (req, res) => { + if (Object.keys(req.query).length === 0) { + db.query("UPDATE images SET date_used = UTC_TIMESTAMP WHERE id = ?", [req.image.id]); + + res.setHeader("Content-Type", "image/" + path.extname(req.image.name).substr(1)); + + return res.end(req.image.data); + } + + let image = sharp(req.image.data); + + let width = +req.query.width; + let height = +req.query.height; + let blur = +req.query.blur; + let sharpen = +req.query.sharpen; + let greyscale = ['y', 'yes', '1', 'on'].includes(req.query.greyscale); + let flip = ['y', 'yes', '1', 'on'].includes(req.query.flip); + let flop = ['y', 'yes', '1', 'on'].includes(req.query.flop); + + if (width > 0 && height > 0) { + image.ignoreAspectRatio(); + } + + if (width > 0 || height > 0) { + image.resize(width || null, height || null); + } + + if (flip) image.flip(); + if (flop) image.flop(); + if (blur > 0) image.blur(blur); + if (sharpen > 0) image.sharpen(sharpen); + if (greyscale) image.greyscale(); + + db.query("UPDATE images SET date_used = UTC_TIMESTAMP WHERE id = ?", [req.image.id]); + + res.setHeader('Content-Type', 'image/' + path.extname(req.image.name).substr(1)); + + image.pipe(res); + }); + + app.delete('/uploads/:image', (req, res) => { + db.query('DELETE FROM images WHERE id = ?', [req.image.id], (err) => { + return res.status(err ? 500 : 200).end(); + }); + }); + + app.get('/stats', (req, res) => { + db.query(` + SELECT + COUNT(*) total, + SUM(size) size, + MAX(date_used) last_used + FROM images + `, (err, rows) => { + if (err) { + return res.status(500).end(); + } + + rows[0].uptime = process.uptime(); + + return res.send(rows[0]); + }); + }) + + app.listen(3000, () => { + console.log('ready'); + }); + + process.on('SIGTERM', () => { + db.end(() => { + process.exit(0); + }); + }); +}) + +module.exports = app; \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/package.json b/node/wikibooks-nodejs-microservice/6/imagini/package.json new file mode 100644 index 0000000000..bfc367cc9e --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/package.json @@ -0,0 +1,17 @@ +{ + "dependencies": { + "body-parser": "^1.19.0", + "express": "^4.17.1", + "mysql": "^2.17.1", + "sharp": "^0.19.0" + }, + "devDependencies": { + "chai": "^4.2.0", + "chai-http": "^4.3.0", + "mocha": "^6.2.0", + "sinon": "^7.4.2" + }, + "scripts": { + "test": "node test/run" + } +} diff --git a/node/wikibooks-nodejs-microservice/6/imagini/settings.json b/node/wikibooks-nodejs-microservice/6/imagini/settings.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-check.js b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-check.js new file mode 100644 index 0000000000..bd77bc72da --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-check.js @@ -0,0 +1,48 @@ +const chai = require('chai'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Checking image', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_check.png') + .end(() => { + return done(); + }); + }); + + it("should return 404 if it doesn't exist", (done) => { + chai + .request(tools.service) + .head('/uploads/test_image_check.png') + .end((err, res) => { + chai.expect(res).to.have.status(404); + + return done(); + }); + }); + + it('should return 200 if it exists', (done) => { + chai + .request(tools.service) + .post('/uploads/test_image_check.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + chai + .request(tools.service) + .head('/uploads/test_image_check.png') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + return done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-delete-old.js b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-delete-old.js new file mode 100644 index 0000000000..1c7b55bfdf --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-delete-old.js @@ -0,0 +1,24 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Deleting older image', () => { + let clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); + + it('should run every hour', (done) => { + chai + .request(tools.service) + .get('/stats') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + clock.tick(3600 * 1000); + clock.restore(); + + return done(); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-delete.js b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-delete.js new file mode 100644 index 0000000000..659ee556de --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-delete.js @@ -0,0 +1,70 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Deleting image', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_delete.png') + .end(() => { + return done(); + }); + }); + + it('should return 200 if it exists', (done) => { + chai + .request(tools.service) + .post('/uploads/test_image_delete.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + chai + .request(tools.service) + .delete('/uploads/test_image_delete.png') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + return done(); + }); + }); + }); + + it('should return 500 if it exists', (done) => { + chai + .request(tools.service) + .post('/uploads/test_image_delete.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + let query = sinon.stub(tools.service.db, 'query'); + + query + .withArgs('DELETE FROM images WHERE id = ?') + .callsArgWithAsync(2, new Error('Fake')); + + query + .callThrough(); + + chai + .request(tools.service) + .delete('/uploads/test_image_delete.png') + .end((err, res) => { + chai.expect(res).to.have.status(500); + + query.restore(); + + return done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-download.js b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-download.js new file mode 100644 index 0000000000..fddeba2d78 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-download.js @@ -0,0 +1,109 @@ +const chai = require('chai'); +const http = require('chai-http'); +const tools = require('../tools'); +const sharp = require('sharp'); + +chai.use(http); + +describe('Downloading image', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_download.png') + .end(() => { + chai + .request(tools.service) + .post('/uploads/test_image_download.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + return done(); + }); + }); + }); + + it('should return the original image size if no parameters given', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_download.png') + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.length(tools.sample.length); + + return done(); + }); + }); + + it('should be able to resize the image as we request', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_download.png?width=200&height=100') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + + let image = sharp(res.body); + + image + .metadata() + .then((metadata) => { + chai.expect(metadata).to.have.property('width', 200); + chai.expect(metadata).to.have.property('height', 100); + + return done(); + }); + }); + }); + + it("should be able to resize the image width as we request", (done) => { + chai + .request(tools.service) + .get("/uploads/test_image_download.png?width=200") + .end((err, res) => { + chai.expect(res).to.have.status(200); + + let image = sharp(res.body); + + image + .metadata() + .then((metadata) => { + chai.expect(metadata).to.have.property("width", 200); + + return done(); + }); + }); + }); + + it("should be able to resize the image height as we request", (done) => { + chai + .request(tools.service) + .get("/uploads/test_image_download.png?height=100") + .end((err, res) => { + chai.expect(res).to.have.status(200); + + let image = sharp(res.body); + + image + .metadata() + .then((metadata) => { + chai.expect(metadata).to.have.property("height", 100); + + return done(); + }); + }); + }); + + it('should be able add image effects as we request', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_download.png?flip=y&flop=y&greyscale=y&blur=10&sharpen=10') + .end((err, res) => { + chai.expect(res).to.have.status(200); + + return done(); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-parameter.js b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-parameter.js new file mode 100644 index 0000000000..55754bdd1f --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-parameter.js @@ -0,0 +1,38 @@ +const chai = require('chai'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('The image parameter', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_upload.png') + .end(() => { + return done(); + }); + }); + + it('should reply 403 fro non image extension', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_paramter.txt') + .end((err, res) => { + chai.expect(res).to.have.status(403); + + return done(); + }); + }); + + it('should reply 404 for non image existence', (done) => { + chai + .request(tools.service) + .get('/uploads/test_image_parameter.png') + .end((err, res) => { + chai.expect(res).to.have.status(404); + + return done(); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-stats.js b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-stats.js new file mode 100644 index 0000000000..80e0c106a1 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-stats.js @@ -0,0 +1,51 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Statistics', () => { + it('should return an object with total, size, last_used and uptime', (done) => { + chai + .request(tools.service) + .get('/stats') + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.property('total'); + chai.expect(res.body).to.have.property('size'); + chai.expect(res.body).to.have.property('last_used'); + chai.expect(res.body).to.have.property('uptime'); + + return done(); + }); + }); + + it('should return 500 if a database error happens', (done) => { + let query = sinon.stub(tools.service.db, 'query'); + + query + .withArgs(` + SELECT + COUNT(*) total, + SUM(size) size, + MAX(date_used) last_used + FROM images + `) + .callsArgWithAsync(1, new Error('Fake')); + + query + .callThrough(); + + chai + .request(tools.service) + .get('/stats') + .end((err, res) => { + chai.expect(res).to.have.status(500); + + query.restore(); + + return done(); + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-upload.js b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-upload.js new file mode 100644 index 0000000000..5eb1f0dc05 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/test/integration/image-upload.js @@ -0,0 +1,56 @@ +const chai = require('chai'); +const http = require('chai-http'); +const tools = require('../tools'); + +chai.use(http); + +describe('Uploading image', () => { + beforeEach((done) => { + chai + .request(tools.service) + .delete('/uploads/test_image_upload.png') + .end(() => { + return done(); + }); + }); + + it('should accept a PNG images', function (done) { + chai + .request(tools.service) + .post('/uploads/test_image_upload.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + return done(); + }); + }); + + it('should deny duplicated images', (done) => { + chai + .request(tools.service) + .post('/uploads/test_image_upload.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('ok'); + + chai + .request(tools.service) + .post('/uploads/test_image_upload.png') + .set('Content-Type', 'image/png') + .send(tools.sample) + .end((err, res) => { + chai.expect(res).to.have.status(200); + chai.expect(res.body).to.have.status('error'); + chai.expect(res.body).to.have.property('code', 'ER_DUP_ENTRY'); + + return done(); + }); + + }); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/run.js b/node/wikibooks-nodejs-microservice/6/imagini/test/run.js new file mode 100644 index 0000000000..5a97128391 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/test/run.js @@ -0,0 +1,17 @@ +const fs = require('fs'); +const path = require('path'); +const mocha = require('mocha'); +const suite = new mocha(); + +fs.readdir(path.join(__dirname, 'integration'), (err, files) => { + if (err) throw err; + + files.filter((filename) => (filename.match(/\.js$/))) + .map((filename) => { + suite.addFile(path.join(__dirname, 'integration', filename)); + }); + + suite.run((failures) => { + process.exit(failures); + }); +}); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/sample.png b/node/wikibooks-nodejs-microservice/6/imagini/test/sample.png new file mode 100644 index 0000000000..fd4089b315 Binary files /dev/null and b/node/wikibooks-nodejs-microservice/6/imagini/test/sample.png differ diff --git a/node/wikibooks-nodejs-microservice/6/imagini/test/tools.js b/node/wikibooks-nodejs-microservice/6/imagini/test/tools.js new file mode 100644 index 0000000000..cdc9096b3c --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/imagini/test/tools.js @@ -0,0 +1,5 @@ +const fs = require('fs'); +const path = require('path'); + +exports.service = require('../imagini.js'); +exports.sample = fs.readFileSync(path.join(__dirname, 'sample.png')); \ No newline at end of file diff --git a/node/wikibooks-nodejs-microservice/6/settings.json b/node/wikibooks-nodejs-microservice/6/settings.json new file mode 100644 index 0000000000..94b05b1985 --- /dev/null +++ b/node/wikibooks-nodejs-microservice/6/settings.json @@ -0,0 +1,3 @@ +{ + "db": "mysql://root:secret@database/imagini" +} \ No newline at end of file