FileHandler.js 5.2 KB

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