├── src ├── router.js ├── log.js ├── utils.js ├── service.js ├── chromium-service.js ├── app.js └── chromium.js ├── .babelrc ├── dev ├── dev.js ├── prod.js ├── config.js ├── setup.md └── Dockerfile ├── README.md ├── package.json ├── LICENSE ├── .gitignore └── yarn.lock /src/router.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | const log4js = require('log4js'); 2 | const logger = log4js.getLogger(); 3 | logger.setLevel('debug'); 4 | module.exports = logger; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": true 6 | } 7 | }], 8 | "stage-3" 9 | ] 10 | } -------------------------------------------------------------------------------- /dev/dev.js: -------------------------------------------------------------------------------- 1 | 2 | process.env.NODE_ENV = 'development'; 3 | 4 | const service = require('../src/service'); 5 | 6 | const config = require('./config.js'); 7 | 8 | service.init(config).serve(); -------------------------------------------------------------------------------- /dev/prod.js: -------------------------------------------------------------------------------- 1 | 2 | process.env.NODE_ENV = 'production'; 3 | 4 | const service = require('../src/service'); 5 | 6 | const config = require('./config.js'); 7 | 8 | service.init(config).serve(); -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('mz/fs'); 3 | 4 | const log = require('./log'); 5 | 6 | exports.createDir = async function createDir(dirPath) { 7 | try { 8 | await fs.mkdir(dirPath); 9 | log.debug(dirPath + ' created'); 10 | } catch (e) { 11 | log.debug(dirPath + ' exists.' + e); 12 | } 13 | }; -------------------------------------------------------------------------------- /dev/config.js: -------------------------------------------------------------------------------- 1 | 2 | const { env } = process; 3 | 4 | const config = { 5 | downloadDir: env.downloadDir || './downloads', 6 | concurrency: env.concurrency || 5, 7 | port: env.port || '3000', 8 | chromiumExec: env.chromiumExec || 'google-chrome', 9 | timeout: env.timeout || 2500, 10 | VirtualTimeBudget: env.VirtualTimeBudget || 1500, 11 | }; 12 | 13 | module.exports = config; -------------------------------------------------------------------------------- /dev/setup.md: -------------------------------------------------------------------------------- 1 | ## 概述 2 | 使用 chrome headless 在 linux 下进行截图 3 | 4 | ## 使用 docker build 5 | [Dockerfile](./Dockerfile) 6 | 7 | ## 环境变量 8 | 9 | ``` 10 | "port=8295" 11 | "concurrency=6" 12 | "chromiumExec=google-chrome" 13 | "timeout=4500" 14 | "VirtualTimeBudget=3500" 15 | ``` 16 | 17 | - port 服务的端口 18 | - concurency 并发数 19 | - chromiumExec chrome 执行地址 20 | - timeout 请求超时时间 21 | - VirtualTimeBudget 渲染等待时间 -------------------------------------------------------------------------------- /src/service.js: -------------------------------------------------------------------------------- 1 | const log = require('./log'); 2 | const App = require('./app'); 3 | const utils = require('./utils'); 4 | 5 | class Service { 6 | static 7 | init(config) { 8 | return new Service(config); 9 | } 10 | constructor(config) { 11 | this.config = config; 12 | } 13 | serve() { 14 | this.setup(); 15 | log.info(this.config); 16 | App(this.config).listen(this.config.port); 17 | } 18 | async setup() { 19 | await utils.createDir(this.config.downloadDir); 20 | } 21 | } 22 | 23 | module.exports = Service; 24 | -------------------------------------------------------------------------------- /dev/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base 2 | FROM node:8.1.2 3 | LABEL GC_BY_CODING_JOB=true 4 | 5 | WORKDIR /app 6 | 7 | COPY imageroot/ / 8 | RUN apt update && \ 9 | apt upgrade -y && \ 10 | apt install -y libxss1 libappindicator1 libindicator7 && \ 11 | wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \ 12 | dpkg -i google-chrome*.deb ; \ 13 | apt install -fy 14 | 15 | RUN wget http://coding-dev.ufile.ucloud.com.cn/SourceHanSansCN.tar.xz 16 | RUN tar -xJf SourceHanSansCN.tar.xz && mv SourceHanSansCN /usr/share/fonts && fc-cache -vf 17 | 18 | COPY ./.src /app/ 19 | 20 | CMD npm run start 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dockerfile# url2pic 2 | A web service for fetching url and converting into image using headless Chromium. 3 | 4 | ## Introduction 5 | This project is inspired by [url2png](https://www.url2png.com/). 6 | 7 | I have no idea what technology behind that. But taking advantage of 8 | [Headless Chrome](https://developers.google.com/web/updates/2017/04/headless-chrome), It quite easy to setup a 9 | micro-service like it. 10 | 11 | schnerd's [post](https://medium.com/@dschnr/using-headless-chrome-as-an-automated-screenshot-tool-4b07dffba79a) and 12 | [repo](https://github.com/schnerd/chrome-headless-screenshots) help a lot. 13 | 14 | ## Usage 15 | 16 | ### Linux 17 | 18 | #### Setup 19 | [setup.md](./dev/setup.md) 20 | 21 | ## Development 22 | 23 | ### prepare 24 | 25 | #### yarn 26 | 27 | ```` 28 | brew install yarn 29 | ``` 30 | 31 | #### Global Binary 32 | ``` 33 | yarn global add pm2 34 | ``` 35 | 36 | ### dev 37 | 38 | ``` 39 | yarn dev 40 | ``` -------------------------------------------------------------------------------- /src/chromium-service.js: -------------------------------------------------------------------------------- 1 | 2 | const async = require('async'); 3 | const Chromium = require('./chromium'); 4 | 5 | class ChromiumService { 6 | constructor(config) { 7 | this.config = config; 8 | this.chromium = new Chromium(config); 9 | this.queue = async.queue(this.worker.bind(this), config.concurrency); 10 | } 11 | 12 | async screenshot(params) { 13 | params.action = 'screenshot'; 14 | return new Promise((resolve, reject) => { 15 | params.finishCallback = resolve; 16 | this.queue.push(params, reject); 17 | }); 18 | } 19 | 20 | worker(task, cb) { 21 | if (task.action === 'screenshot') { 22 | this.chromium.screenshot(task).then(task.finishCallback).then(cb).catch(cb); 23 | return; 24 | } 25 | log.error('should not come here.'); 26 | task.finishCallback(); 27 | cb(); 28 | } 29 | } 30 | 31 | module.exports = ChromiumService; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url2pic", 3 | "version": "0.0.0", 4 | "description": "A web service for fetching url and converting into image using headless Chromium.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node dev/prod.js", 8 | "dev:delete": "pm2 delete dev_url2pic", 9 | "dev:watch": "pm2 start dev/dev.js --no-autorestart --name dev_url2pic --watch src --watch dev", 10 | "dev:log": "pm2 log", 11 | "dev": "yarn dev:delete ; yarn dev:watch && yarn dev:log" 12 | }, 13 | "repository": "git@github.com:Coding/url2pic.git", 14 | "author": "wusisu ", 15 | "license": "MIT", 16 | "devDependencies": {}, 17 | "dependencies": { 18 | "async": "^2.5.0", 19 | "delay": "^2.0.0", 20 | "koa": "^2.3.0", 21 | "koa-logger": "^3.0.0", 22 | "lodash": "^4.17.4", 23 | "log4js": "^1.1.1", 24 | "mz": "^2.6.0" 25 | }, 26 | "engines": { 27 | "node": ">= 7.6.0" 28 | }, 29 | "os": [ 30 | "darwin", 31 | "linux" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Coding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const log = require('./log'); 3 | const KoaLogger = require('koa-logger'); 4 | const ChromiumService = require('./chromium-service'); 5 | 6 | const App = function (config) { 7 | const app = new Koa(); 8 | 9 | app.use(KoaLogger()); 10 | 11 | const chromiumService = new ChromiumService(config); 12 | 13 | app.use(async (ctx) => { 14 | if (ctx.path !== '/internal/screenshot') { 15 | ctx.state = 404; 16 | return; 17 | } 18 | // cache is ok 19 | if (ctx.fresh) { 20 | ctx.status = 304; 21 | return; 22 | } 23 | const query = ctx.query; 24 | const params = { 25 | width: query.width || 1440, 26 | height: query.height || 900, 27 | id: query.id, 28 | etag: query.etag || 'no', 29 | force: query.force === 'true', 30 | url: decodeURIComponent(query.url), 31 | }; 32 | const data = await chromiumService.screenshot(params); 33 | ctx.status = 200; 34 | ctx.type = 'png'; 35 | ctx.etag = query.etag; 36 | ctx.body = data; 37 | }); 38 | 39 | app.on('error', err => 40 | log.error(err) 41 | ); 42 | 43 | return app; 44 | }; 45 | 46 | module.exports = App; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | cache 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # idea 64 | .idea/ 65 | 66 | # General 67 | *.DS_Store 68 | .AppleDouble 69 | .LSOverride 70 | 71 | # Icon must end with two \r 72 | Icon 73 | 74 | 75 | # Thumbnails 76 | ._* 77 | 78 | # Files that might appear in the root of a volume 79 | .DocumentRevisions-V100 80 | .fseventsd 81 | .Spotlight-V100 82 | .TemporaryItems 83 | .Trashes 84 | .VolumeIcon.icns 85 | .com.apple.timemachine.donotpresent 86 | 87 | # Directories potentially created on remote AFP share 88 | .AppleDB 89 | .AppleDesktop 90 | Network Trash Folder 91 | Temporary Items 92 | .apdisk 93 | 94 | *~ 95 | 96 | # temporary files which can be created if a process still has a handle open of a deleted file 97 | .fuse_hidden* 98 | 99 | # KDE directory preferences 100 | .directory 101 | 102 | # Linux trash folder which might appear on any partition or disk 103 | .Trash-* 104 | 105 | # .nfs files are created when an open file is removed but is still being accessed 106 | .nfs* 107 | 108 | # Windows thumbnail cache files 109 | Thumbs.db 110 | ehthumbs.db 111 | ehthumbs_vista.db 112 | 113 | # Dump file 114 | *.stackdump 115 | 116 | # Folder config file 117 | Desktop.ini 118 | 119 | # Recycle Bin used on file shares 120 | $RECYCLE.BIN/ 121 | 122 | # Windows Installer files 123 | *.cab 124 | *.msi 125 | *.msm 126 | *.msp 127 | 128 | # Windows shortcuts 129 | *.lnk 130 | -------------------------------------------------------------------------------- /src/chromium.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'); 3 | const fs = require('mz/fs'); 4 | const child_process = require('mz/child_process'); 5 | const _ = require('lodash'); 6 | 7 | const log = require('./log'); 8 | const utils = require('./utils'); 9 | 10 | class Chromium { 11 | constructor(config) { 12 | this.defaultParams = { 13 | userAgent: null, 14 | width: 1440, 15 | height: 900, 16 | url: "https://coding.net", 17 | id: 'default', 18 | action: 'screenshot', 19 | force: false, 20 | etag: 'no', 21 | timeout: ~~config.timeout || 2500, 22 | VirtualTimeBudget: ~~config.VirtualTimeBudget || 1500, 23 | }; 24 | this.config = config; 25 | } 26 | 27 | chromiumUri() { 28 | return this.config.chromiumExec; 29 | } 30 | 31 | async checkEtagFresh(params) { 32 | if (!params.etag) return true; 33 | try { 34 | const lastEtag = await fs.readFile(path.join(params.path, 'etag.txt'), 'utf8'); 35 | return lastEtag !== params.etag; 36 | } catch (e) { 37 | log.warn(`error when fetching etag ${params.path}, error: ${e}`); 38 | return true; 39 | } 40 | } 41 | 42 | async writeEtag(params) { 43 | if (!params.etag) return; 44 | try { 45 | await fs.writeFile(path.join(params.path, 'etag.txt'), params.etag); 46 | } catch (e) { 47 | log.warn(`error when writing etag ${params.path}, error: ${e}`); 48 | } 49 | } 50 | 51 | callChromium(params) { 52 | const uri = this.chromiumUri(); 53 | const args = [ 54 | '--headless', 55 | '--hide-scrollbars', 56 | '--disable-gpu', 57 | `--window-size=${params.width},${params.height}`, 58 | `--timeout=${~~params.timeout}`, 59 | `--virtual-time-budget=${~~params.VirtualTimeBudget}`, 60 | '--screenshot', 61 | ]; 62 | args.push(params.url); 63 | const options = { 64 | cwd: params.path, 65 | }; 66 | log.debug(`${uri} ${args.join(' ')}`); 67 | const chromium = child_process.spawn(uri, args, options); 68 | return new Promise((resolve, reject) => { 69 | chromium.on('close', function (code) { 70 | log.debug('chromium exits with code ' + code); 71 | resolve(code); 72 | }); 73 | chromium.on('error', function (err) { 74 | log.error('chromium error:' + err); 75 | reject(err); 76 | }) 77 | }) 78 | } 79 | 80 | async readFromFile(dirPath) { 81 | try { 82 | return await fs.readFile(path.join(dirPath, 'screenshot.png')); 83 | } catch (err) { 84 | log.error(`read ${dirPath} failed with error: ${err}`); 85 | return null; 86 | } 87 | } 88 | 89 | async screenshot(userParams={}) { 90 | const params = _.extend({}, this.defaultParams, userParams); 91 | params.path = path.join(this.config.downloadDir, params.id); 92 | let fresh = params.force || await this.checkEtagFresh(params); 93 | if (fresh) { 94 | await utils.createDir(params.path); 95 | await this.callChromium(params); 96 | await this.writeEtag(params); 97 | } 98 | return await this.readFromFile(params.path); 99 | } 100 | } 101 | 102 | module.exports = Chromium; -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@^1.2.2: 6 | version "1.3.3" 7 | resolved "http://registry.npm.taobao.org/accepts/download/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 8 | dependencies: 9 | mime-types "~2.1.11" 10 | negotiator "0.6.1" 11 | 12 | ansi-regex@^2.0.0: 13 | version "2.1.1" 14 | resolved "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 15 | 16 | ansi-styles@^2.2.1: 17 | version "2.2.1" 18 | resolved "http://registry.npm.taobao.org/ansi-styles/download/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 19 | 20 | any-promise@^1.0.0, any-promise@^1.1.0: 21 | version "1.3.0" 22 | resolved "http://registry.npm.taobao.org/any-promise/download/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" 23 | 24 | async@^2.5.0: 25 | version "2.5.0" 26 | resolved "http://registry.npm.taobao.org/async/download/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" 27 | dependencies: 28 | lodash "^4.14.0" 29 | 30 | bytes@^2.5.0: 31 | version "2.5.0" 32 | resolved "http://registry.npm.taobao.org/bytes/download/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a" 33 | 34 | chalk@^1.1.3: 35 | version "1.1.3" 36 | resolved "http://registry.npm.taobao.org/chalk/download/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 37 | dependencies: 38 | ansi-styles "^2.2.1" 39 | escape-string-regexp "^1.0.2" 40 | has-ansi "^2.0.0" 41 | strip-ansi "^3.0.0" 42 | supports-color "^2.0.0" 43 | 44 | co@^4.6.0: 45 | version "4.6.0" 46 | resolved "http://registry.npm.taobao.org/co/download/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 47 | 48 | content-disposition@~0.5.0: 49 | version "0.5.2" 50 | resolved "http://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 51 | 52 | content-type@^1.0.0: 53 | version "1.0.2" 54 | resolved "http://registry.npm.taobao.org/content-type/download/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 55 | 56 | cookies@~0.7.0: 57 | version "0.7.0" 58 | resolved "http://registry.npm.taobao.org/cookies/download/cookies-0.7.0.tgz#0bc961d910c35254980fc7c9eff5da12011bbf00" 59 | dependencies: 60 | depd "~1.1.0" 61 | keygrip "~1.0.1" 62 | 63 | core-util-is@~1.0.0: 64 | version "1.0.2" 65 | resolved "http://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 66 | 67 | date-format@^0.0.0: 68 | version "0.0.0" 69 | resolved "http://registry.npm.taobao.org/date-format/download/date-format-0.0.0.tgz#09206863ab070eb459acea5542cbd856b11966b3" 70 | 71 | debug@*, debug@^2.2.0: 72 | version "2.6.8" 73 | resolved "http://registry.npm.taobao.org/debug/download/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" 74 | dependencies: 75 | ms "2.0.0" 76 | 77 | debug@^0.7.2: 78 | version "0.7.4" 79 | resolved "http://registry.npm.taobao.org/debug/download/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" 80 | 81 | deep-equal@~1.0.1: 82 | version "1.0.1" 83 | resolved "http://registry.npm.taobao.org/deep-equal/download/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" 84 | 85 | delay@^2.0.0: 86 | version "2.0.0" 87 | resolved "http://registry.npm.taobao.org/delay/download/delay-2.0.0.tgz#9112eadc03e4ec7e00297337896f273bbd91fae5" 88 | dependencies: 89 | p-defer "^1.0.0" 90 | 91 | delegates@^1.0.0: 92 | version "1.0.0" 93 | resolved "http://registry.npm.taobao.org/delegates/download/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 94 | 95 | depd@1.1.0, depd@^1.1.0, depd@~1.1.0: 96 | version "1.1.0" 97 | resolved "http://registry.npm.taobao.org/depd/download/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 98 | 99 | destroy@^1.0.3: 100 | version "1.0.4" 101 | resolved "http://registry.npm.taobao.org/destroy/download/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 102 | 103 | ee-first@1.1.1: 104 | version "1.1.1" 105 | resolved "http://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 106 | 107 | error-inject@~1.0.0: 108 | version "1.0.0" 109 | resolved "http://registry.npm.taobao.org/error-inject/download/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" 110 | 111 | escape-html@~1.0.1: 112 | version "1.0.3" 113 | resolved "http://registry.npm.taobao.org/escape-html/download/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 114 | 115 | escape-string-regexp@^1.0.2: 116 | version "1.0.5" 117 | resolved "http://registry.npm.taobao.org/escape-string-regexp/download/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 118 | 119 | fresh@^0.5.0: 120 | version "0.5.0" 121 | resolved "http://registry.npm.taobao.org/fresh/download/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" 122 | 123 | has-ansi@^2.0.0: 124 | version "2.0.0" 125 | resolved "http://registry.npm.taobao.org/has-ansi/download/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 126 | dependencies: 127 | ansi-regex "^2.0.0" 128 | 129 | http-assert@^1.1.0: 130 | version "1.3.0" 131 | resolved "http://registry.npm.taobao.org/http-assert/download/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a" 132 | dependencies: 133 | deep-equal "~1.0.1" 134 | http-errors "~1.6.1" 135 | 136 | http-errors@^1.2.8, http-errors@~1.6.1: 137 | version "1.6.1" 138 | resolved "http://registry.npm.taobao.org/http-errors/download/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" 139 | dependencies: 140 | depd "1.1.0" 141 | inherits "2.0.3" 142 | setprototypeof "1.0.3" 143 | statuses ">= 1.3.1 < 2" 144 | 145 | humanize-number@0.0.2: 146 | version "0.0.2" 147 | resolved "http://registry.npm.taobao.org/humanize-number/download/humanize-number-0.0.2.tgz#11c0af6a471643633588588048f1799541489c18" 148 | 149 | inherits@2.0.3, inherits@~2.0.1: 150 | version "2.0.3" 151 | resolved "http://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 152 | 153 | is-generator-function@^1.0.3: 154 | version "1.0.6" 155 | resolved "http://registry.npm.taobao.org/is-generator-function/download/is-generator-function-1.0.6.tgz#9e71653cd15fff341c79c4151460a131d31e9fc4" 156 | 157 | isarray@0.0.1: 158 | version "0.0.1" 159 | resolved "http://registry.npm.taobao.org/isarray/download/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 160 | 161 | keygrip@~1.0.1: 162 | version "1.0.1" 163 | resolved "http://registry.npm.taobao.org/keygrip/download/keygrip-1.0.1.tgz#b02fa4816eef21a8c4b35ca9e52921ffc89a30e9" 164 | 165 | koa-compose@^3.0.0: 166 | version "3.2.1" 167 | resolved "http://registry.npm.taobao.org/koa-compose/download/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" 168 | dependencies: 169 | any-promise "^1.1.0" 170 | 171 | koa-compose@^4.0.0: 172 | version "4.0.0" 173 | resolved "http://registry.npm.taobao.org/koa-compose/download/koa-compose-4.0.0.tgz#2800a513d9c361ef0d63852b038e4f6f2d5a773c" 174 | 175 | koa-convert@^1.2.0: 176 | version "1.2.0" 177 | resolved "http://registry.npm.taobao.org/koa-convert/download/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" 178 | dependencies: 179 | co "^4.6.0" 180 | koa-compose "^3.0.0" 181 | 182 | koa-is-json@^1.0.0: 183 | version "1.0.0" 184 | resolved "http://registry.npm.taobao.org/koa-is-json/download/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" 185 | 186 | koa-logger@^3.0.0: 187 | version "3.0.0" 188 | resolved "http://registry.npm.taobao.org/koa-logger/download/koa-logger-3.0.0.tgz#6ca5bef929a10d17171ceb19b081600ff5add91d" 189 | dependencies: 190 | bytes "^2.5.0" 191 | chalk "^1.1.3" 192 | humanize-number "0.0.2" 193 | passthrough-counter "^1.0.0" 194 | 195 | koa@^2.3.0: 196 | version "2.3.0" 197 | resolved "http://registry.npm.taobao.org/koa/download/koa-2.3.0.tgz#9e1e8e4da401839c57b8527eadc57f76127555a7" 198 | dependencies: 199 | accepts "^1.2.2" 200 | content-disposition "~0.5.0" 201 | content-type "^1.0.0" 202 | cookies "~0.7.0" 203 | debug "*" 204 | delegates "^1.0.0" 205 | depd "^1.1.0" 206 | destroy "^1.0.3" 207 | error-inject "~1.0.0" 208 | escape-html "~1.0.1" 209 | fresh "^0.5.0" 210 | http-assert "^1.1.0" 211 | http-errors "^1.2.8" 212 | is-generator-function "^1.0.3" 213 | koa-compose "^4.0.0" 214 | koa-convert "^1.2.0" 215 | koa-is-json "^1.0.0" 216 | mime-types "^2.0.7" 217 | on-finished "^2.1.0" 218 | only "0.0.2" 219 | parseurl "^1.3.0" 220 | statuses "^1.2.0" 221 | type-is "^1.5.5" 222 | vary "^1.0.0" 223 | 224 | lodash@^4.14.0, lodash@^4.17.4: 225 | version "4.17.4" 226 | resolved "http://registry.npm.taobao.org/lodash/download/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 227 | 228 | log4js@^1.1.1: 229 | version "1.1.1" 230 | resolved "http://registry.npm.taobao.org/log4js/download/log4js-1.1.1.tgz#c21d29c7604089e4f255833e7f94b3461de1ff43" 231 | dependencies: 232 | debug "^2.2.0" 233 | semver "^5.3.0" 234 | streamroller "^0.4.0" 235 | 236 | media-typer@0.3.0: 237 | version "0.3.0" 238 | resolved "http://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 239 | 240 | mime-db@~1.27.0: 241 | version "1.27.0" 242 | resolved "http://registry.npm.taobao.org/mime-db/download/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" 243 | 244 | mime-types@^2.0.7, mime-types@~2.1.11, mime-types@~2.1.15: 245 | version "2.1.15" 246 | resolved "http://registry.npm.taobao.org/mime-types/download/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" 247 | dependencies: 248 | mime-db "~1.27.0" 249 | 250 | minimist@0.0.8: 251 | version "0.0.8" 252 | resolved "http://registry.npm.taobao.org/minimist/download/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 253 | 254 | mkdirp@^0.5.1: 255 | version "0.5.1" 256 | resolved "http://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 257 | dependencies: 258 | minimist "0.0.8" 259 | 260 | ms@2.0.0: 261 | version "2.0.0" 262 | resolved "http://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 263 | 264 | mz@^2.6.0: 265 | version "2.6.0" 266 | resolved "http://registry.npm.taobao.org/mz/download/mz-2.6.0.tgz#c8b8521d958df0a4f2768025db69c719ee4ef1ce" 267 | dependencies: 268 | any-promise "^1.0.0" 269 | object-assign "^4.0.1" 270 | thenify-all "^1.0.0" 271 | 272 | negotiator@0.6.1: 273 | version "0.6.1" 274 | resolved "http://registry.npm.taobao.org/negotiator/download/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 275 | 276 | object-assign@^4.0.1: 277 | version "4.1.1" 278 | resolved "http://registry.npm.taobao.org/object-assign/download/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 279 | 280 | on-finished@^2.1.0: 281 | version "2.3.0" 282 | resolved "http://registry.npm.taobao.org/on-finished/download/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 283 | dependencies: 284 | ee-first "1.1.1" 285 | 286 | only@0.0.2: 287 | version "0.0.2" 288 | resolved "http://registry.npm.taobao.org/only/download/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" 289 | 290 | p-defer@^1.0.0: 291 | version "1.0.0" 292 | resolved "http://registry.npm.taobao.org/p-defer/download/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" 293 | 294 | parseurl@^1.3.0: 295 | version "1.3.1" 296 | resolved "http://registry.npm.taobao.org/parseurl/download/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 297 | 298 | passthrough-counter@^1.0.0: 299 | version "1.0.0" 300 | resolved "http://registry.npm.taobao.org/passthrough-counter/download/passthrough-counter-1.0.0.tgz#1967d9e66da572b5c023c787db112a387ab166fa" 301 | 302 | readable-stream@^1.1.7: 303 | version "1.1.14" 304 | resolved "http://registry.npm.taobao.org/readable-stream/download/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" 305 | dependencies: 306 | core-util-is "~1.0.0" 307 | inherits "~2.0.1" 308 | isarray "0.0.1" 309 | string_decoder "~0.10.x" 310 | 311 | semver@^5.3.0: 312 | version "5.3.0" 313 | resolved "http://registry.npm.taobao.org/semver/download/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" 314 | 315 | setprototypeof@1.0.3: 316 | version "1.0.3" 317 | resolved "http://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 318 | 319 | "statuses@>= 1.3.1 < 2", statuses@^1.2.0: 320 | version "1.3.1" 321 | resolved "http://registry.npm.taobao.org/statuses/download/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 322 | 323 | streamroller@^0.4.0: 324 | version "0.4.1" 325 | resolved "http://registry.npm.taobao.org/streamroller/download/streamroller-0.4.1.tgz#d435bd5974373abd9bd9068359513085106cc05f" 326 | dependencies: 327 | date-format "^0.0.0" 328 | debug "^0.7.2" 329 | mkdirp "^0.5.1" 330 | readable-stream "^1.1.7" 331 | 332 | string_decoder@~0.10.x: 333 | version "0.10.31" 334 | resolved "http://registry.npm.taobao.org/string_decoder/download/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 335 | 336 | strip-ansi@^3.0.0: 337 | version "3.0.1" 338 | resolved "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 339 | dependencies: 340 | ansi-regex "^2.0.0" 341 | 342 | supports-color@^2.0.0: 343 | version "2.0.0" 344 | resolved "http://registry.npm.taobao.org/supports-color/download/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 345 | 346 | thenify-all@^1.0.0: 347 | version "1.6.0" 348 | resolved "http://registry.npm.taobao.org/thenify-all/download/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" 349 | dependencies: 350 | thenify ">= 3.1.0 < 4" 351 | 352 | "thenify@>= 3.1.0 < 4": 353 | version "3.3.0" 354 | resolved "http://registry.npm.taobao.org/thenify/download/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" 355 | dependencies: 356 | any-promise "^1.0.0" 357 | 358 | type-is@^1.5.5: 359 | version "1.6.15" 360 | resolved "http://registry.npm.taobao.org/type-is/download/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 361 | dependencies: 362 | media-typer "0.3.0" 363 | mime-types "~2.1.15" 364 | 365 | vary@^1.0.0: 366 | version "1.1.1" 367 | resolved "http://registry.npm.taobao.org/vary/download/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" 368 | --------------------------------------------------------------------------------