2021-07-12 12:47:19 -04:00
|
|
|
const Settings = require('@overleaf/settings')
|
2024-11-08 05:21:56 -05:00
|
|
|
const { callbackify } = require('node:util')
|
|
|
|
const fs = require('node:fs')
|
2024-10-24 07:43:10 -04:00
|
|
|
let PersistorManager = require('./PersistorManager')
|
2019-12-16 05:42:31 -05:00
|
|
|
const LocalFileWriter = require('./LocalFileWriter')
|
|
|
|
const FileConverter = require('./FileConverter')
|
|
|
|
const KeyBuilder = require('./KeyBuilder')
|
|
|
|
const ImageOptimiser = require('./ImageOptimiser')
|
2020-03-16 07:35:01 -04:00
|
|
|
const { ConversionError, InvalidParametersError } = require('./Errors')
|
2024-01-16 10:20:39 -05:00
|
|
|
const metrics = require('@overleaf/metrics')
|
2019-12-18 10:40:30 -05:00
|
|
|
|
|
|
|
module.exports = {
|
2024-10-24 07:42:38 -04:00
|
|
|
copyObject: callbackify(copyObject),
|
2020-02-23 17:34:40 -05:00
|
|
|
insertFile: callbackify(insertFile),
|
|
|
|
deleteFile: callbackify(deleteFile),
|
2020-03-14 10:56:29 -04:00
|
|
|
deleteProject: callbackify(deleteProject),
|
2020-02-23 17:34:40 -05:00
|
|
|
getFile: callbackify(getFile),
|
2020-04-09 12:11:19 -04:00
|
|
|
getRedirectUrl: callbackify(getRedirectUrl),
|
2020-02-23 17:34:40 -05:00
|
|
|
getFileSize: callbackify(getFileSize),
|
|
|
|
getDirectorySize: callbackify(getDirectorySize),
|
2019-12-18 10:40:30 -05:00
|
|
|
promises: {
|
2024-10-24 07:42:38 -04:00
|
|
|
copyObject,
|
2020-02-23 17:34:40 -05:00
|
|
|
getFile,
|
2020-04-09 12:11:19 -04:00
|
|
|
getRedirectUrl,
|
2020-02-23 17:34:40 -05:00
|
|
|
insertFile,
|
|
|
|
deleteFile,
|
2020-03-14 10:56:29 -04:00
|
|
|
deleteProject,
|
2020-02-23 17:34:40 -05:00
|
|
|
getFileSize,
|
2021-07-13 07:04:46 -04:00
|
|
|
getDirectorySize,
|
|
|
|
},
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|
|
|
|
|
2024-10-24 07:43:10 -04:00
|
|
|
if (process.env.NODE_ENV === 'test') {
|
|
|
|
module.exports._TESTONLYSwapPersistorManager = _PersistorManager => {
|
|
|
|
PersistorManager = _PersistorManager
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-24 07:42:38 -04:00
|
|
|
async function copyObject(bucket, sourceKey, destinationKey) {
|
|
|
|
await PersistorManager.copyObject(bucket, sourceKey, destinationKey)
|
|
|
|
}
|
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
async function insertFile(bucket, key, stream) {
|
2019-12-18 10:40:30 -05:00
|
|
|
const convertedKey = KeyBuilder.getConvertedFolderKey(key)
|
2020-05-26 11:21:21 -04:00
|
|
|
if (!convertedKey.match(/^[0-9a-f]{24}\/([0-9a-f]{24}|v\/[0-9]+\/[a-z]+)/i)) {
|
2020-04-30 08:20:40 -04:00
|
|
|
throw new InvalidParametersError('key does not match validation regex', {
|
|
|
|
bucket,
|
|
|
|
key,
|
2021-07-13 07:04:46 -04:00
|
|
|
convertedKey,
|
2020-03-14 10:31:30 -04:00
|
|
|
})
|
|
|
|
}
|
2020-07-07 08:49:54 -04:00
|
|
|
await PersistorManager.sendStream(bucket, key, stream)
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
async function deleteFile(bucket, key) {
|
2019-12-18 10:40:30 -05:00
|
|
|
const convertedKey = KeyBuilder.getConvertedFolderKey(key)
|
2020-05-26 11:21:21 -04:00
|
|
|
if (!convertedKey.match(/^[0-9a-f]{24}\/([0-9a-f]{24}|v\/[0-9]+\/[a-z]+)/i)) {
|
2020-04-30 08:20:40 -04:00
|
|
|
throw new InvalidParametersError('key does not match validation regex', {
|
|
|
|
bucket,
|
|
|
|
key,
|
2021-07-13 07:04:46 -04:00
|
|
|
convertedKey,
|
2020-03-14 10:31:30 -04:00
|
|
|
})
|
|
|
|
}
|
2020-07-07 08:49:54 -04:00
|
|
|
const jobs = [PersistorManager.deleteObject(bucket, key)]
|
2024-10-24 07:43:25 -04:00
|
|
|
if (
|
|
|
|
Settings.enableConversions &&
|
|
|
|
bucket === Settings.filestore.stores.template_files
|
|
|
|
) {
|
2020-07-07 08:49:54 -04:00
|
|
|
jobs.push(PersistorManager.deleteDirectory(bucket, convertedKey))
|
2020-03-31 10:38:42 -04:00
|
|
|
}
|
2020-03-31 11:03:38 -04:00
|
|
|
await Promise.all(jobs)
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|
|
|
|
|
2020-03-14 10:56:29 -04:00
|
|
|
async function deleteProject(bucket, key) {
|
|
|
|
if (!key.match(/^[0-9a-f]{24}\//i)) {
|
2020-04-30 08:20:40 -04:00
|
|
|
throw new InvalidParametersError('key does not match validation regex', {
|
|
|
|
bucket,
|
2021-07-13 07:04:46 -04:00
|
|
|
key,
|
2020-03-14 10:56:29 -04:00
|
|
|
})
|
|
|
|
}
|
2020-07-07 08:49:54 -04:00
|
|
|
await PersistorManager.deleteDirectory(bucket, key)
|
2020-03-14 10:56:29 -04:00
|
|
|
}
|
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
async function getFile(bucket, key, opts) {
|
2020-01-07 16:19:26 -05:00
|
|
|
opts = opts || {}
|
2019-12-18 10:40:30 -05:00
|
|
|
if (!opts.format && !opts.style) {
|
2024-05-22 05:37:08 -04:00
|
|
|
return await PersistorManager.getObjectStream(bucket, key, opts)
|
2019-12-18 10:40:30 -05:00
|
|
|
} else {
|
2024-05-22 05:37:08 -04:00
|
|
|
return await _getConvertedFile(bucket, key, opts)
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-16 10:20:39 -05:00
|
|
|
let ACTIVE_SIGNED_URL_CALLS = 0
|
|
|
|
|
2020-04-09 12:11:19 -04:00
|
|
|
async function getRedirectUrl(bucket, key, opts) {
|
|
|
|
// if we're doing anything unusual with options, or the request isn't for
|
|
|
|
// one of the default buckets, return null so that we proxy the file
|
|
|
|
opts = opts || {}
|
|
|
|
if (
|
|
|
|
!opts.start &&
|
|
|
|
!opts.end &&
|
|
|
|
!opts.format &&
|
|
|
|
!opts.style &&
|
|
|
|
Object.values(Settings.filestore.stores).includes(bucket) &&
|
|
|
|
Settings.filestore.allowRedirects
|
|
|
|
) {
|
2024-01-16 10:20:39 -05:00
|
|
|
// record the number of in-flight calls to generate signed URLs
|
|
|
|
metrics.gauge('active_signed_url_calls', ++ACTIVE_SIGNED_URL_CALLS, {
|
|
|
|
path: bucket,
|
|
|
|
})
|
|
|
|
try {
|
|
|
|
const timer = new metrics.Timer('signed_url_call_time', {
|
|
|
|
path: bucket,
|
|
|
|
})
|
|
|
|
const redirectUrl = await PersistorManager.getRedirectUrl(bucket, key)
|
|
|
|
timer.done()
|
|
|
|
return redirectUrl
|
|
|
|
} finally {
|
|
|
|
metrics.gauge('active_signed_url_calls', --ACTIVE_SIGNED_URL_CALLS, {
|
|
|
|
path: bucket,
|
|
|
|
})
|
|
|
|
}
|
2020-04-09 12:11:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
async function getFileSize(bucket, key) {
|
2024-05-22 05:37:08 -04:00
|
|
|
return await PersistorManager.getObjectSize(bucket, key)
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
async function getDirectorySize(bucket, projectId) {
|
2024-11-08 03:31:26 -05:00
|
|
|
return await PersistorManager.directorySize(bucket, projectId)
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
async function _getConvertedFile(bucket, key, opts) {
|
2019-12-18 10:40:30 -05:00
|
|
|
const convertedKey = KeyBuilder.addCachingToKey(key, opts)
|
2020-07-07 08:49:54 -04:00
|
|
|
const exists = await PersistorManager.checkIfObjectExists(
|
2020-02-23 17:34:40 -05:00
|
|
|
bucket,
|
|
|
|
convertedKey
|
|
|
|
)
|
|
|
|
if (exists) {
|
2024-05-22 05:37:08 -04:00
|
|
|
return await PersistorManager.getObjectStream(bucket, convertedKey, opts)
|
2020-02-23 17:34:40 -05:00
|
|
|
} else {
|
2024-05-22 05:37:08 -04:00
|
|
|
return await _getConvertedFileAndCache(bucket, key, convertedKey, opts)
|
2020-02-23 17:34:40 -05:00
|
|
|
}
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
async function _getConvertedFileAndCache(bucket, key, convertedKey, opts) {
|
2019-12-18 10:40:30 -05:00
|
|
|
let convertedFsPath
|
2020-02-23 17:34:40 -05:00
|
|
|
try {
|
|
|
|
convertedFsPath = await _convertFile(bucket, key, opts)
|
|
|
|
await ImageOptimiser.promises.compressPng(convertedFsPath)
|
2020-07-07 08:49:54 -04:00
|
|
|
await PersistorManager.sendFile(bucket, convertedKey, convertedFsPath)
|
2020-02-23 17:34:40 -05:00
|
|
|
} catch (err) {
|
|
|
|
LocalFileWriter.deleteFile(convertedFsPath, () => {})
|
2020-07-07 08:49:54 -04:00
|
|
|
throw new ConversionError(
|
|
|
|
'failed to convert file',
|
|
|
|
{ opts, bucket, key, convertedKey },
|
|
|
|
err
|
|
|
|
)
|
2020-02-23 17:34:40 -05:00
|
|
|
}
|
|
|
|
// Send back the converted file from the local copy to avoid problems
|
|
|
|
// with the file not being present in S3 yet. As described in the
|
|
|
|
// documentation below, we have already made a 'HEAD' request in
|
|
|
|
// checkIfFileExists so we only have "eventual consistency" if we try
|
|
|
|
// to stream it from S3 here. This was a cause of many 403 errors.
|
|
|
|
//
|
|
|
|
// "Amazon S3 provides read-after-write consistency for PUTS of new
|
|
|
|
// objects in your S3 bucket in all regions with one caveat. The
|
|
|
|
// caveat is that if you make a HEAD or GET request to the key name
|
|
|
|
// (to find if the object exists) before creating the object, Amazon
|
|
|
|
// S3 provides eventual consistency for read-after-write.""
|
|
|
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/Introduction.html#ConsistencyModel
|
|
|
|
const readStream = fs.createReadStream(convertedFsPath)
|
2021-11-18 08:43:07 -05:00
|
|
|
readStream.on('error', function () {
|
|
|
|
LocalFileWriter.deleteFile(convertedFsPath, function () {})
|
|
|
|
})
|
2020-08-10 12:01:12 -04:00
|
|
|
readStream.on('end', function () {
|
|
|
|
LocalFileWriter.deleteFile(convertedFsPath, function () {})
|
2020-02-23 17:34:40 -05:00
|
|
|
})
|
|
|
|
return readStream
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|
2014-02-14 11:39:05 -05:00
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
async function _convertFile(bucket, originalKey, opts) {
|
|
|
|
let originalFsPath
|
|
|
|
try {
|
|
|
|
originalFsPath = await _writeFileToDisk(bucket, originalKey, opts)
|
|
|
|
} catch (err) {
|
2020-07-07 08:49:54 -04:00
|
|
|
throw new ConversionError(
|
|
|
|
'unable to write file to disk',
|
|
|
|
{ bucket, originalKey, opts },
|
|
|
|
err
|
|
|
|
)
|
2020-02-23 17:34:40 -05:00
|
|
|
}
|
2014-03-04 08:36:47 -05:00
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
let promise
|
|
|
|
if (opts.format) {
|
|
|
|
promise = FileConverter.promises.convert(originalFsPath, opts.format)
|
|
|
|
} else if (opts.style === 'thumbnail') {
|
|
|
|
promise = FileConverter.promises.thumbnail(originalFsPath)
|
|
|
|
} else if (opts.style === 'preview') {
|
|
|
|
promise = FileConverter.promises.preview(originalFsPath)
|
|
|
|
} else {
|
2020-04-30 08:20:40 -04:00
|
|
|
throw new ConversionError('invalid file conversion options', {
|
|
|
|
bucket,
|
|
|
|
originalKey,
|
2021-07-13 07:04:46 -04:00
|
|
|
opts,
|
2020-02-23 17:34:40 -05:00
|
|
|
})
|
|
|
|
}
|
|
|
|
let destPath
|
|
|
|
try {
|
|
|
|
destPath = await promise
|
|
|
|
} catch (err) {
|
2020-07-07 08:49:54 -04:00
|
|
|
throw new ConversionError(
|
|
|
|
'error converting file',
|
|
|
|
{ bucket, originalKey, opts },
|
|
|
|
err
|
|
|
|
)
|
2020-02-23 17:34:40 -05:00
|
|
|
}
|
2020-08-10 12:01:12 -04:00
|
|
|
LocalFileWriter.deleteFile(originalFsPath, function () {})
|
2020-02-23 17:34:40 -05:00
|
|
|
return destPath
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|
|
|
|
|
2020-02-23 17:34:40 -05:00
|
|
|
async function _writeFileToDisk(bucket, key, opts) {
|
2020-07-07 08:49:54 -04:00
|
|
|
const fileStream = await PersistorManager.getObjectStream(bucket, key, opts)
|
2024-05-22 05:37:08 -04:00
|
|
|
return await LocalFileWriter.promises.writeStream(fileStream, key)
|
2019-12-18 10:40:30 -05:00
|
|
|
}
|