functions.js 5.8 KB

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