FileWriter.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /* eslint-disable
  2. n/handle-callback-err,
  3. max-len,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * DS207: Consider shorter variations of null checks
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. const fs = require('fs')
  14. const OError = require('@overleaf/o-error')
  15. const logger = require('@overleaf/logger')
  16. const uuid = require('uuid')
  17. const _ = require('underscore')
  18. const Settings = require('@overleaf/settings')
  19. const request = require('request')
  20. const { Transform, pipeline } = require('stream')
  21. const { FileTooLargeError } = require('../Features/Errors/Errors')
  22. const { promisifyAll } = require('../util/promises')
  23. class SizeLimitedStream extends Transform {
  24. constructor(options) {
  25. options.autoDestroy = true
  26. super(options)
  27. this.bytes = 0
  28. this.maxSizeBytes = options.maxSizeBytes
  29. this.drain = false
  30. this.on('error', () => {
  31. this.drain = true
  32. this.resume()
  33. })
  34. }
  35. _transform(chunk, encoding, done) {
  36. if (this.drain) {
  37. // mechanism to drain the source stream on error, to avoid leaks
  38. // we consume the rest of the incoming stream and don't push it anywhere
  39. return done()
  40. }
  41. this.bytes += chunk.length
  42. if (this.maxSizeBytes && this.bytes > this.maxSizeBytes) {
  43. return done(
  44. new FileTooLargeError({
  45. message: 'stream size limit reached',
  46. info: { size: this.bytes },
  47. })
  48. )
  49. }
  50. this.push(chunk)
  51. done()
  52. }
  53. }
  54. const FileWriter = {
  55. ensureDumpFolderExists(callback) {
  56. if (callback == null) {
  57. callback = function () {}
  58. }
  59. return fs.mkdir(Settings.path.dumpFolder, function (error) {
  60. if (error != null && error.code !== 'EEXIST') {
  61. // Ignore error about already existing
  62. return callback(error)
  63. }
  64. return callback(null)
  65. })
  66. },
  67. writeLinesToDisk(identifier, lines, callback) {
  68. if (callback == null) {
  69. callback = function () {}
  70. }
  71. return FileWriter.writeContentToDisk(identifier, lines.join('\n'), callback)
  72. },
  73. writeContentToDisk(identifier, content, callback) {
  74. if (callback == null) {
  75. callback = function () {}
  76. }
  77. callback = _.once(callback)
  78. const fsPath = `${Settings.path.dumpFolder}/${identifier}_${uuid.v4()}`
  79. return FileWriter.ensureDumpFolderExists(function (error) {
  80. if (error != null) {
  81. return callback(error)
  82. }
  83. return fs.writeFile(fsPath, content, function (error) {
  84. if (error != null) {
  85. return callback(error)
  86. }
  87. return callback(null, fsPath)
  88. })
  89. })
  90. },
  91. writeStreamToDisk(identifier, stream, options, callback) {
  92. if (typeof options === 'function') {
  93. callback = options
  94. options = {}
  95. }
  96. if (callback == null) {
  97. callback = function () {}
  98. }
  99. options = options || {}
  100. const fsPath = `${Settings.path.dumpFolder}/${identifier}_${uuid.v4()}`
  101. stream.pause()
  102. FileWriter.ensureDumpFolderExists(function (error) {
  103. const writeStream = fs.createWriteStream(fsPath)
  104. if (error != null) {
  105. return callback(error)
  106. }
  107. stream.resume()
  108. const passThrough = new SizeLimitedStream({
  109. maxSizeBytes: options.maxSizeBytes,
  110. })
  111. // if writing fails, we want to consume the bytes from the source, to avoid leaks
  112. for (const evt of ['error', 'close']) {
  113. writeStream.on(evt, function () {
  114. passThrough.unpipe(writeStream)
  115. passThrough.resume()
  116. })
  117. }
  118. pipeline(stream, passThrough, writeStream, function (err) {
  119. if (
  120. options.maxSizeBytes &&
  121. passThrough.bytes >= options.maxSizeBytes &&
  122. !(err instanceof FileTooLargeError)
  123. ) {
  124. err = new FileTooLargeError({
  125. message: 'stream size limit reached',
  126. info: { size: passThrough.bytes },
  127. }).withCause(err || {})
  128. }
  129. if (err) {
  130. OError.tag(
  131. err,
  132. '[writeStreamToDisk] something went wrong writing the stream to disk',
  133. {
  134. identifier,
  135. fsPath,
  136. }
  137. )
  138. return callback(err)
  139. }
  140. logger.debug(
  141. { identifier, fsPath },
  142. '[writeStreamToDisk] write stream finished'
  143. )
  144. callback(null, fsPath)
  145. })
  146. })
  147. },
  148. writeUrlToDisk(identifier, url, options, callback) {
  149. if (typeof options === 'function') {
  150. callback = options
  151. options = {}
  152. }
  153. if (callback == null) {
  154. callback = function () {}
  155. }
  156. options = options || {}
  157. callback = _.once(callback)
  158. const stream = request.get(url)
  159. stream.on('error', function (err) {
  160. logger.warn(
  161. { err, identifier, url },
  162. '[writeUrlToDisk] something went wrong with writing to disk'
  163. )
  164. callback(err)
  165. })
  166. stream.on('response', function (response) {
  167. if (response.statusCode >= 200 && response.statusCode < 300) {
  168. FileWriter.writeStreamToDisk(identifier, stream, options, callback)
  169. } else {
  170. const err = new Error(`bad response from url: ${response.statusCode}`)
  171. logger.warn({ err, identifier, url }, `[writeUrlToDisk] ${err.message}`)
  172. return callback(err)
  173. }
  174. })
  175. },
  176. }
  177. module.exports = FileWriter
  178. module.exports.promises = promisifyAll(FileWriter)
  179. module.exports.SizeLimitedStream = SizeLimitedStream