FileHandler.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. const { promisify } = require('util')
  2. const fs = require('fs')
  3. const PersistorManager = require('./PersistorManager')
  4. const LocalFileWriter = require('./LocalFileWriter')
  5. const logger = require('logger-sharelatex')
  6. const FileConverter = require('./FileConverter')
  7. const KeyBuilder = require('./KeyBuilder')
  8. const async = require('async')
  9. const ImageOptimiser = require('./ImageOptimiser')
  10. const {
  11. WriteError,
  12. ReadError,
  13. ConversionError,
  14. NotFoundError
  15. } = require('./Errors')
  16. module.exports = {
  17. insertFile,
  18. deleteFile,
  19. getFile,
  20. getFileSize,
  21. getDirectorySize,
  22. promises: {
  23. getFile: promisify(getFile),
  24. insertFile: promisify(insertFile),
  25. deleteFile: promisify(deleteFile),
  26. getFileSize: promisify(getFileSize),
  27. getDirectorySize: promisify(getDirectorySize)
  28. }
  29. }
  30. function insertFile(bucket, key, stream, callback) {
  31. const convertedKey = KeyBuilder.getConvertedFolderKey(key)
  32. PersistorManager.deleteDirectory(bucket, convertedKey, function(error) {
  33. if (error) {
  34. return callback(new WriteError('error inserting file').withCause(error))
  35. }
  36. PersistorManager.sendStream(bucket, key, stream, callback)
  37. })
  38. }
  39. function deleteFile(bucket, key, callback) {
  40. const convertedKey = KeyBuilder.getConvertedFolderKey(key)
  41. async.parallel(
  42. [
  43. done => PersistorManager.deleteFile(bucket, key, done),
  44. done => PersistorManager.deleteDirectory(bucket, convertedKey, done)
  45. ],
  46. callback
  47. )
  48. }
  49. function getFile(bucket, key, opts, callback) {
  50. // In this call, opts can contain credentials
  51. if (!opts) {
  52. opts = {}
  53. }
  54. logger.log({ bucket, key, opts: _scrubSecrets(opts) }, 'getting file')
  55. if (!opts.format && !opts.style) {
  56. _getStandardFile(bucket, key, opts, callback)
  57. } else {
  58. _getConvertedFile(bucket, key, opts, callback)
  59. }
  60. }
  61. function getFileSize(bucket, key, callback) {
  62. PersistorManager.getFileSize(bucket, key, callback)
  63. }
  64. function getDirectorySize(bucket, projectId, callback) {
  65. logger.log({ bucket, project_id: projectId }, 'getting project size')
  66. PersistorManager.directorySize(bucket, projectId, function(err, size) {
  67. if (err) {
  68. return callback(
  69. new ReadError('error getting project size').withCause(err)
  70. )
  71. }
  72. callback(null, size)
  73. })
  74. }
  75. function _getStandardFile(bucket, key, opts, callback) {
  76. PersistorManager.getFileStream(bucket, key, opts, function(err, fileStream) {
  77. if (err && !(err instanceof NotFoundError)) {
  78. logger.err(
  79. { bucket, key, opts: _scrubSecrets(opts) },
  80. 'error getting fileStream'
  81. )
  82. }
  83. callback(err, fileStream)
  84. })
  85. }
  86. function _getConvertedFile(bucket, key, opts, callback) {
  87. const convertedKey = KeyBuilder.addCachingToKey(key, opts)
  88. PersistorManager.checkIfFileExists(bucket, convertedKey, (err, exists) => {
  89. if (err) {
  90. return callback(err)
  91. }
  92. if (exists) {
  93. PersistorManager.getFileStream(bucket, convertedKey, opts, callback)
  94. } else {
  95. _getConvertedFileAndCache(bucket, key, convertedKey, opts, callback)
  96. }
  97. })
  98. }
  99. function _getConvertedFileAndCache(bucket, key, convertedKey, opts, callback) {
  100. let convertedFsPath
  101. async.series(
  102. [
  103. cb => {
  104. _convertFile(bucket, key, opts, function(err, fileSystemPath) {
  105. convertedFsPath = fileSystemPath
  106. cb(err)
  107. })
  108. },
  109. cb => ImageOptimiser.compressPng(convertedFsPath, cb),
  110. cb => PersistorManager.sendFile(bucket, convertedKey, convertedFsPath, cb)
  111. ],
  112. function(err) {
  113. if (err) {
  114. LocalFileWriter.deleteFile(convertedFsPath, function() {})
  115. return callback(
  116. new ConversionError('failed to convert file').withCause(err)
  117. )
  118. }
  119. // Send back the converted file from the local copy to avoid problems
  120. // with the file not being present in S3 yet. As described in the
  121. // documentation below, we have already made a 'HEAD' request in
  122. // checkIfFileExists so we only have "eventual consistency" if we try
  123. // to stream it from S3 here. This was a cause of many 403 errors.
  124. //
  125. // "Amazon S3 provides read-after-write consistency for PUTS of new
  126. // objects in your S3 bucket in all regions with one caveat. The
  127. // caveat is that if you make a HEAD or GET request to the key name
  128. // (to find if the object exists) before creating the object, Amazon
  129. // S3 provides eventual consistency for read-after-write.""
  130. // https://docs.aws.amazon.com/AmazonS3/latest/dev/Introduction.html#ConsistencyModel
  131. const readStream = fs.createReadStream(convertedFsPath)
  132. readStream.on('end', function() {
  133. LocalFileWriter.deleteFile(convertedFsPath, function() {})
  134. })
  135. callback(null, readStream)
  136. }
  137. )
  138. }
  139. function _convertFile(bucket, originalKey, opts, callback) {
  140. _writeFileToDisk(bucket, originalKey, opts, function(err, originalFsPath) {
  141. if (err) {
  142. return callback(
  143. new ConversionError('unable to write file to disk').withCause(err)
  144. )
  145. }
  146. const done = function(err, destPath) {
  147. if (err) {
  148. logger.err(
  149. { err, bucket, originalKey, opts: _scrubSecrets(opts) },
  150. 'error converting file'
  151. )
  152. return callback(
  153. new ConversionError('error converting file').withCause(err)
  154. )
  155. }
  156. LocalFileWriter.deleteFile(originalFsPath, function() {})
  157. callback(err, destPath)
  158. }
  159. logger.log({ opts }, 'converting file depending on opts')
  160. if (opts.format) {
  161. FileConverter.convert(originalFsPath, opts.format, done)
  162. } else if (opts.style === 'thumbnail') {
  163. FileConverter.thumbnail(originalFsPath, done)
  164. } else if (opts.style === 'preview') {
  165. FileConverter.preview(originalFsPath, done)
  166. } else {
  167. callback(
  168. new ConversionError(
  169. `should have specified opts to convert file with ${JSON.stringify(
  170. opts
  171. )}`
  172. )
  173. )
  174. }
  175. })
  176. }
  177. function _writeFileToDisk(bucket, key, opts, callback) {
  178. PersistorManager.getFileStream(bucket, key, opts, function(err, fileStream) {
  179. if (err) {
  180. return callback(
  181. new ReadError('unable to get read stream for file').withCause(err)
  182. )
  183. }
  184. LocalFileWriter.writeStream(fileStream, key, callback)
  185. })
  186. }
  187. function _scrubSecrets(opts) {
  188. const safe = Object.assign({}, opts)
  189. delete safe.credentials
  190. return safe
  191. }