mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-31 04:41:32 +02:00
193 lines
5.5 KiB
JavaScript
193 lines
5.5 KiB
JavaScript
// TODO: This file was created by bulk-decaffeinate.
|
|
/*
|
|
* 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
|
|
*/
|
|
import fs from 'node:fs'
|
|
|
|
import OError from '@overleaf/o-error'
|
|
import logger from '@overleaf/logger'
|
|
import crypto from 'node:crypto'
|
|
import Settings from '@overleaf/settings'
|
|
import { fetchStream } from '@overleaf/fetch-utils'
|
|
import { Transform, pipeline } from 'node:stream'
|
|
import { FileTooLargeError } from '../Features/Errors/Errors.js'
|
|
import { callbackify, promisify } from '@overleaf/promise-utils'
|
|
|
|
export class SizeLimitedStream extends Transform {
|
|
constructor(options) {
|
|
options.autoDestroy = true
|
|
super(options)
|
|
|
|
this.bytes = 0
|
|
this.maxSizeBytes = options.maxSizeBytes || Settings.maxUploadSize
|
|
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()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @type {{
|
|
* ensureDumpFolderExists: () => void,
|
|
* writeLinesToDisk: (identifier: any, lines: any, callback?: any) => void,
|
|
* writeContentToDisk: (identifier: any, content: any, callback?: any) => void,
|
|
* writeStreamToDisk: (identifier: any, stream: any, options?: any, callback?: any) => void,
|
|
* writeUrlToDisk: (identifier: any, url: any, options?: any, callback?: any) => void,
|
|
* promises: {
|
|
* writeLinesToDisk: (identifier: any, lines: any) => Promise<string>,
|
|
* writeContentToDisk: (identifier: any, content: any) => Promise<string>,
|
|
* writeStreamToDisk: (identifier: any, stream: any, options?: any) => Promise<string>,
|
|
* writeUrlToDisk: (identifier: any, url: any, options?: any) => Promise<string>,
|
|
* },
|
|
* }}
|
|
*/
|
|
const FileWriter = {
|
|
ensureDumpFolderExists() {
|
|
fs.mkdirSync(Settings.path.dumpFolder, { recursive: true })
|
|
},
|
|
|
|
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 () {}
|
|
}
|
|
const fsPath = `${
|
|
Settings.path.dumpFolder
|
|
}/${identifier}_${crypto.randomUUID()}`
|
|
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()}`
|
|
|
|
const writeStream = fs.createWriteStream(fsPath)
|
|
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) {
|
|
stream.destroy()
|
|
writeStream.destroy()
|
|
fs.unlink(fsPath, error => {
|
|
if (error && error.code !== 'ENOENT') {
|
|
logger.warn(
|
|
{ error, fsPath },
|
|
'Failed to delete partial file after error'
|
|
)
|
|
}
|
|
})
|
|
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)
|
|
})
|
|
},
|
|
}
|
|
|
|
async function writeUrlToDisk(identifier, url, options = {}) {
|
|
let stream
|
|
try {
|
|
stream = await fetchStream(url)
|
|
} catch (error) {
|
|
const err = new OError('bad response from url', {
|
|
statusCode: error.response?.status,
|
|
})
|
|
logger.warn({ err, identifier, url }, `[writeUrlToDisk] ${err.message}`)
|
|
throw err
|
|
}
|
|
return await FileWriter.promises.writeStreamToDisk(
|
|
identifier,
|
|
stream,
|
|
options
|
|
)
|
|
}
|
|
|
|
FileWriter.writeUrlToDisk = callbackify(writeUrlToDisk)
|
|
|
|
FileWriter.promises = {
|
|
writeLinesToDisk: promisify(FileWriter.writeLinesToDisk),
|
|
writeContentToDisk: promisify(FileWriter.writeContentToDisk),
|
|
writeStreamToDisk: promisify(FileWriter.writeStreamToDisk),
|
|
writeUrlToDisk,
|
|
}
|
|
|
|
export default FileWriter
|