Browse Source

Merge branch 'release/v0.0.6'

Jérôme BUISINE 1 year ago
parent
commit
1b8995d936
14 changed files with 445 additions and 43 deletions
  1. 0 2
      .dockerignore
  2. 1 1
      .eslintrc.js
  3. 1 2
      .gitignore
  4. 21 8
      README.md
  5. 1 1
      back.Dockerfile
  6. 6 3
      config.js
  7. 2 1
      docker-compose.yml
  8. 1 0
      package.json
  9. 5 5
      server/functions.js
  10. 40 12
      server/routes/getImage.js
  11. 165 0
      server/routes/getImageExtracts.js
  12. 2 0
      server/routes/index.js
  13. 3 1
      webhook_deploy_gogs.js
  14. 197 7
      yarn.lock

+ 0 - 2
.dockerignore

@@ -3,5 +3,3 @@ npm-debug.log
 .git
 images
 dist
-combined.log
-error.log

+ 1 - 1
.eslintrc.js

@@ -79,7 +79,7 @@ module.exports = {
     'no-iterator': 2,
     'no-labels': 2,
     'no-lone-blocks': 2,
-    'no-loop-func': 2,
+    'no-loop-func': 0,
     'no-multi-spaces': 2,
     'no-multi-str': 2,
     'no-native-reassign': 2,

+ 1 - 2
.gitignore

@@ -21,5 +21,4 @@ yarn-error.log*
 
 /images
 /dist
-combined.log
-error.log
+/logs

+ 21 - 8
README.md

@@ -21,13 +21,14 @@ Configure more deeply the way the app works by modifying *[config.js](config.js)
 
 | Option      | Default value | Description |
 | ----------- | ------------- | ----------- |
-| apiPrefix | `/api` | The url prefix for the API |
-| imageServedUrl | `/api/images` | The url prefix from where the images are served |
-| serverPort | `5000` | The port used by the server |
-| imagesPath | `./images` | The directory where the images are stored |
-| serveClient | `true` | Should the server serve client files from the `/dist` directory |
-| fileNameConvention | `/^(.*)?_([0-9]{2,})\.(.*)$/` | File name convention for images |
-| sceneFileNameBlackList | `['config', 'seuilExpe']` | Files to ignore in scenes |
+| `apiPrefix` | `/api` | The url prefix for the API |
+| `imageServedUrl` | `/api/images` | The url prefix from where the images are served |
+| `serverPort` | `5000` | The port used by the server |
+| `imagesPath` | `./images` | The directory where the images are stored |
+| `serveClient` | `true` | Should the server serve client files from the `/dist` directory |
+| `fileNameConvention` | `/^(.*)?_([0-9]{2,})\.(.*)$/` | File name convention for images |
+| `sceneFileNameBlackList` | `['config', 'seuilExpe', 'extracts']` | Files to ignore in scenes |
+| `extractsDirName` | `extracts` | Name of the directory containing extracts |
 
 ### Run server + client
 Linux
@@ -69,7 +70,7 @@ docker-compose build
 docker-compose -f docker-compose.frontapp_only.yml build
 ```
 
-### Using Windows
+#### Using Windows
 When using Windows, it may happen that you can't properly run the containers because of Windows's path system being different. To circumvant this problem, you can do the [following steps](https://github.com/docker/compose/issues/4303#issuecomment-379563170).
 > 1. On **Command Line**: "set COMPOSE_CONVERT_WINDOWS_PATHS=1";
 > 2. Restart **Docker for Windows**;
@@ -131,5 +132,17 @@ yarn run app:dev
 yarn run app:lint
 ```
 
+## Automated deployment
+The app can be automatically deployed when a push event is sent to the master branch using Gogs. Open a port to the web and start *[webhook_deploy_gogs.js](webhook_deploy_gogs.js)*.
+```sh
+WEBHOOK_SECRET=your_webhook_secret WEBHOOK_PORT=5000 SERVE_CLIENT=true PORT=8080 node webhook_deploy_gogs.js
+```
+You can pass any parameters to the script, they will be passed to the Docker instance. The following are required.
+
+| Option      | Description |
+| ----------- | ----------- |
+| `WEBHOOK_SECRET` | The secret set on Gogs to verify the identity |
+| `WEBHOOK_PORT` | The port the script is listening to |
+
 ## License
 [The MIT license](LICENSE)

+ 1 - 1
back.Dockerfile

@@ -5,7 +5,7 @@ COPY . /usr/src/app
 
 WORKDIR /usr/src/app
 
-EXPOSE $PORT
+EXPOSE 5000
 
 RUN yarn install
 

+ 6 - 3
config.js

@@ -24,15 +24,18 @@ export const serveClient = process.env.SERVE_CLIENT === 'true' || true
 export const fileNameConvention = /^(.*)?_([0-9]{2,})\.(.*)$/
 
 // Files to ignore in scenes
-export const sceneFileNameBlackList = ['config', 'seuilExpe']
+export const sceneFileNameBlackList = ['config', 'seuilExpe', 'extracts']
+
+// Name of the directory containing extracts
+export const extractsDirName = 'extracts'
 
 // Logger configuration
 export const logger = winston.createLogger({
   level: 'info',
   format: winston.format.json(),
   transports: [
-    new winston.transports.File({ filename: 'combined.log' }),
-    new winston.transports.File({ filename: 'error.log', level: 'error' }),
+    new winston.transports.File({ filename: 'logs/combined.log' }),
+    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
     new winston.transports.Console({
       level: 'debug',
       handleExceptions: true,

+ 2 - 1
docker-compose.yml

@@ -8,10 +8,11 @@ services:
           dockerfile: back.Dockerfile
       image: backapp
       ports:
-          - "${PORT}:${PORT}"
+          - "${PORT:-5000}:5000"
       environment:
           NODE_ENV: production
           SERVE_CLIENT: "${SERVE_CLIENT:-true}"
           PORT: "${PORT:-5000}"
       volumes:
           - "${IMAGES_PATH:-./images}:/usr/src/app/images"
+          - "./logs:/usr/src/app/logs"

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "helmet": "^3.16.0",
     "morgan": "^1.9.1",
     "serve-static": "^1.13.2",
+    "sharp": "^0.22.0",
     "vue": "^2.6.6",
     "vue-router": "^3.0.1",
     "winston": "^3.2.1"

+ 5 - 5
server/functions.js

@@ -37,7 +37,7 @@ export const errorHandler = (err, req, res, next) => {
 
   // Send the error to the client
   res.status(payload.statusCode).json({
-    message: payload.message,
+    message: err.message,
     data: err.data || undefined
   })
 
@@ -67,7 +67,7 @@ export const checkRequiredParameters = (requiredParameters, parameters) => {
  * @throws invalid scene name
  */
 export const checkSceneName = sceneName => {
-  if (!/^(?!.*\.\.).*$/.test(sceneName))
+  if (sceneName === '' || !/^(?!.*\.\.).*$/.test(sceneName))
     throw boom.conflict(`The requested scene name "${sceneName}" is not valid.`)
 }
 
@@ -87,7 +87,7 @@ export const checkFileName = fileName => {
  * Get all files in a scene
  *
  * @param {string} sceneName the scene name
- * @returns {string[]} the list of all files in the scene
+ * @returns {Promise<string[]>} the list of all files in the scene
  * @throws scene directory is not accessible
  */
 export const getSceneFiles = sceneName => {
@@ -98,7 +98,7 @@ export const getSceneFiles = sceneName => {
   const scenePath = path.resolve(imagesPath, sceneName)
 
   return fs.readdir(scenePath).catch(() => {
-    throw boom.badRequest(`Can't access the "${scenePath}" directory. Check it exists and you have read permission on it.`)
+    throw boom.internal(`Can't access the "${sceneName}" scene directory. Check it exists and you have read permission on it.`)
   })
 }
 
@@ -163,7 +163,7 @@ export const getSceneFilesData = async sceneName => {
 
   // Check if the parse fail list is empty
   if (failList.length > 0)
-    throw boom.conflict(`Failed to parse file names in the "${sceneName}"'s scene directory.`, failList)
+    throw boom.internal(`Failed to parse file names in the "${sceneName}"'s scene directory.`, failList)
 
   return data
 }

+ 40 - 12
server/routes/getImage.js

@@ -1,13 +1,49 @@
 'use strict'
 
 import express from 'express'
+import path from 'path'
 import boom from 'boom'
 
-import { imageServedUrl } from '../../config'
+import { imagesPath, imageServedUrl } from '../../config'
 import { asyncMiddleware, checkSceneName, checkRequiredParameters, getSceneFilesData } from '../functions'
 
 const router = express.Router()
 
+/**
+ * @typedef {Object} Image
+ * @property {string} link the link (URL) to an image on the app
+ * @property {string} path the path to the image in the file system
+ * @property {string} fileName the name of the image
+ * @property {string} sceneName the scene of the image
+ * @property {number} quality the quality of the image
+ * @property {string} ext the extension of the image
+ */
+
+/**
+ * Get the link and path to an image
+ * @param {string} sceneName the scene to get the image from
+ * @param {number} qualityInt the requested quality
+ * @returns {Image} the link and path to the image
+ */
+export const getImage = async (sceneName, qualityInt) => {
+  const sceneData = await getSceneFilesData(sceneName)
+
+  // Search an image with the requested quality in the scene
+  for (const [imageName, imageData] of sceneData.entries())
+    if (qualityInt === imageData.quality)
+      return {
+        link: `${imageServedUrl}/${sceneName}/${imageName}`,
+        path: path.resolve(imagesPath, sceneName, imageName),
+        fileName: imageName,
+        sceneName,
+        quality: imageData.quality,
+        ext: imageData.ext
+      }
+
+  // Image not found
+  throw boom.notFound(`The requested quality (${qualityInt}) was not found for the requested scene (${sceneName}).`)
+}
+
 router.get('/', asyncMiddleware(async (req, res) => {
   // Check the request contains all the required parameters
   checkRequiredParameters(['sceneName', 'imageQuality'], req.query)
@@ -21,7 +57,7 @@ router.get('/', asyncMiddleware(async (req, res) => {
     checkSceneName(sceneName)
   }
   catch (err) {
-    errorList.push(err)
+    errorList.push(err.message)
   }
 
   // Check `imageQuality` is an integer
@@ -32,16 +68,8 @@ router.get('/', asyncMiddleware(async (req, res) => {
   if (errorList.length > 0)
     throw boom.badRequest('Invalid query parameter(s).', errorList)
 
-
-  const sceneData = await getSceneFilesData(sceneName)
-
-  // Search an image with the requested quality in the scene
-  for (const [imageName, imageData] of sceneData.entries()) {
-    if (qualityInt === imageData.quality)
-      return res.json({ link: `${imageServedUrl}/${sceneName}/${imageName}` })
-  }
-
-  throw boom.notFound(`The requested quality (${imageQuality}) was not found for the requested scene (${sceneName}).`)
+  const { link } = await getImage(sceneName, qualityInt)
+  res.json({ link })
 }))
 
 export default router

+ 165 - 0
server/routes/getImageExtracts.js

@@ -0,0 +1,165 @@
+'use strict'
+
+import express from 'express'
+import sharp from 'sharp'
+import { constants as fsConstants, promises as fs } from 'fs'
+import path from 'path'
+import boom from 'boom'
+
+import { asyncMiddleware, checkSceneName, checkRequiredParameters } from '../functions'
+import { getImage } from './getImage'
+import { imageServedUrl, imagesPath, extractsDirName } from '../../config'
+
+const router = express.Router()
+
+/**
+ * @typedef {import('./getImage').Image} Image
+ */
+/**
+ * Cut an image, save its extracts and get the url of these extracts
+ *
+ * @param {Image} image the path to the image to cut
+ * @param {Number} xExtracts the number of extract to do on the horizontal axis (integer)
+ * @param {Number} yExtracts the number of extract to do on the vertical axis (integer)
+ * @returns {Promise<Image[]>} the list of extracted images
+ */
+const cutImage = async (image, xExtracts, yExtracts) => {
+  const input = sharp(image.path)
+
+  const { width, height } = await input.metadata()
+
+  const xCropSize = width / xExtracts
+  const yCropSize = height / yExtracts
+
+  // Check the image is cuttable with the current parameters
+  let errorsList = []
+  if (!Number.isInteger(xCropSize)) errorsList.push('Incompatible number of horizontal extracts (width % numberOfExtracts != 0)')
+  if (!Number.isInteger(yCropSize)) errorsList.push('Incompatible number of vertical extracts (height % numberOfExtracts != 0)')
+  if (errorsList.length > 0) throw boom.badRequest('Invalid query parameter(s).', errorsList)
+
+  let extracts = []
+  // Cut images
+  // Vertical
+  for (let y = 0; y < yExtracts; y++) {
+    // Horizontal
+    for (let x = 0; x < xExtracts; x++) {
+      // How to cut the image
+      const config = {
+        left: x * xCropSize,
+        top: y * yCropSize,
+        width: xCropSize,
+        height: yCropSize
+      }
+
+      // Zone number of the extract `00020`
+      const fileNameCount = (extracts.length + 1).toString().padStart(5, '0')
+
+      // File name of the extract : `Scene2_zone00199_100.png`
+      const extractName = `${image.sceneName}_zone${fileNameCount}_${image.quality}.${image.ext}`
+
+      // Configured path to the image (Check defined convention)
+      const pathToImage = [image.sceneName, extractsDirName, `x${xExtracts}_y${yExtracts}`, `zone${fileNameCount}`, extractName]
+
+      // File system path to the extract
+      const extractPath = path.resolve(imagesPath, ...pathToImage)
+
+      // URL to the extract on the app
+      const extractLink = `${imageServedUrl}/${pathToImage.join('/')}`
+
+      const extractObj = {
+        link: extractLink,
+        path: extractPath,
+        fileName: extractName,
+        sceneName: image.sceneName
+      }
+
+      // Check the file already exist
+      let fileAlreadyExists = false
+      try {
+        await fs.access(extractPath, fsConstants.R_OK)
+        fileAlreadyExists = true
+      }
+      catch (err) {
+        // File does not exist already
+      }
+
+      // File already exist, just send its data
+      if (fileAlreadyExists) {
+        extracts.push(extractObj)
+        continue
+      }
+
+      // File does not already exist, create it
+      // Create the arborescence
+      try {
+        await fs.mkdir(path.resolve(imagesPath, ...pathToImage.slice(0, pathToImage.length - 1)), { recursive: true })
+      }
+      catch (err) {
+        // An error was caught, add it and go to next extract
+        errorsList.push(err.message)
+        continue
+      }
+
+      // Cut and save the extract
+      try {
+        await input.extract(config).toFile(extractPath)
+        extracts.push(extractObj)
+      }
+      catch (err) {
+        // Error while cutting image
+        errorsList.push(err)
+      }
+    }
+  }
+
+  // Extraction finished, check for errors
+  if (errorsList.length > 0) throw boom.internal('Error(s) while extracting from image.', errorsList)
+
+  return extracts
+}
+
+
+router.get('/', asyncMiddleware(async (req, res) => {
+  // Check the request contains all the required parameters
+  checkRequiredParameters(['sceneName', 'imageQuality', 'horizontalExtractCount', 'verticalExtractCount'], req.query)
+
+  const { sceneName, imageQuality, horizontalExtractCount, verticalExtractCount } = req.query
+
+  let errorList = []
+
+  // Check the scene name is valid
+  try {
+    checkSceneName(sceneName)
+  }
+  catch (err) {
+    errorList.push(err.message)
+  }
+
+  // Check `imageQuality` is an integer
+  const qualityInt = parseInt(imageQuality, 10)
+  if (isNaN(qualityInt)) errorList.push('The specified quality is not an integer.')
+
+  // Check `horizontalExtractCount` is an integer
+  const horizontalExtractCountInt = parseInt(horizontalExtractCount, 10)
+  if (isNaN(horizontalExtractCountInt)) errorList.push('The specified number of extract for the horizontal axis is not an integer.')
+
+  // Check `imageQuality` is an integer
+  const verticalExtractCountInt = parseInt(verticalExtractCount, 10)
+  if (isNaN(verticalExtractCountInt)) errorList.push('The specified number of extract for the vertical axis is not an integer.')
+
+
+  // Check there is no errors with parameters
+  if (errorList.length > 0)
+    throw boom.badRequest('Invalid query parameter(s).', errorList)
+
+  // Get the image path and link
+  const image = await getImage(sceneName, qualityInt)
+
+  // Cut the image
+  const extracts = await cutImage(image, horizontalExtractCountInt, verticalExtractCountInt)
+
+  // Send an array of links
+  res.json({ data: extracts.map(x => x.link) })
+}))
+
+export default router

+ 2 - 0
server/routes/index.js

@@ -4,11 +4,13 @@ import express from 'express'
 import listScenes from './listScenes'
 import listSceneQualities from './listSceneQualities'
 import getImage from './getImage'
+import getImageExtracts from './getImageExtracts'
 
 const router = express.Router()
 
 router.use('/listScenes', listScenes)
 router.use('/listSceneQualities', listSceneQualities)
 router.use('/getImage', getImage)
+router.use('/getImageExtracts', getImageExtracts)
 
 export default router

+ 3 - 1
webhook_deploy_gogs.js

@@ -60,11 +60,13 @@ if (!process.env.SERVE_CLIENT || !['true', 'false'].some(x => x === process.env.
   process.exit(1)
 }
 
-const env = {
+let env = {
   PORT: parseInt(process.env.PORT, 10),
   SERVE_CLIENT: process.env.SERVE_CLIENT,
   IMAGES_PATH: process.env.IMAGES_PATH
 }
+env = Object.assign(process.env, env)
+
 // Recap used environment variables
 Object.keys(env).forEach(x => console.log(`${x}=${env[x]}`))
 

+ 197 - 7
yarn.lock

@@ -1538,6 +1538,21 @@ binary-extensions@^1.0.0:
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
   integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
 
+bindings@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+  dependencies:
+    file-uri-to-path "1.0.0"
+
+bl@^1.0.0:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
+  integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==
+  dependencies:
+    readable-stream "^2.3.5"
+    safe-buffer "^5.1.1"
+
 bluebird@^3.1.1, bluebird@^3.5.1, bluebird@^3.5.3:
   version "3.5.4"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.4.tgz#d6cc661595de30d5b3af5fcedd3c0b3ef6ec5714"
@@ -1698,6 +1713,24 @@ browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.5.4:
     electron-to-chromium "^1.3.122"
     node-releases "^1.1.13"
 
+buffer-alloc-unsafe@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
+  integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
+
+buffer-alloc@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
+  integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
+  dependencies:
+    buffer-alloc-unsafe "^1.1.0"
+    buffer-fill "^1.0.0"
+
+buffer-fill@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
+  integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
+
 buffer-from@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -2096,7 +2129,7 @@ color@3.0.x:
     color-convert "^1.9.1"
     color-string "^1.5.2"
 
-color@^3.0.0:
+color@^3.0.0, color@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/color/-/color-3.1.0.tgz#d8e9fb096732875774c84bf922815df0308d0ffc"
   integrity sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg==
@@ -2657,6 +2690,13 @@ decode-uri-component@^0.2.0:
   resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
   integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
 
+decompress-response@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
+  integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
+  dependencies:
+    mimic-response "^1.0.0"
+
 deep-equal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -2766,7 +2806,7 @@ destroy@~1.0.4:
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
 
-detect-libc@^1.0.2:
+detect-libc@^1.0.2, detect-libc@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
   integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
@@ -3438,6 +3478,11 @@ expand-brackets@^2.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
+expand-template@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
+  integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
+
 expect-ct@0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/expect-ct/-/expect-ct-0.1.1.tgz#de84476a2dbcb85000d5903737e9bc8a5ba7b897"
@@ -3642,6 +3687,11 @@ file-loader@^3.0.1:
     loader-utils "^1.0.2"
     schema-utils "^1.0.0"
 
+file-uri-to-path@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
 filesize@^3.6.1:
   version "3.6.1"
   resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
@@ -3807,6 +3857,16 @@ from2@^2.1.0:
     inherits "^2.0.1"
     readable-stream "^2.0.0"
 
+fs-constants@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
+fs-copy-file-sync@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/fs-copy-file-sync/-/fs-copy-file-sync-1.1.1.tgz#11bf32c096c10d126e5f6b36d06eece776062918"
+  integrity sha512-2QY5eeqVv4m2PfyMiEuy9adxNP+ajf+8AR05cEi+OAzPcOj90hvFImeZhTmKLBgSd9EvG33jsD7ZRxsx9dThkQ==
+
 fs-extra@^7.0.1:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
@@ -3899,6 +3959,11 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
+github-from-package@0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
+  integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
+
 glob-parent@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@@ -5356,6 +5421,11 @@ mimic-fn@^2.0.0:
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
+mimic-response@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
+  integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
+
 mini-css-extract-plugin@^0.5.0:
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0"
@@ -5505,7 +5575,7 @@ mute-stream@0.0.7:
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
   integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
 
-nan@^2.9.2:
+nan@^2.13.1, nan@^2.9.2:
   version "2.13.2"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
   integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
@@ -5527,6 +5597,11 @@ nanomatch@^1.2.9:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
+napi-build-utils@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508"
+  integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==
+
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -5568,6 +5643,13 @@ nocache@2.0.0:
   resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.0.0.tgz#202b48021a0c4cbde2df80de15a17443c8b43980"
   integrity sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=
 
+node-abi@^2.7.0:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.7.1.tgz#a8997ae91176a5fbaa455b194976e32683cda643"
+  integrity sha512-OV8Bq1OrPh6z+Y4dqwo05HqrRL9YNF7QVMRfq1/pguwKLG+q9UB/Lk0x5qXjO23JjJg+/jqCHSTaG1P3tfKfuw==
+  dependencies:
+    semver "^5.4.1"
+
 node-forge@0.7.5:
   version "0.7.5"
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
@@ -5650,6 +5732,11 @@ nodemon@^1.18.10:
     undefsafe "^2.0.2"
     update-notifier "^2.5.0"
 
+noop-logger@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
+  integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=
+
 nopt@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
@@ -5722,7 +5809,7 @@ npm-run-path@^2.0.0:
   dependencies:
     path-key "^2.0.0"
 
-npmlog@^4.0.2:
+npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -5904,7 +5991,7 @@ os-browserify@^0.3.0:
   resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
   integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
 
-os-homedir@^1.0.0:
+os-homedir@^1.0.0, os-homedir@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
   integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
@@ -6551,6 +6638,28 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.5:
     source-map "^0.6.1"
     supports-color "^6.1.0"
 
+prebuild-install@^5.2.5:
+  version "5.2.5"
+  resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.2.5.tgz#c7485911fe98950b7f7cd15bb9daee11b875cc44"
+  integrity sha512-6uZgMVg7yDfqlP5CPurVhtq3hUKBFNufiar4J5hZrlHTo59DDBEtyxw01xCdFss9j0Zb9+qzFVf/s4niayba3w==
+  dependencies:
+    detect-libc "^1.0.3"
+    expand-template "^2.0.3"
+    github-from-package "0.0.0"
+    minimist "^1.2.0"
+    mkdirp "^0.5.1"
+    napi-build-utils "^1.0.1"
+    node-abi "^2.7.0"
+    noop-logger "^0.1.1"
+    npmlog "^4.0.1"
+    os-homedir "^1.0.1"
+    pump "^2.0.1"
+    rc "^1.2.7"
+    simple-get "^2.7.0"
+    tar-fs "^1.13.0"
+    tunnel-agent "^0.6.0"
+    which-pm-runs "^1.0.0"
+
 prelude-ls@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -6639,6 +6748,14 @@ public-encrypt@^4.0.0:
     randombytes "^2.0.1"
     safe-buffer "^5.1.2"
 
+pump@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
+  integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
 pump@^2.0.0, pump@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
@@ -6770,7 +6887,7 @@ read-pkg@^4.0.1:
     parse-json "^4.0.0"
     pify "^3.0.0"
 
-"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
   integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
@@ -7272,6 +7389,23 @@ sha.js@^2.4.0, sha.js@^2.4.8:
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
 
+sharp@^0.22.0:
+  version "0.22.0"
+  resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.22.0.tgz#cf4cfcb019941fd06ac24555d9f5bc84536d29be"
+  integrity sha512-yInpiWYvVbE0hJylso2Q2A7QaYFBxGdSlVVHGeUf1F9JsQNAUpmaqdnX54TImgKbSCy9mQpEAoGm1pcKCZhCsQ==
+  dependencies:
+    bindings "^1.5.0"
+    color "^3.1.0"
+    detect-libc "^1.0.3"
+    fs-copy-file-sync "^1.1.1"
+    nan "^2.13.1"
+    npmlog "^4.1.2"
+    prebuild-install "^5.2.5"
+    semver "^5.6.0"
+    simple-get "^3.0.3"
+    tar "^4.4.8"
+    tunnel-agent "^0.6.0"
+
 shebang-command@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -7299,6 +7433,29 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
   integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
 
+simple-concat@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
+  integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=
+
+simple-get@^2.7.0:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d"
+  integrity sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==
+  dependencies:
+    decompress-response "^3.3.0"
+    once "^1.3.1"
+    simple-concat "^1.0.0"
+
+simple-get@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.0.3.tgz#924528ac3f9d7718ce5e9ec1b1a69c0be4d62efa"
+  integrity sha512-Wvre/Jq5vgoz31Z9stYWPLn0PqRqmBDpFSdypAnHu5AvRVCYPRYGnvryNLiXu8GOBNDH82J2FRHUGMjjHUpXFw==
+  dependencies:
+    decompress-response "^3.3.0"
+    once "^1.3.1"
+    simple-concat "^1.0.0"
+
 simple-swizzle@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
@@ -7760,7 +7917,30 @@ tapable@^1.0.0, tapable@^1.1.0:
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e"
   integrity sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==
 
-tar@^4:
+tar-fs@^1.13.0:
+  version "1.16.3"
+  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509"
+  integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==
+  dependencies:
+    chownr "^1.0.1"
+    mkdirp "^0.5.1"
+    pump "^1.0.0"
+    tar-stream "^1.1.2"
+
+tar-stream@^1.1.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
+  integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
+  dependencies:
+    bl "^1.0.0"
+    buffer-alloc "^1.2.0"
+    end-of-stream "^1.0.0"
+    fs-constants "^1.0.0"
+    readable-stream "^2.3.0"
+    to-buffer "^1.1.1"
+    xtend "^4.0.0"
+
+tar@^4, tar@^4.4.8:
   version "4.4.8"
   resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
   integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==
@@ -7869,6 +8049,11 @@ to-arraybuffer@^1.0.0:
   resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
   integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
 
+to-buffer@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
+  integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==
+
 to-fast-properties@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -8487,6 +8672,11 @@ which-module@^2.0.0:
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
 
+which-pm-runs@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
+  integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
+
 which@^1.2.9:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"