functions.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. 'use strict'
  2. import { promises as fs } from 'fs'
  3. import path from 'path'
  4. import boom from '@hapi/boom'
  5. import { logger, imagesPath, fileNameConvention, sceneFileNameBlackList } from '../config'
  6. /**
  7. * Call the error handler if a middleware function throw an error
  8. *
  9. * @param {Function} fn original middleware function of the route
  10. * @returns {Promise<Function>} the same middleware function of the route but error handled
  11. */
  12. export const asyncMiddleware = fn => (req, res, next) => {
  13. Promise.resolve(fn(req, res, next)).catch(err => {
  14. // Check whether the error is a boom error
  15. if (!err.isBoom) {
  16. // The error was not recognized, send a 500 HTTP error
  17. return next(boom.internal(err))
  18. }
  19. // It is a boom error, pass it to the error handler
  20. next(err)
  21. })
  22. }
  23. // Middleware to handle middleware errors
  24. export const errorHandler = (err, req, res, next) => {
  25. const { output: { payload } } = err
  26. // Pass the error to the logging handler
  27. let errorLogged = new Error(`Error ${payload.statusCode} - ${payload.error} - Message :\n${payload.message}`)
  28. errorLogged.stack = err.stack
  29. logger.error(formatError(errorLogged, err.data))
  30. // Send the error to the client
  31. res.status(payload.statusCode).json({
  32. message: err.message || payload.message,
  33. data: err.data || undefined
  34. })
  35. next()
  36. }
  37. /**
  38. * Check the request contains all the required parameters
  39. *
  40. * @param {string[]} requiredParameters list of all required parameters
  41. * @param {object} parameters parameters provided in the request (req.query)
  42. * @returns {void}
  43. * @throws missing parameters
  44. */
  45. export const checkRequiredParameters = (requiredParameters, parameters) => {
  46. if (!requiredParameters.every(aRequiredParameter => Object.keys(parameters).includes(aRequiredParameter)))
  47. throw boom.badRequest(`Missing parameter(s). Required parameters : ${requiredParameters.join(', ')}.`)
  48. }
  49. /**
  50. * Check a scene name is valid
  51. * (Not trying to go back in the file system tree by using `/../`)
  52. *
  53. * @param {string} sceneName the scene name to check
  54. * @returns {void}
  55. * @throws invalid scene name
  56. */
  57. export const checkSceneName = sceneName => {
  58. if (!sceneName || sceneName === '.' || !/^(?!.*\.\.).*$/.test(sceneName))
  59. throw boom.badRequest(`The requested scene name "${sceneName}" is not valid.`)
  60. }
  61. /**
  62. * Check a file name is valid with configured convention
  63. *
  64. * @param {string} fileName the file name to check
  65. * @returns {void}
  66. * @throws file name does not match convention
  67. */
  68. export const checkFileName = fileName => {
  69. if (!fileNameConvention.test(fileName))
  70. throw new Error(`The file name does not match convention (scene_000150.ext - ${fileNameConvention.toString()}) : "${fileName}".`)
  71. }
  72. /**
  73. * Get all files in a scene
  74. *
  75. * @param {string} sceneName the scene name
  76. * @returns {Promise<string[]>} the list of all files in the scene
  77. * @throws scene directory is not accessible
  78. */
  79. export const getSceneFiles = sceneName => {
  80. // Check the scene name is valid
  81. checkSceneName(sceneName)
  82. // Path to the scene directory
  83. const scenePath = path.resolve(imagesPath, sceneName)
  84. return fs.readdir(scenePath).catch(() => {
  85. throw boom.internal(`Can't access the "${sceneName}" scene directory. Check it exists and you have read permission on it.`)
  86. })
  87. }
  88. /** Image data type definition (do no remove)
  89. * @typedef {object} ImageData
  90. * @property {string} fileName file name of image
  91. * @property {string} sceneName scene name of image
  92. * @property {string} prefix prefix of image
  93. * @property {number} quality quality of image
  94. * @property {string} ext extension of image
  95. */
  96. /**
  97. * Get image data from every files in a scene (exclude blacklisted ones)
  98. * @typedef {string} filename path to the image
  99. * @param {string} sceneName the scene name
  100. * @returns {Promise<ImageData[]>} the data for all images in a scene
  101. * @throws some file names could not be parsed
  102. */
  103. export const getSceneFilesData = async sceneName => {
  104. // Get scene files
  105. const files = await getSceneFiles(sceneName)
  106. // A list of all fails parsing scene file names
  107. let failList = []
  108. // Parse file name to get qualities
  109. const data = files.reduce((acc, image) => {
  110. // Go to next file if its name contains a blacklisted word
  111. if (!sceneFileNameBlackList.every(x => image !== x))
  112. return acc
  113. // Check file name is valid
  114. try {
  115. checkFileName(image)
  116. }
  117. catch (err) {
  118. failList.push(err.message)
  119. return acc
  120. }
  121. // Parse file name
  122. try {
  123. const regexRes = fileNameConvention.exec(image)
  124. // Check valid file name
  125. if (regexRes.length !== 4) return acc
  126. const fileData = {
  127. prefix: regexRes[1],
  128. quality: parseInt(regexRes[2], 10),
  129. ext: regexRes[3],
  130. fileName: regexRes[0],
  131. sceneName
  132. }
  133. // Check valid quality
  134. if (isNaN(fileData.quality)) return acc
  135. // Data is valid, set it
  136. acc.push(fileData)
  137. }
  138. catch (err) {
  139. failList.push(`Failed to parse file name : "${image}".`)
  140. }
  141. return acc
  142. }, [])
  143. // Check if the parse fail list is empty
  144. if (failList.length > 0)
  145. throw boom.internal(`Failed to parse file names in the "${sceneName}"'s scene directory.`, failList)
  146. return data
  147. }
  148. /**
  149. * Format a string or object to a log object
  150. *
  151. * @param {object|string} data any message or object
  152. * @param {('info'|'message'|'error'|any|undefined)} event the type of event
  153. * @returns {string} the log object stringified
  154. */
  155. export const formatLog = (data, event = undefined) => (({
  156. event,
  157. log: data,
  158. date: new Date()
  159. }))
  160. /**
  161. * Format an error object
  162. *
  163. * @param {Error} errObj an Error object
  164. * @param {any} data any data to pass to the formatter
  165. * @returns {string} formatted log object stringified
  166. */
  167. export const formatError = (errObj, data = undefined) => formatLog({ error: errObj.message, stack: errObj.stack, data })