FileWriter.mjs 5.3 KB

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