Files
overleaf-cep/libraries/object-persistor/src/FSPersistor.js
Brian Gough e0bf4217a8 Merge pull request #5258 from overleaf/revert-5214-bg-migrate-node-uuid-to-uuid
Revert "migrate node-uuid to uuid"

GitOrigin-RevId: f512eb1040e2ce6d87774de9c1fc884594480cef
2021-09-28 08:04:11 +00:00

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)
}
}