2019-12-16 05:42:31 -05:00
|
|
|
const fs = require('fs')
|
2020-01-06 09:09:28 -05:00
|
|
|
const glob = require('glob')
|
2019-12-16 05:42:31 -05:00
|
|
|
const path = require('path')
|
|
|
|
const rimraf = require('rimraf')
|
2020-01-02 06:29:28 -05:00
|
|
|
const Stream = require('stream')
|
|
|
|
const { promisify, callbackify } = require('util')
|
|
|
|
|
|
|
|
const LocalFileWriter = require('./LocalFileWriter').promises
|
2020-01-06 10:11:35 -05:00
|
|
|
const { NotFoundError, ReadError, WriteError } = require('./Errors')
|
2020-01-02 06:29:28 -05:00
|
|
|
|
|
|
|
const pipeline = promisify(Stream.pipeline)
|
|
|
|
const fsUnlink = promisify(fs.unlink)
|
|
|
|
const fsOpen = promisify(fs.open)
|
|
|
|
const fsStat = promisify(fs.stat)
|
2020-01-06 09:09:28 -05:00
|
|
|
const fsGlob = promisify(glob)
|
2020-01-02 06:29:28 -05:00
|
|
|
const rmrf = promisify(rimraf)
|
2015-08-31 11:47:16 -04:00
|
|
|
|
2019-12-16 05:42:31 -05:00
|
|
|
const filterName = key => key.replace(/\//g, '_')
|
2014-02-26 10:10:55 -05:00
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
async function sendFile(location, target, source) {
|
|
|
|
const filteredTarget = filterName(target)
|
|
|
|
|
|
|
|
// actually copy the file (instead of moving it) to maintain consistent behaviour
|
|
|
|
// between the different implementations
|
2020-01-06 10:11:35 -05:00
|
|
|
try {
|
|
|
|
const sourceStream = fs.createReadStream(source)
|
|
|
|
const targetStream = fs.createWriteStream(`${location}/${filteredTarget}`)
|
|
|
|
await pipeline(sourceStream, targetStream)
|
|
|
|
} catch (err) {
|
|
|
|
throw _wrapError(
|
|
|
|
err,
|
|
|
|
'failed to copy the specified file',
|
|
|
|
{ location, target, source },
|
|
|
|
WriteError
|
|
|
|
)
|
|
|
|
}
|
2020-01-02 06:29:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
async function sendStream(location, target, sourceStream) {
|
2020-01-06 10:11:35 -05:00
|
|
|
const fsPath = await LocalFileWriter.writeStream(sourceStream)
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
try {
|
|
|
|
await sendFile(location, target, fsPath)
|
|
|
|
} finally {
|
|
|
|
await LocalFileWriter.deleteFile(fsPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// opts may be {start: Number, end: Number}
|
|
|
|
async function getFileStream(location, name, opts) {
|
|
|
|
const filteredName = filterName(name)
|
|
|
|
|
|
|
|
try {
|
|
|
|
opts.fd = await fsOpen(`${location}/${filteredName}`, 'r')
|
|
|
|
} catch (err) {
|
2020-01-06 10:11:35 -05:00
|
|
|
throw _wrapError(
|
|
|
|
err,
|
|
|
|
'failed to open file for streaming',
|
|
|
|
{ location, filteredName, opts },
|
|
|
|
ReadError
|
|
|
|
)
|
2020-01-02 06:29:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return fs.createReadStream(null, opts)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getFileSize(location, filename) {
|
|
|
|
const fullPath = path.join(location, filterName(filename))
|
|
|
|
|
|
|
|
try {
|
|
|
|
const stat = await fsStat(fullPath)
|
|
|
|
return stat.size
|
|
|
|
} catch (err) {
|
2020-01-06 10:11:35 -05:00
|
|
|
throw _wrapError(
|
|
|
|
err,
|
|
|
|
'failed to stat file',
|
|
|
|
{ location, filename },
|
|
|
|
ReadError
|
|
|
|
)
|
2020-01-02 06:29:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function copyFile(location, fromName, toName) {
|
|
|
|
const filteredFromName = filterName(fromName)
|
|
|
|
const filteredToName = filterName(toName)
|
|
|
|
|
2020-01-06 10:11:35 -05:00
|
|
|
try {
|
|
|
|
const sourceStream = fs.createReadStream(`${location}/${filteredFromName}`)
|
|
|
|
const targetStream = fs.createWriteStream(`${location}/${filteredToName}`)
|
|
|
|
await pipeline(sourceStream, targetStream)
|
|
|
|
} catch (err) {
|
|
|
|
throw _wrapError(
|
|
|
|
err,
|
|
|
|
'failed to copy file',
|
|
|
|
{ location, filteredFromName, filteredToName },
|
|
|
|
WriteError
|
|
|
|
)
|
|
|
|
}
|
2020-01-02 06:29:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
async function deleteFile(location, name) {
|
|
|
|
const filteredName = filterName(name)
|
2020-01-06 10:11:35 -05:00
|
|
|
try {
|
|
|
|
await fsUnlink(`${location}/${filteredName}`)
|
|
|
|
} catch (err) {
|
2020-01-16 11:25:12 -05:00
|
|
|
const wrappedError = _wrapError(
|
2020-01-06 10:11:35 -05:00
|
|
|
err,
|
|
|
|
'failed to delete file',
|
|
|
|
{ location, filteredName },
|
|
|
|
WriteError
|
|
|
|
)
|
2020-01-16 11:25:12 -05:00
|
|
|
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
|
|
|
|
}
|
2020-01-06 10:11:35 -05:00
|
|
|
}
|
2020-01-02 06:29:28 -05:00
|
|
|
}
|
|
|
|
|
2020-01-06 10:13:50 -05:00
|
|
|
// this is only called internally for clean-up by `FileHandler` and isn't part of the external API
|
2020-01-02 06:29:28 -05:00
|
|
|
async function deleteDirectory(location, name) {
|
|
|
|
const filteredName = filterName(name.replace(/\/$/, ''))
|
|
|
|
|
2020-01-06 10:11:35 -05:00
|
|
|
try {
|
|
|
|
await rmrf(`${location}/${filteredName}`)
|
|
|
|
} catch (err) {
|
|
|
|
throw _wrapError(
|
|
|
|
err,
|
|
|
|
'failed to delete directory',
|
|
|
|
{ location, filteredName },
|
|
|
|
WriteError
|
|
|
|
)
|
|
|
|
}
|
2020-01-02 06:29:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
async function checkIfFileExists(location, name) {
|
|
|
|
const filteredName = filterName(name)
|
|
|
|
try {
|
|
|
|
const stat = await fsStat(`${location}/${filteredName}`)
|
|
|
|
return !!stat
|
|
|
|
} catch (err) {
|
|
|
|
if (err.code === 'ENOENT') {
|
|
|
|
return false
|
2019-12-16 05:42:31 -05:00
|
|
|
}
|
2020-01-06 10:11:35 -05:00
|
|
|
throw _wrapError(
|
|
|
|
err,
|
|
|
|
'failed to stat file',
|
|
|
|
{ location, filteredName },
|
|
|
|
ReadError
|
|
|
|
)
|
2020-01-02 06:29:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-06 09:09:28 -05:00
|
|
|
// note, does not recurse into subdirectories, as we use a flattened directory structure
|
2020-01-02 06:29:28 -05:00
|
|
|
async function directorySize(location, name) {
|
|
|
|
const filteredName = filterName(name.replace(/\/$/, ''))
|
|
|
|
let size = 0
|
|
|
|
|
|
|
|
try {
|
2020-01-06 09:09:28 -05:00
|
|
|
const files = await fsGlob(`${location}/${filteredName}_*`)
|
2020-01-02 06:29:28 -05:00
|
|
|
for (const file of files) {
|
2020-01-06 09:09:28 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2019-12-16 05:42:31 -05:00
|
|
|
}
|
2020-01-02 06:29:28 -05:00
|
|
|
} catch (err) {
|
2020-01-06 10:11:35 -05:00
|
|
|
throw _wrapError(
|
|
|
|
err,
|
|
|
|
'failed to get directory size',
|
|
|
|
{ location, name },
|
|
|
|
ReadError
|
|
|
|
)
|
2020-01-02 06:29:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return size
|
|
|
|
}
|
|
|
|
|
2020-01-06 10:11:35 -05:00
|
|
|
function _wrapError(error, message, params, ErrorType) {
|
|
|
|
if (error.code === 'ENOENT') {
|
|
|
|
return new NotFoundError({
|
|
|
|
message: 'no such file or directory',
|
|
|
|
info: params
|
|
|
|
}).withCause(error)
|
|
|
|
} else {
|
|
|
|
return new ErrorType({
|
|
|
|
message: message,
|
|
|
|
info: params
|
|
|
|
}).withCause(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
module.exports = {
|
|
|
|
sendFile: callbackify(sendFile),
|
|
|
|
sendStream: callbackify(sendStream),
|
|
|
|
getFileStream: callbackify(getFileStream),
|
|
|
|
getFileSize: callbackify(getFileSize),
|
|
|
|
copyFile: callbackify(copyFile),
|
|
|
|
deleteFile: callbackify(deleteFile),
|
|
|
|
deleteDirectory: callbackify(deleteDirectory),
|
|
|
|
checkIfFileExists: callbackify(checkIfFileExists),
|
|
|
|
directorySize: callbackify(directorySize),
|
|
|
|
promises: {
|
|
|
|
sendFile,
|
|
|
|
sendStream,
|
|
|
|
getFileStream,
|
|
|
|
getFileSize,
|
|
|
|
copyFile,
|
|
|
|
deleteFile,
|
|
|
|
deleteDirectory,
|
|
|
|
checkIfFileExists,
|
|
|
|
directorySize
|
2019-12-16 05:24:35 -05:00
|
|
|
}
|
2019-12-16 05:42:31 -05:00
|
|
|
}
|