FileHandler.js 5.7 KB

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