mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-25 02:00:10 +02:00
[misc] move the linting setup to the root of the monorepo GitOrigin-RevId: 1633e2a58598add0b727738cd3bfba0ab7bae781
277 lines
7.0 KiB
JavaScript
277 lines
7.0 KiB
JavaScript
const fs = require('fs')
|
|
const glob = require('glob')
|
|
const uuid = require('node-uuid')
|
|
const path = require('path')
|
|
const Stream = require('stream')
|
|
const { promisify } = require('util')
|
|
|
|
const AbstractPersistor = require('./AbstractPersistor')
|
|
const { NotFoundError, ReadError, WriteError } = require('./Errors')
|
|
const PersistorHelper = require('./PersistorHelper')
|
|
|
|
const pipeline = promisify(Stream.pipeline)
|
|
const fsUnlink = promisify(fs.unlink)
|
|
const fsOpen = promisify(fs.open)
|
|
const fsStat = promisify(fs.stat)
|
|
const fsGlob = promisify(glob)
|
|
|
|
const filterName = key => key.replace(/\//g, '_')
|
|
|
|
module.exports = class FSPersistor extends AbstractPersistor {
|
|
constructor(settings) {
|
|
super()
|
|
|
|
this.settings = settings
|
|
}
|
|
|
|
async sendFile(location, target, source) {
|
|
const filteredTarget = filterName(target)
|
|
|
|
// actually copy the file (instead of moving it) to maintain consistent behaviour
|
|
// between the different implementations
|
|
try {
|
|
const sourceStream = fs.createReadStream(source)
|
|
const targetStream = fs.createWriteStream(`${location}/${filteredTarget}`)
|
|
await pipeline(sourceStream, targetStream)
|
|
} catch (err) {
|
|
throw PersistorHelper.wrapError(
|
|
err,
|
|
'failed to copy the specified file',
|
|
{ location, target, source },
|
|
WriteError
|
|
)
|
|
}
|
|
}
|
|
|
|
async sendStream(location, target, sourceStream, opts = {}) {
|
|
const fsPath = await this._writeStream(sourceStream)
|
|
let sourceMd5 = opts.sourceMd5
|
|
if (!sourceMd5) {
|
|
sourceMd5 = await FSPersistor._getFileMd5HashForPath(fsPath)
|
|
}
|
|
|
|
try {
|
|
await this.sendFile(location, target, fsPath)
|
|
const destMd5 = await this.getObjectMd5Hash(location, target)
|
|
if (sourceMd5 !== destMd5) {
|
|
await this._deleteFile(`${location}/${filterName(target)}`)
|
|
throw new WriteError('md5 hash mismatch', {
|
|
sourceMd5,
|
|
destMd5,
|
|
location,
|
|
target,
|
|
})
|
|
}
|
|
} finally {
|
|
await this._deleteFile(fsPath)
|
|
}
|
|
}
|
|
|
|
// opts may be {start: Number, end: Number}
|
|
async getObjectStream(location, name, opts) {
|
|
const filteredName = filterName(name)
|
|
|
|
try {
|
|
opts.fd = await fsOpen(`${location}/${filteredName}`, 'r')
|
|
} catch (err) {
|
|
throw PersistorHelper.wrapError(
|
|
err,
|
|
'failed to open file for streaming',
|
|
{ location, filteredName, opts },
|
|
ReadError
|
|
)
|
|
}
|
|
|
|
return fs.createReadStream(null, opts)
|
|
}
|
|
|
|
async getRedirectUrl() {
|
|
// not implemented
|
|
return null
|
|
}
|
|
|
|
async getObjectSize(location, filename) {
|
|
const fullPath = path.join(location, filterName(filename))
|
|
|
|
try {
|
|
const stat = await fsStat(fullPath)
|
|
return stat.size
|
|
} catch (err) {
|
|
throw PersistorHelper.wrapError(
|
|
err,
|
|
'failed to stat file',
|
|
{ location, filename },
|
|
ReadError
|
|
)
|
|
}
|
|
}
|
|
|
|
async getObjectMd5Hash(location, filename) {
|
|
const fullPath = path.join(location, filterName(filename))
|
|
try {
|
|
return await FSPersistor._getFileMd5HashForPath(fullPath)
|
|
} catch (err) {
|
|
throw new ReadError(
|
|
'unable to get md5 hash from file',
|
|
{ location, filename },
|
|
err
|
|
)
|
|
}
|
|
}
|
|
|
|
async copyObject(location, fromName, toName) {
|
|
const filteredFromName = filterName(fromName)
|
|
const filteredToName = filterName(toName)
|
|
|
|
try {
|
|
const sourceStream = fs.createReadStream(
|
|
`${location}/${filteredFromName}`
|
|
)
|
|
const targetStream = fs.createWriteStream(`${location}/${filteredToName}`)
|
|
await pipeline(sourceStream, targetStream)
|
|
} catch (err) {
|
|
throw PersistorHelper.wrapError(
|
|
err,
|
|
'failed to copy file',
|
|
{ location, filteredFromName, filteredToName },
|
|
WriteError
|
|
)
|
|
}
|
|
}
|
|
|
|
async deleteObject(location, name) {
|
|
const filteredName = filterName(name)
|
|
try {
|
|
await fsUnlink(`${location}/${filteredName}`)
|
|
} catch (err) {
|
|
const wrappedError = PersistorHelper.wrapError(
|
|
err,
|
|
'failed to delete file',
|
|
{ location, filteredName },
|
|
WriteError
|
|
)
|
|
if (!(wrappedError instanceof NotFoundError)) {
|
|
// S3 doesn't give us a 404 when a file wasn't there to be deleted, so we
|
|
// should be consistent here as well
|
|
throw wrappedError
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteDirectory(location, name) {
|
|
const filteredName = filterName(name.replace(/\/$/, ''))
|
|
|
|
try {
|
|
await Promise.all(
|
|
(
|
|
await fsGlob(`${location}/${filteredName}_*`)
|
|
).map(file => fsUnlink(file))
|
|
)
|
|
} catch (err) {
|
|
throw PersistorHelper.wrapError(
|
|
err,
|
|
'failed to delete directory',
|
|
{ location, filteredName },
|
|
WriteError
|
|
)
|
|
}
|
|
}
|
|
|
|
async checkIfObjectExists(location, name) {
|
|
const filteredName = filterName(name)
|
|
try {
|
|
const stat = await fsStat(`${location}/${filteredName}`)
|
|
return !!stat
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') {
|
|
return false
|
|
}
|
|
throw PersistorHelper.wrapError(
|
|
err,
|
|
'failed to stat file',
|
|
{ location, filteredName },
|
|
ReadError
|
|
)
|
|
}
|
|
}
|
|
|
|
// note, does not recurse into subdirectories, as we use a flattened directory structure
|
|
async directorySize(location, name) {
|
|
const filteredName = filterName(name.replace(/\/$/, ''))
|
|
let size = 0
|
|
|
|
try {
|
|
const files = await fsGlob(`${location}/${filteredName}_*`)
|
|
for (const file of files) {
|
|
try {
|
|
const stat = await fsStat(file)
|
|
if (stat.isFile()) {
|
|
size += stat.size
|
|
}
|
|
} catch (err) {
|
|
// ignore files that may have just been deleted
|
|
if (err.code !== 'ENOENT') {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
throw PersistorHelper.wrapError(
|
|
err,
|
|
'failed to get directory size',
|
|
{ location, name },
|
|
ReadError
|
|
)
|
|
}
|
|
|
|
return size
|
|
}
|
|
|
|
_getPath(key) {
|
|
if (key == null) {
|
|
key = uuid.v1()
|
|
}
|
|
key = key.replace(/\//g, '-')
|
|
return path.join(this.settings.paths.uploadFolder, key)
|
|
}
|
|
|
|
async _writeStream(stream, key) {
|
|
let timer
|
|
if (this.settings.Metrics) {
|
|
timer = new this.settings.Metrics.Timer('writingFile')
|
|
}
|
|
const fsPath = this._getPath(key)
|
|
|
|
const writeStream = fs.createWriteStream(fsPath)
|
|
try {
|
|
await pipeline(stream, writeStream)
|
|
if (timer) {
|
|
timer.done()
|
|
}
|
|
return fsPath
|
|
} catch (err) {
|
|
await this._deleteFile(fsPath)
|
|
|
|
throw new WriteError('problem writing file locally', { err, fsPath }, err)
|
|
}
|
|
}
|
|
|
|
async _deleteFile(fsPath) {
|
|
if (!fsPath) {
|
|
return
|
|
}
|
|
try {
|
|
await fsUnlink(fsPath)
|
|
} catch (err) {
|
|
if (err.code !== 'ENOENT') {
|
|
throw new WriteError('failed to delete file', { fsPath }, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
static async _getFileMd5HashForPath(fullPath) {
|
|
const stream = fs.createReadStream(fullPath)
|
|
return PersistorHelper.calculateStreamMd5(stream)
|
|
}
|
|
}
|