functions.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. 'use strict'
  2. import { promises as fs } from 'fs'
  3. import path from 'path'
  4. import boom from '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 === '' || !/^(?!.*\.\.).*$/.test(sceneName))
  61. throw boom.conflict(`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} 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<Map<filename, ImageData>>} the data for all images in a scene (Map key = file name)
  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. }
  131. // Check valid quality
  132. if (isNaN(fileData.quality)) return acc
  133. // Data is valid, set it
  134. acc.set(regexRes[0], fileData)
  135. }
  136. catch (err) {
  137. failList.push(`Failed to parse file name : "${image}".`)
  138. }
  139. return acc
  140. }, new Map())
  141. // Check if the parse fail list is empty
  142. if (failList.length > 0)
  143. throw boom.internal(`Failed to parse file names in the "${sceneName}"'s scene directory.`, failList)
  144. return data
  145. }
  146. /**
  147. * Format a string or object to a log object
  148. *
  149. * @param {object|string} data any message or object
  150. * @param {('info'|'message'|'error'|any|undefined)} event the type of event
  151. * @returns {string} the log object stringified
  152. */
  153. export const formatLog = (data, event = undefined) => (JSON.stringify({
  154. event,
  155. log: data,
  156. date: new Date()
  157. }))
  158. /**
  159. * Format an error object
  160. *
  161. * @param {Error} errObj an Error object
  162. * @returns {string} formatted log object stringified
  163. */
  164. export const formatError = errObj => formatLog({ error: errObj.message, stack: errObj.stack })