FileHandler.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. const { callbackify } = require('util')
  2. const fs = require('fs')
  3. const PersistorManager = require('./PersistorManager')
  4. const LocalFileWriter = require('./LocalFileWriter')
  5. const FileConverter = require('./FileConverter')
  6. const KeyBuilder = require('./KeyBuilder')
  7. const ImageOptimiser = require('./ImageOptimiser')
  8. const { ConversionError, InvalidParametersError } = require('./Errors')
  9. module.exports = {
  10. insertFile: callbackify(insertFile),
  11. deleteFile: callbackify(deleteFile),
  12. deleteProject: callbackify(deleteProject),
  13. getFile: callbackify(getFile),
  14. getFileSize: callbackify(getFileSize),
  15. getDirectorySize: callbackify(getDirectorySize),
  16. promises: {
  17. getFile,
  18. insertFile,
  19. deleteFile,
  20. deleteProject,
  21. getFileSize,
  22. getDirectorySize
  23. }
  24. }
  25. async function insertFile(bucket, key, stream) {
  26. const convertedKey = KeyBuilder.getConvertedFolderKey(key)
  27. if (!convertedKey.match(/^[0-9a-f]{24}\/[0-9a-f]{24}/i)) {
  28. throw new InvalidParametersError({
  29. message: 'key does not match validation regex',
  30. info: { bucket, key, convertedKey }
  31. })
  32. }
  33. await PersistorManager.promises.deleteDirectory(bucket, convertedKey)
  34. await PersistorManager.promises.sendStream(bucket, key, stream)
  35. }
  36. async function deleteFile(bucket, key) {
  37. const convertedKey = KeyBuilder.getConvertedFolderKey(key)
  38. if (!convertedKey.match(/^[0-9a-f]{24}\/[0-9a-f]{24}/i)) {
  39. throw new InvalidParametersError({
  40. message: 'key does not match validation regex',
  41. info: { bucket, key, convertedKey }
  42. })
  43. }
  44. await Promise.all([
  45. PersistorManager.promises.deleteFile(bucket, key),
  46. PersistorManager.promises.deleteDirectory(bucket, convertedKey)
  47. ])
  48. }
  49. async function deleteProject(bucket, key) {
  50. if (!key.match(/^[0-9a-f]{24}\//i)) {
  51. throw new InvalidParametersError({
  52. message: 'key does not match validation regex',
  53. info: { bucket, key }
  54. })
  55. }
  56. await PersistorManager.promises.deleteDirectory(bucket, key)
  57. }
  58. async function getFile(bucket, key, opts) {
  59. opts = opts || {}
  60. if (!opts.format && !opts.style) {
  61. return PersistorManager.promises.getFileStream(bucket, key, opts)
  62. } else {
  63. return _getConvertedFile(bucket, key, opts)
  64. }
  65. }
  66. async function getFileSize(bucket, key) {
  67. return PersistorManager.promises.getFileSize(bucket, key)
  68. }
  69. async function getDirectorySize(bucket, projectId) {
  70. return PersistorManager.promises.directorySize(bucket, projectId)
  71. }
  72. async function _getConvertedFile(bucket, key, opts) {
  73. const convertedKey = KeyBuilder.addCachingToKey(key, opts)
  74. const exists = await PersistorManager.promises.checkIfFileExists(
  75. bucket,
  76. convertedKey
  77. )
  78. if (exists) {
  79. return PersistorManager.promises.getFileStream(bucket, convertedKey, opts)
  80. } else {
  81. return _getConvertedFileAndCache(bucket, key, convertedKey, opts)
  82. }
  83. }
  84. async function _getConvertedFileAndCache(bucket, key, convertedKey, opts) {
  85. let convertedFsPath
  86. try {
  87. convertedFsPath = await _convertFile(bucket, key, opts)
  88. await ImageOptimiser.promises.compressPng(convertedFsPath)
  89. await PersistorManager.promises.sendFile(
  90. bucket,
  91. convertedKey,
  92. convertedFsPath
  93. )
  94. } catch (err) {
  95. LocalFileWriter.deleteFile(convertedFsPath, () => {})
  96. throw new ConversionError({
  97. message: 'failed to convert file',
  98. info: { opts, bucket, key, convertedKey }
  99. }).withCause(err)
  100. }
  101. // Send back the converted file from the local copy to avoid problems
  102. // with the file not being present in S3 yet. As described in the
  103. // documentation below, we have already made a 'HEAD' request in
  104. // checkIfFileExists so we only have "eventual consistency" if we try
  105. // to stream it from S3 here. This was a cause of many 403 errors.
  106. //
  107. // "Amazon S3 provides read-after-write consistency for PUTS of new
  108. // objects in your S3 bucket in all regions with one caveat. The
  109. // caveat is that if you make a HEAD or GET request to the key name
  110. // (to find if the object exists) before creating the object, Amazon
  111. // S3 provides eventual consistency for read-after-write.""
  112. // https://docs.aws.amazon.com/AmazonS3/latest/dev/Introduction.html#ConsistencyModel
  113. const readStream = fs.createReadStream(convertedFsPath)
  114. readStream.on('end', function() {
  115. LocalFileWriter.deleteFile(convertedFsPath, function() {})
  116. })
  117. return readStream
  118. }
  119. async function _convertFile(bucket, originalKey, opts) {
  120. let originalFsPath
  121. try {
  122. originalFsPath = await _writeFileToDisk(bucket, originalKey, opts)
  123. } catch (err) {
  124. throw new ConversionError({
  125. message: 'unable to write file to disk',
  126. info: { bucket, originalKey, opts }
  127. }).withCause(err)
  128. }
  129. let promise
  130. if (opts.format) {
  131. promise = FileConverter.promises.convert(originalFsPath, opts.format)
  132. } else if (opts.style === 'thumbnail') {
  133. promise = FileConverter.promises.thumbnail(originalFsPath)
  134. } else if (opts.style === 'preview') {
  135. promise = FileConverter.promises.preview(originalFsPath)
  136. } else {
  137. throw new ConversionError({
  138. message: 'invalid file conversion options',
  139. info: {
  140. bucket,
  141. originalKey,
  142. opts
  143. }
  144. })
  145. }
  146. let destPath
  147. try {
  148. destPath = await promise
  149. } catch (err) {
  150. throw new ConversionError({
  151. message: 'error converting file',
  152. info: { bucket, originalKey, opts }
  153. }).withCause(err)
  154. }
  155. LocalFileWriter.deleteFile(originalFsPath, function() {})
  156. return destPath
  157. }
  158. async function _writeFileToDisk(bucket, key, opts) {
  159. const fileStream = await PersistorManager.promises.getFileStream(
  160. bucket,
  161. key,
  162. opts
  163. )
  164. return LocalFileWriter.promises.writeStream(fileStream, key)
  165. }