FileHandler.js 4.8 KB

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