functions.js 5.9 KB

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