mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-24 21:12:38 -04:00
8b723c5363
Replace `uuid`/`node-uuid` with `node:crypto` GitOrigin-RevId: 1be4f09c4681edac7e8bc58af3b5726d0d5ee322
202 lines
5.5 KiB
JavaScript
202 lines
5.5 KiB
JavaScript
/* eslint-disable
|
|
n/handle-callback-err,
|
|
max-len,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
const fs = require('fs')
|
|
const OError = require('@overleaf/o-error')
|
|
const logger = require('@overleaf/logger')
|
|
const crypto = require('crypto')
|
|
const _ = require('underscore')
|
|
const Settings = require('@overleaf/settings')
|
|
const request = require('request')
|
|
const { Transform, pipeline } = require('stream')
|
|
const { FileTooLargeError } = require('../Features/Errors/Errors')
|
|
const { promisifyAll } = require('../util/promises')
|
|
|
|
class SizeLimitedStream extends Transform {
|
|
constructor(options) {
|
|
options.autoDestroy = true
|
|
super(options)
|
|
|
|
this.bytes = 0
|
|
this.maxSizeBytes = options.maxSizeBytes
|
|
this.drain = false
|
|
this.on('error', () => {
|
|
this.drain = true
|
|
this.resume()
|
|
})
|
|
}
|
|
|
|
_transform(chunk, encoding, done) {
|
|
if (this.drain) {
|
|
// mechanism to drain the source stream on error, to avoid leaks
|
|
// we consume the rest of the incoming stream and don't push it anywhere
|
|
return done()
|
|
}
|
|
|
|
this.bytes += chunk.length
|
|
if (this.maxSizeBytes && this.bytes > this.maxSizeBytes) {
|
|
return done(
|
|
new FileTooLargeError({
|
|
message: 'stream size limit reached',
|
|
info: { size: this.bytes },
|
|
})
|
|
)
|
|
}
|
|
this.push(chunk)
|
|
done()
|
|
}
|
|
}
|
|
|
|
const FileWriter = {
|
|
ensureDumpFolderExists(callback) {
|
|
if (callback == null) {
|
|
callback = function () {}
|
|
}
|
|
return fs.mkdir(Settings.path.dumpFolder, function (error) {
|
|
if (error != null && error.code !== 'EEXIST') {
|
|
// Ignore error about already existing
|
|
return callback(error)
|
|
}
|
|
return callback(null)
|
|
})
|
|
},
|
|
|
|
writeLinesToDisk(identifier, lines, callback) {
|
|
if (callback == null) {
|
|
callback = function () {}
|
|
}
|
|
return FileWriter.writeContentToDisk(identifier, lines.join('\n'), callback)
|
|
},
|
|
|
|
writeContentToDisk(identifier, content, callback) {
|
|
if (callback == null) {
|
|
callback = function () {}
|
|
}
|
|
callback = _.once(callback)
|
|
const fsPath = `${
|
|
Settings.path.dumpFolder
|
|
}/${identifier}_${crypto.randomUUID()}`
|
|
return FileWriter.ensureDumpFolderExists(function (error) {
|
|
if (error != null) {
|
|
return callback(error)
|
|
}
|
|
return fs.writeFile(fsPath, content, function (error) {
|
|
if (error != null) {
|
|
return callback(error)
|
|
}
|
|
return callback(null, fsPath)
|
|
})
|
|
})
|
|
},
|
|
|
|
writeStreamToDisk(identifier, stream, options, callback) {
|
|
if (typeof options === 'function') {
|
|
callback = options
|
|
options = {}
|
|
}
|
|
if (callback == null) {
|
|
callback = function () {}
|
|
}
|
|
options = options || {}
|
|
|
|
const fsPath = `${
|
|
Settings.path.dumpFolder
|
|
}/${identifier}_${crypto.randomUUID()}`
|
|
|
|
stream.pause()
|
|
|
|
FileWriter.ensureDumpFolderExists(function (error) {
|
|
const writeStream = fs.createWriteStream(fsPath)
|
|
|
|
if (error != null) {
|
|
return callback(error)
|
|
}
|
|
stream.resume()
|
|
|
|
const passThrough = new SizeLimitedStream({
|
|
maxSizeBytes: options.maxSizeBytes,
|
|
})
|
|
|
|
// if writing fails, we want to consume the bytes from the source, to avoid leaks
|
|
for (const evt of ['error', 'close']) {
|
|
writeStream.on(evt, function () {
|
|
passThrough.unpipe(writeStream)
|
|
passThrough.resume()
|
|
})
|
|
}
|
|
|
|
pipeline(stream, passThrough, writeStream, function (err) {
|
|
if (
|
|
options.maxSizeBytes &&
|
|
passThrough.bytes >= options.maxSizeBytes &&
|
|
!(err instanceof FileTooLargeError)
|
|
) {
|
|
err = new FileTooLargeError({
|
|
message: 'stream size limit reached',
|
|
info: { size: passThrough.bytes },
|
|
}).withCause(err || {})
|
|
}
|
|
if (err) {
|
|
OError.tag(
|
|
err,
|
|
'[writeStreamToDisk] something went wrong writing the stream to disk',
|
|
{
|
|
identifier,
|
|
fsPath,
|
|
}
|
|
)
|
|
return callback(err)
|
|
}
|
|
|
|
logger.debug(
|
|
{ identifier, fsPath },
|
|
'[writeStreamToDisk] write stream finished'
|
|
)
|
|
callback(null, fsPath)
|
|
})
|
|
})
|
|
},
|
|
|
|
writeUrlToDisk(identifier, url, options, callback) {
|
|
if (typeof options === 'function') {
|
|
callback = options
|
|
options = {}
|
|
}
|
|
if (callback == null) {
|
|
callback = function () {}
|
|
}
|
|
options = options || {}
|
|
callback = _.once(callback)
|
|
|
|
const stream = request.get(url)
|
|
stream.on('error', function (err) {
|
|
logger.warn(
|
|
{ err, identifier, url },
|
|
'[writeUrlToDisk] something went wrong with writing to disk'
|
|
)
|
|
callback(err)
|
|
})
|
|
stream.on('response', function (response) {
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
FileWriter.writeStreamToDisk(identifier, stream, options, callback)
|
|
} else {
|
|
const err = new Error(`bad response from url: ${response.statusCode}`)
|
|
logger.warn({ err, identifier, url }, `[writeUrlToDisk] ${err.message}`)
|
|
return callback(err)
|
|
}
|
|
})
|
|
},
|
|
}
|
|
|
|
module.exports = FileWriter
|
|
module.exports.promises = promisifyAll(FileWriter)
|
|
module.exports.SizeLimitedStream = SizeLimitedStream
|