Parcourir la source

Major refactor. Better doc and performance. getImage working fine

rigwild il y a 5 ans
Parent
commit
c7a5936fcc
6 fichiers modifiés avec 185 ajouts et 80 suppressions
  1. 10 1
      config.js
  2. 116 2
      server/functions.js
  3. 7 7
      server/index.js
  4. 33 7
      server/routes/getImage.js
  5. 5 55
      server/routes/listSceneQualities.js
  6. 14 8
      server/routes/listScenes.js

+ 10 - 1
config.js

@@ -8,6 +8,9 @@ export const PRODUCTION_MODE = process.env.NODE_ENV === 'production'
 // The url prefix for the API
 export const apiPrefix = '/api'
 
+// The url prefix from where the images are served
+export const imageServedUrl = apiPrefix + '/images'
+
 // The port used by the server
 export const serverPort = parseInt(process.env.PORT, 10) || 5000
 
@@ -15,7 +18,13 @@ export const serverPort = parseInt(process.env.PORT, 10) || 5000
 export const imagesPath = path.resolve(__dirname, 'images')
 
 // Should the server serve client files from the `/dist` directory
-export const serveClient = process.env.SERVE_CLIENT === 'true'
+export const serveClient = process.env.SERVE_CLIENT === 'true' || true
+
+// File name convention for images
+export const fileNameConvention = /^(.*)?_([0-9]{2,})\.(.*)$/
+
+// Files to ignore in scenes
+export const sceneFileNameBlackList = ['config', 'seuilExpe']
 
 // Logger configuration
 export const logger = winston.createLogger({

+ 116 - 2
server/functions.js

@@ -1,13 +1,15 @@
 'use strict'
 
+import { promises as fs } from 'fs'
+import path from 'path'
 import boom from 'boom'
-import { logger } from '../config'
+import { logger, imagesPath, fileNameConvention, sceneFileNameBlackList } from '../config'
 
 /**
  * Call the error handler if a middleware function throw an error
  *
  * @param {Function} fn original middleware function of the route
- * @returns {Function} the same middleware function of the route but error handled
+ * @returns {Promise<Function>} the same middleware function of the route but error handled
  */
 export const asyncMiddleware = fn => (req, res, next) => {
   Promise.resolve(fn(req, res, next)).catch(err => {
@@ -29,6 +31,7 @@ export const errorHandler = (err, req, res, next) => {
   let logMsg = `Error ${payload.statusCode} - ${payload.error}` +
     ` - Message :\n${payload.message}`
   if (err.data) logMsg += `\nData : \n${JSON.stringify(err.data, null, 2) || err.data}`
+  logMsg += `\nStack trace : \n${err.stack}`
 
   logger.error(logMsg)
 
@@ -48,8 +51,119 @@ export const errorHandler = (err, req, res, next) => {
  * @param {string[]} requiredParameters list of all required parameters
  * @param {object} parameters parameters provided in the request (req.query)
  * @returns {void}
+ * @throws missing parameters
  */
 export const checkRequiredParameters = (requiredParameters, parameters) => {
   if (!requiredParameters.every(aRequiredParameter => Object.keys(parameters).includes(aRequiredParameter)))
     throw boom.badRequest(`Missing parameter(s). Required parameters : ${requiredParameters.join(', ')}.`)
 }
+
+/**
+ * Check a scene name is valid
+ * (Not trying to go back in the file system tree by using `/../`)
+ *
+ * @param {string} sceneName the scene name to check
+ * @returns {void}
+ * @throws invalid scene name
+ */
+export const checkSceneName = sceneName => {
+  if (!/^(?!.*\.\.).*$/.test(sceneName))
+    throw boom.conflict(`The requested scene name "${sceneName}" is not valid.`)
+}
+
+/**
+ * Check a file name is valid with configure convention
+ *
+ * @param {string} fileName the file name to check
+ * @returns {void}
+ * @throws file name does not match convention
+ */
+export const checkFileName = fileName => {
+  if (!fileNameConvention.test(fileName))
+    throw new Error(`The file name does not match convention (scene_000150.ext - ${fileNameConvention.toString()}) : "${fileName}"`)
+}
+
+/**
+ * Get all files in a scene
+ *
+ * @param {string} sceneName the scene name
+ * @returns {string[]} the list of all files in the scene
+ * @throws scene directory is not accessible
+ */
+export const getSceneFiles = sceneName => {
+  // Check the scene name is valid
+  checkSceneName(sceneName)
+
+  // Path to the scene directory
+  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.`)
+  })
+}
+
+/** Image data type definition (do no remove)
+ * @typedef {object} ImageData
+ * @property {string} prefix prefix of image
+ * @property {number} quality quality of image
+ * @property {string} ext extension of image
+ */
+/**
+ * Get image data from every files in a scene (exclude blacklisted ones)
+ * @typedef {string} filename path to the image
+ * @param {string} sceneName the scene name
+ * @returns {Promise<Map<filename, ImageData>>} the data for all images in a scene (Map key = file name)
+ * @throws some file names could not be parsed
+ */
+export const getSceneFilesData = async sceneName => {
+  // Get scene files
+  const files = await getSceneFiles(sceneName)
+
+  // A list of all fails parsing scene file names
+  let failList = []
+
+  // Parse file name to get qualities
+  const data = files.reduce((acc, image) => {
+    // Go to next file if its name contains a blacklisted word
+    if (!sceneFileNameBlackList.every(x => image !== x))
+      return acc
+
+    // Check file name is valid
+    try {
+      checkFileName(image)
+    }
+    catch (err) {
+      failList.push(err.message)
+      return acc
+    }
+
+    // Parse file name
+    try {
+      const regexRes = fileNameConvention.exec(image)
+      // Check valid file name
+      if (regexRes.length !== 4) return acc
+
+      const fileData = {
+        prefix: regexRes[1],
+        quality: parseInt(regexRes[2], 10),
+        ext: regexRes[3]
+      }
+
+      // Check valid quality
+      if (isNaN(fileData.quality)) return acc
+
+      // Data is valid, set it
+      acc.set(regexRes[0], fileData)
+    }
+    catch (err) {
+      failList.push(`Failed to parse file name : ${image}`)
+    }
+    return acc
+  }, new Map())
+
+  // 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)
+
+  return data
+}

+ 7 - 7
server/index.js

@@ -8,7 +8,7 @@ import helmet from 'helmet'
 import cors from 'cors'
 import routes from './routes'
 import { errorHandler } from './functions'
-import { apiPrefix, serverPort, serveClient, imagesPath, logger } from '../config'
+import { apiPrefix, imageServedUrl, serverPort, serveClient, imagesPath, logger } from '../config'
 const morgan = require('morgan')
 
 const app = express()
@@ -22,6 +22,11 @@ app.use(compression())
 // Enhance the app security by setting some HTTP headers
 app.use(helmet())
 
+// Serve images. "serve-static" is used because it caches images ("express.static" doesn't)
+app.use(imageServedUrl, serveStatic(imagesPath))
+
+// Load all the API routes in the server
+app.use(apiPrefix, routes)
 
 if (serveClient) {
   // Serve client files (Client is local)
@@ -31,14 +36,9 @@ else {
   // Don't serve client files (Client is remote)
   // Turn "Cross-origin resource sharing" on to allow the remote client to connect to the API
   app.use(cors())
+  app.get('*', (req, res) => res.status(404).send('Client is not served.'))
 }
 
-// Serve images. "serve-static" is used because it caches images ("express.static" doesn't)
-app.use(apiPrefix + '/images', serveStatic(imagesPath))
-
-// Load all the API routes in the server
-app.use(apiPrefix, routes)
-
 // Error handler (Middleware called when throwing in another middleware)
 app.use(errorHandler)
 

+ 33 - 7
server/routes/getImage.js

@@ -1,12 +1,10 @@
 'use strict'
 
 import express from 'express'
-import { promises } from 'fs'
+import boom from 'boom'
 
-import { imagesPath } from '../../config'
-import { asyncMiddleware, checkRequiredParameters } from '../functions'
-
-const fs = promises
+import { imageServedUrl } from '../../config'
+import { asyncMiddleware, checkSceneName, checkRequiredParameters, getSceneFilesData } from '../functions'
 
 const router = express.Router()
 
@@ -14,8 +12,36 @@ router.get('/', asyncMiddleware(async (req, res) => {
   // Check the request contains all the required parameters
   checkRequiredParameters(['sceneName', 'imageQuality'], req.query)
 
-  const dirContent = await fs.readdir(imagesPath)
-  res.json({ msg: 'Not ready yet' })
+  const { sceneName, imageQuality } = req.query
+
+  let errorList = []
+
+  // Check the scene name is valid
+  try {
+    checkSceneName(sceneName)
+  }
+  catch (err) {
+    errorList.push(err)
+  }
+
+  // Check `imageQuality` is an integer
+  const qualityInt = parseInt(imageQuality, 10)
+  if (isNaN(qualityInt)) errorList.push('The specified quality is not an integer.')
+
+  // Check there is no errors with parameters
+  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}).`)
 }))
 
 export default router

+ 5 - 55
server/routes/listSceneQualities.js

@@ -1,11 +1,7 @@
 'use strict'
 
 import express from 'express'
-import { promises as fs } from 'fs'
-import path from 'path'
-import boom from 'boom'
-import { imagesPath } from '../../config'
-import { asyncMiddleware, checkRequiredParameters } from '../functions'
+import { asyncMiddleware, checkRequiredParameters, getSceneFilesData } from '../functions'
 
 const router = express.Router()
 
@@ -14,56 +10,10 @@ router.get('/', asyncMiddleware(async (req, res) => {
   // Check the request contains all the required parameters
   checkRequiredParameters(['sceneName'], req.query)
 
-  const sceneName = req.query.sceneName
-
-  // Check the scene name is valid (Not trying to go back in the file system tree by using `/../`)
-  if (!/^(?!.*\.\.).*$/.test(sceneName))
-    throw boom.conflict(`The requested scene name "${sceneName}" is not valid.`)
-
-  // Path to the scene directory
-  const scenePath = path.resolve(imagesPath, sceneName)
-
-  // Get the list of all images in the selected scene
-  const images = await fs.readdir(scenePath)
-    .catch(() => {
-      // The images directory does not exist or is not accessible
-      throw boom.badRequest(`Can't access the "${scenePath}" directory. Check it exists and you have read permission on it.`)
-    })
-
-  // List of blacklisted words from image names
-  const blackList = ['config', 'seuilExpe']
-
-  // A list of all fails parsing file names
-  let failList = []
-  // Parse file name to get qualities
-  const qualities = images.reduce((acc, image) => {
-    // Go to next file if its name contains a blacklisted word
-    if (!blackList.every(x => image !== x))
-      return acc
-
-    // Check if file name contains "_"
-    if (!/^.*?_[0-9]{5}\..*$/.test(image)) {
-      failList.push(`The file name does not match convention (scene_00150.ext) : "${image}"`)
-      return acc
-    }
-    try {
-      const sp = image.split('_')
-      const end = sp[sp.length - 1] // 000650.png
-      const qualityString = end.replace(/\..*/g, '') // 000650
-      const qualityInteger = parseInt(qualityString, 10) // 650
-      acc.push(qualityInteger)
-    }
-    catch (err) {
-      failList.push(`Failed to parse file name : ${image}`)
-    }
-    return acc
-  }, [])
-
-  // 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)
-
-  res.json(qualities)
+  const { sceneName } = req.query
+  const sceneData = await getSceneFilesData(sceneName)
+  const data = Array.from(sceneData.values()).map(x => x.quality)
+  res.json(data)
 }))
 
 export default router

+ 14 - 8
server/routes/listScenes.js

@@ -3,21 +3,27 @@
 import express from 'express'
 import { promises as fs } from 'fs'
 import boom from 'boom'
-import { imagesPath } from '../../config'
 import { asyncMiddleware } from '../functions'
+import { imagesPath } from '../../config'
 
 const router = express.Router()
 
-// Route which returns a list of all available scenes in the `imagesPath` directory
-router.get('/', asyncMiddleware(async (req, res) => {
+/**
+ * Get the list of all files in the images directory
+ *
+ * @returns {string[]} the list of files
+ * @throws the directory does not exist or is not accessible
+ */
+export const getSceneList = () => {
   try {
-    // Return the list of all files in the images directory
-    res.json(await fs.readdir(imagesPath))
+    return fs.readdir(imagesPath)
   }
   catch (err) {
-    // The images directory does not exist or is not accessible
-    throw boom.badRequest(`Can't access the "${imagesPath}" directory. Check it exists and you have read permission on it.`)
+    throw boom.conflict(`Can't access the "${imagesPath}" directory. Check it exists and you have read permission on it.`)
   }
-}))
+}
+
+// Route which returns a list of all available scenes in the `imagesPath` directory
+router.get('/', asyncMiddleware(async (req, res) => res.json(await getSceneList())))
 
 export default router