FileHandler.js 5.7 KB

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