FileHandler.js 6.3 KB

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