mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 22:23:42 +01:00
Merge pull request #93 from overleaf/spd-migration-persistor-redux
Add migration persistor with md5 fix
This commit is contained in:
@@ -23,7 +23,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
// Swap the no-unused-expressions rule with a more chai-friendly one
|
// Swap the no-unused-expressions rule with a more chai-friendly one
|
||||||
"no-unused-expressions": 0,
|
"no-unused-expressions": 0,
|
||||||
"chai-friendly/no-unused-expressions": "error"
|
"chai-friendly/no-unused-expressions": "error",
|
||||||
|
"no-console": "error"
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|||||||
+45
-23
@@ -7,6 +7,7 @@ const { promisify, callbackify } = require('util')
|
|||||||
|
|
||||||
const LocalFileWriter = require('./LocalFileWriter').promises
|
const LocalFileWriter = require('./LocalFileWriter').promises
|
||||||
const { NotFoundError, ReadError, WriteError } = require('./Errors')
|
const { NotFoundError, ReadError, WriteError } = require('./Errors')
|
||||||
|
const PersistorHelper = require('./PersistorHelper')
|
||||||
|
|
||||||
const pipeline = promisify(Stream.pipeline)
|
const pipeline = promisify(Stream.pipeline)
|
||||||
const fsUnlink = promisify(fs.unlink)
|
const fsUnlink = promisify(fs.unlink)
|
||||||
@@ -27,7 +28,7 @@ async function sendFile(location, target, source) {
|
|||||||
const targetStream = fs.createWriteStream(`${location}/${filteredTarget}`)
|
const targetStream = fs.createWriteStream(`${location}/${filteredTarget}`)
|
||||||
await pipeline(sourceStream, targetStream)
|
await pipeline(sourceStream, targetStream)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to copy the specified file',
|
'failed to copy the specified file',
|
||||||
{ location, target, source },
|
{ location, target, source },
|
||||||
@@ -36,11 +37,22 @@ async function sendFile(location, target, source) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendStream(location, target, sourceStream) {
|
async function sendStream(location, target, sourceStream, sourceMd5) {
|
||||||
const fsPath = await LocalFileWriter.writeStream(sourceStream)
|
const fsPath = await LocalFileWriter.writeStream(sourceStream)
|
||||||
|
if (!sourceMd5) {
|
||||||
|
sourceMd5 = await _getFileMd5HashForPath(fsPath)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendFile(location, target, fsPath)
|
await sendFile(location, target, fsPath)
|
||||||
|
const destMd5 = await getFileMd5Hash(location, target)
|
||||||
|
if (sourceMd5 !== destMd5) {
|
||||||
|
await LocalFileWriter.deleteFile(`${location}/${filterName(target)}`)
|
||||||
|
throw new WriteError({
|
||||||
|
message: 'md5 hash mismatch',
|
||||||
|
info: { sourceMd5, destMd5, location, target }
|
||||||
|
})
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await LocalFileWriter.deleteFile(fsPath)
|
await LocalFileWriter.deleteFile(fsPath)
|
||||||
}
|
}
|
||||||
@@ -53,7 +65,7 @@ async function getFileStream(location, name, opts) {
|
|||||||
try {
|
try {
|
||||||
opts.fd = await fsOpen(`${location}/${filteredName}`, 'r')
|
opts.fd = await fsOpen(`${location}/${filteredName}`, 'r')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to open file for streaming',
|
'failed to open file for streaming',
|
||||||
{ location, filteredName, opts },
|
{ location, filteredName, opts },
|
||||||
@@ -71,7 +83,7 @@ async function getFileSize(location, filename) {
|
|||||||
const stat = await fsStat(fullPath)
|
const stat = await fsStat(fullPath)
|
||||||
return stat.size
|
return stat.size
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to stat file',
|
'failed to stat file',
|
||||||
{ location, filename },
|
{ location, filename },
|
||||||
@@ -80,6 +92,18 @@ async function getFileSize(location, filename) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getFileMd5Hash(location, filename) {
|
||||||
|
const fullPath = path.join(location, filterName(filename))
|
||||||
|
try {
|
||||||
|
return await _getFileMd5HashForPath(fullPath)
|
||||||
|
} catch (err) {
|
||||||
|
throw new ReadError({
|
||||||
|
message: 'unable to get md5 hash from file',
|
||||||
|
info: { location, filename }
|
||||||
|
}).withCause(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function copyFile(location, fromName, toName) {
|
async function copyFile(location, fromName, toName) {
|
||||||
const filteredFromName = filterName(fromName)
|
const filteredFromName = filterName(fromName)
|
||||||
const filteredToName = filterName(toName)
|
const filteredToName = filterName(toName)
|
||||||
@@ -89,7 +113,7 @@ async function copyFile(location, fromName, toName) {
|
|||||||
const targetStream = fs.createWriteStream(`${location}/${filteredToName}`)
|
const targetStream = fs.createWriteStream(`${location}/${filteredToName}`)
|
||||||
await pipeline(sourceStream, targetStream)
|
await pipeline(sourceStream, targetStream)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to copy file',
|
'failed to copy file',
|
||||||
{ location, filteredFromName, filteredToName },
|
{ location, filteredFromName, filteredToName },
|
||||||
@@ -103,12 +127,17 @@ async function deleteFile(location, name) {
|
|||||||
try {
|
try {
|
||||||
await fsUnlink(`${location}/${filteredName}`)
|
await fsUnlink(`${location}/${filteredName}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
const wrappedError = PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to delete file',
|
'failed to delete file',
|
||||||
{ location, filteredName },
|
{ location, filteredName },
|
||||||
WriteError
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +148,7 @@ async function deleteDirectory(location, name) {
|
|||||||
try {
|
try {
|
||||||
await rmrf(`${location}/${filteredName}`)
|
await rmrf(`${location}/${filteredName}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to delete directory',
|
'failed to delete directory',
|
||||||
{ location, filteredName },
|
{ location, filteredName },
|
||||||
@@ -137,7 +166,7 @@ async function checkIfFileExists(location, name) {
|
|||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to stat file',
|
'failed to stat file',
|
||||||
{ location, filteredName },
|
{ location, filteredName },
|
||||||
@@ -167,7 +196,7 @@ async function directorySize(location, name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to get directory size',
|
'failed to get directory size',
|
||||||
{ location, name },
|
{ location, name },
|
||||||
@@ -178,25 +207,12 @@ async function directorySize(location, name) {
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendFile: callbackify(sendFile),
|
sendFile: callbackify(sendFile),
|
||||||
sendStream: callbackify(sendStream),
|
sendStream: callbackify(sendStream),
|
||||||
getFileStream: callbackify(getFileStream),
|
getFileStream: callbackify(getFileStream),
|
||||||
getFileSize: callbackify(getFileSize),
|
getFileSize: callbackify(getFileSize),
|
||||||
|
getFileMd5Hash: callbackify(getFileMd5Hash),
|
||||||
copyFile: callbackify(copyFile),
|
copyFile: callbackify(copyFile),
|
||||||
deleteFile: callbackify(deleteFile),
|
deleteFile: callbackify(deleteFile),
|
||||||
deleteDirectory: callbackify(deleteDirectory),
|
deleteDirectory: callbackify(deleteDirectory),
|
||||||
@@ -207,6 +223,7 @@ module.exports = {
|
|||||||
sendStream,
|
sendStream,
|
||||||
getFileStream,
|
getFileStream,
|
||||||
getFileSize,
|
getFileSize,
|
||||||
|
getFileMd5Hash,
|
||||||
copyFile,
|
copyFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
deleteDirectory,
|
deleteDirectory,
|
||||||
@@ -214,3 +231,8 @@ module.exports = {
|
|||||||
directorySize
|
directorySize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _getFileMd5HashForPath(fullPath) {
|
||||||
|
const stream = fs.createReadStream(fullPath)
|
||||||
|
return PersistorHelper.calculateStreamMd5(stream)
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
const metrics = require('metrics-sharelatex')
|
||||||
|
const Settings = require('settings-sharelatex')
|
||||||
|
const logger = require('logger-sharelatex')
|
||||||
|
const Stream = require('stream')
|
||||||
|
const { callbackify, promisify } = require('util')
|
||||||
|
const { NotFoundError, WriteError } = require('./Errors')
|
||||||
|
|
||||||
|
const pipeline = promisify(Stream.pipeline)
|
||||||
|
|
||||||
|
// Persistor that wraps two other persistors. Talks to the 'primary' by default,
|
||||||
|
// but will fall back to an older persistor in the case of a not-found error.
|
||||||
|
// If `Settings.filestore.fallback.copyOnMiss` is set, this will copy files from the fallback
|
||||||
|
// to the primary, in the event that they are missing.
|
||||||
|
//
|
||||||
|
// It is unlikely that the bucket/location name will be the same on the fallback
|
||||||
|
// as the primary. The bucket names should be overridden in `Settings.filestore.fallback.buckets`
|
||||||
|
// e.g.
|
||||||
|
// Settings.filestore.fallback.buckets = {
|
||||||
|
// myBucketOnS3: 'myBucketOnGCS'
|
||||||
|
// }
|
||||||
|
|
||||||
|
module.exports = function(primary, fallback) {
|
||||||
|
function _wrapMethodOnBothPersistors(method) {
|
||||||
|
return async function(bucket, key, ...moreArgs) {
|
||||||
|
const fallbackBucket = _getFallbackBucket(bucket)
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
primary.promises[method](bucket, key, ...moreArgs),
|
||||||
|
fallback.promises[method](fallbackBucket, key, ...moreArgs)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileStreamWithFallback(bucket, key, opts) {
|
||||||
|
const shouldCopy =
|
||||||
|
Settings.filestore.fallback.copyOnMiss && !opts.start && !opts.end
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await primary.promises.getFileStream(bucket, key, opts)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof NotFoundError) {
|
||||||
|
const fallbackBucket = _getFallbackBucket(bucket)
|
||||||
|
const fallbackStream = await fallback.promises.getFileStream(
|
||||||
|
fallbackBucket,
|
||||||
|
key,
|
||||||
|
opts
|
||||||
|
)
|
||||||
|
// tee the stream to the client, and as a copy to the primary (if necessary)
|
||||||
|
// start listening on both straight away so that we don't consume bytes
|
||||||
|
// in one place before the other
|
||||||
|
const returnStream = new Stream.PassThrough()
|
||||||
|
pipeline(fallbackStream, returnStream)
|
||||||
|
|
||||||
|
if (shouldCopy) {
|
||||||
|
const copyStream = new Stream.PassThrough()
|
||||||
|
pipeline(fallbackStream, copyStream)
|
||||||
|
|
||||||
|
_copyStreamFromFallbackAndVerify(
|
||||||
|
copyStream,
|
||||||
|
fallbackBucket,
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
key
|
||||||
|
).catch(() => {
|
||||||
|
// swallow errors, as this runs in the background and will log a warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return returnStream
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyFileWithFallback(bucket, sourceKey, destKey) {
|
||||||
|
try {
|
||||||
|
return await primary.promises.copyFile(bucket, sourceKey, destKey)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof NotFoundError) {
|
||||||
|
const fallbackBucket = _getFallbackBucket(bucket)
|
||||||
|
const fallbackStream = await fallback.promises.getFileStream(
|
||||||
|
fallbackBucket,
|
||||||
|
sourceKey,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const copyStream = new Stream.PassThrough()
|
||||||
|
pipeline(fallbackStream, copyStream)
|
||||||
|
|
||||||
|
if (Settings.filestore.fallback.copyOnMiss) {
|
||||||
|
const missStream = new Stream.PassThrough()
|
||||||
|
pipeline(fallbackStream, missStream)
|
||||||
|
|
||||||
|
// copy from sourceKey -> sourceKey
|
||||||
|
_copyStreamFromFallbackAndVerify(
|
||||||
|
missStream,
|
||||||
|
fallbackBucket,
|
||||||
|
bucket,
|
||||||
|
sourceKey,
|
||||||
|
sourceKey
|
||||||
|
).then(() => {
|
||||||
|
// swallow errors, as this runs in the background and will log a warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// copy from sourceKey -> destKey
|
||||||
|
return _copyStreamFromFallbackAndVerify(
|
||||||
|
copyStream,
|
||||||
|
fallbackBucket,
|
||||||
|
bucket,
|
||||||
|
sourceKey,
|
||||||
|
destKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getFallbackBucket(bucket) {
|
||||||
|
return Settings.filestore.fallback.buckets[bucket]
|
||||||
|
}
|
||||||
|
|
||||||
|
function _wrapFallbackMethod(method) {
|
||||||
|
return async function(bucket, key, ...moreArgs) {
|
||||||
|
try {
|
||||||
|
return await primary.promises[method](bucket, key, ...moreArgs)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof NotFoundError) {
|
||||||
|
const fallbackBucket = _getFallbackBucket(bucket)
|
||||||
|
if (Settings.filestore.fallback.copyOnMiss) {
|
||||||
|
const fallbackStream = await fallback.promises.getFileStream(
|
||||||
|
fallbackBucket,
|
||||||
|
key,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
// run in background
|
||||||
|
_copyStreamFromFallbackAndVerify(
|
||||||
|
fallbackStream,
|
||||||
|
fallbackBucket,
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
key
|
||||||
|
).catch(err => {
|
||||||
|
logger.warn({ err }, 'failed to copy file from fallback')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return fallback.promises[method](fallbackBucket, key, ...moreArgs)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _copyStreamFromFallbackAndVerify(
|
||||||
|
stream,
|
||||||
|
sourceBucket,
|
||||||
|
destBucket,
|
||||||
|
sourceKey,
|
||||||
|
destKey
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
let sourceMd5
|
||||||
|
try {
|
||||||
|
sourceMd5 = await fallback.promises.getFileMd5Hash(
|
||||||
|
sourceBucket,
|
||||||
|
sourceKey
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(err, 'error getting md5 hash from fallback persistor')
|
||||||
|
}
|
||||||
|
|
||||||
|
await primary.promises.sendStream(destBucket, destKey, stream, sourceMd5)
|
||||||
|
} catch (err) {
|
||||||
|
const error = new WriteError({
|
||||||
|
message: 'unable to copy file to destination persistor',
|
||||||
|
info: {
|
||||||
|
sourceBucket,
|
||||||
|
destBucket,
|
||||||
|
sourceKey,
|
||||||
|
destKey
|
||||||
|
}
|
||||||
|
}).withCause(err)
|
||||||
|
metrics.inc('fallback.copy.failure')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await primary.promises.deleteFile(destBucket, destKey)
|
||||||
|
} catch (err) {
|
||||||
|
error.info.cleanupError = new WriteError({
|
||||||
|
message: 'unable to clean up destination copy artifact',
|
||||||
|
info: {
|
||||||
|
destBucket,
|
||||||
|
destKey
|
||||||
|
}
|
||||||
|
}).withCause(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn({ error }, 'failed to copy file from fallback')
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
primaryPersistor: primary,
|
||||||
|
fallbackPersistor: fallback,
|
||||||
|
sendFile: primary.sendFile,
|
||||||
|
sendStream: primary.sendStream,
|
||||||
|
getFileStream: callbackify(getFileStreamWithFallback),
|
||||||
|
getFileMd5Hash: callbackify(_wrapFallbackMethod('getFileMd5Hash')),
|
||||||
|
deleteDirectory: callbackify(
|
||||||
|
_wrapMethodOnBothPersistors('deleteDirectory')
|
||||||
|
),
|
||||||
|
getFileSize: callbackify(_wrapFallbackMethod('getFileSize')),
|
||||||
|
deleteFile: callbackify(_wrapMethodOnBothPersistors('deleteFile')),
|
||||||
|
copyFile: callbackify(copyFileWithFallback),
|
||||||
|
checkIfFileExists: callbackify(_wrapFallbackMethod('checkIfFileExists')),
|
||||||
|
directorySize: callbackify(_wrapFallbackMethod('directorySize')),
|
||||||
|
promises: {
|
||||||
|
sendFile: primary.promises.sendFile,
|
||||||
|
sendStream: primary.promises.sendStream,
|
||||||
|
getFileStream: getFileStreamWithFallback,
|
||||||
|
getFileMd5Hash: _wrapFallbackMethod('getFileMd5Hash'),
|
||||||
|
deleteDirectory: _wrapMethodOnBothPersistors('deleteDirectory'),
|
||||||
|
getFileSize: _wrapFallbackMethod('getFileSize'),
|
||||||
|
deleteFile: _wrapMethodOnBothPersistors('deleteFile'),
|
||||||
|
copyFile: copyFileWithFallback,
|
||||||
|
checkIfFileExists: _wrapFallbackMethod('checkIfFileExists'),
|
||||||
|
directorySize: _wrapFallbackMethod('directorySize')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
const crypto = require('crypto')
|
||||||
|
const meter = require('stream-meter')
|
||||||
|
const Stream = require('stream')
|
||||||
|
const logger = require('logger-sharelatex')
|
||||||
|
const { WriteError, ReadError, NotFoundError } = require('./Errors')
|
||||||
|
const { promisify } = require('util')
|
||||||
|
|
||||||
|
const pipeline = promisify(Stream.pipeline)
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
calculateStreamMd5,
|
||||||
|
verifyMd5,
|
||||||
|
getMeteredStream,
|
||||||
|
waitForStreamReady,
|
||||||
|
wrapError
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a promise which resolves with the md5 hash of the stream
|
||||||
|
function calculateStreamMd5(stream) {
|
||||||
|
const hash = crypto.createHash('md5')
|
||||||
|
hash.setEncoding('hex')
|
||||||
|
|
||||||
|
return pipeline(stream, hash).then(() => hash.read())
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifies the md5 hash of a file against the supplied md5 or the one stored in
|
||||||
|
// storage if not supplied - deletes the new file if the md5 does not match and
|
||||||
|
// throws an error
|
||||||
|
async function verifyMd5(persistor, bucket, key, sourceMd5, destMd5 = null) {
|
||||||
|
if (!destMd5) {
|
||||||
|
destMd5 = await persistor.promises.getFileMd5Hash(bucket, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceMd5 !== destMd5) {
|
||||||
|
try {
|
||||||
|
await persistor.promises.deleteFile(bucket, key)
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(err, 'error deleting file for invalid upload')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new WriteError({
|
||||||
|
message: 'source and destination hashes do not match',
|
||||||
|
info: {
|
||||||
|
sourceMd5,
|
||||||
|
destMd5,
|
||||||
|
bucket,
|
||||||
|
key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the next stream in the pipeline, and calls the callback with the byte count
|
||||||
|
// when the stream finishes or receives an error
|
||||||
|
function getMeteredStream(stream, callback) {
|
||||||
|
const meteredStream = meter()
|
||||||
|
|
||||||
|
pipeline(stream, meteredStream)
|
||||||
|
.then(() => {
|
||||||
|
callback(null, meteredStream.bytes)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
// on error, just send how many bytes we received before the stream stopped
|
||||||
|
callback(err, meteredStream.bytes)
|
||||||
|
})
|
||||||
|
|
||||||
|
return meteredStream
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolves when a stream is 'readable', or rejects if the stream throws an error
|
||||||
|
// before that happens - this lets us handle protocol-level errors before trying
|
||||||
|
// to read them
|
||||||
|
function waitForStreamReady(stream) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onError = function(err) {
|
||||||
|
reject(wrapError(err, 'error before stream became ready', {}, ReadError))
|
||||||
|
}
|
||||||
|
const onStreamReady = function() {
|
||||||
|
stream.removeListener('readable', onStreamReady)
|
||||||
|
stream.removeListener('error', onError)
|
||||||
|
resolve(stream)
|
||||||
|
}
|
||||||
|
stream.on('readable', onStreamReady)
|
||||||
|
stream.on('error', onError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapError(error, message, params, ErrorType) {
|
||||||
|
if (
|
||||||
|
error instanceof NotFoundError ||
|
||||||
|
['NoSuchKey', 'NotFound', 404, 'AccessDenied', 'ENOENT'].includes(
|
||||||
|
error.code
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return new NotFoundError({
|
||||||
|
message: 'no such file',
|
||||||
|
info: params
|
||||||
|
}).withCause(error)
|
||||||
|
} else {
|
||||||
|
return new ErrorType({
|
||||||
|
message: message,
|
||||||
|
info: params
|
||||||
|
}).withCause(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ const logger = require('logger-sharelatex')
|
|||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
{
|
{
|
||||||
backend: settings.filestore.backend
|
backend: settings.filestore.backend,
|
||||||
|
fallback: settings.filestore.fallback && settings.filestore.fallback.backend
|
||||||
},
|
},
|
||||||
'Loading backend'
|
'Loading backend'
|
||||||
)
|
)
|
||||||
@@ -11,14 +12,26 @@ if (!settings.filestore.backend) {
|
|||||||
throw new Error('no backend specified - config incomplete')
|
throw new Error('no backend specified - config incomplete')
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (settings.filestore.backend) {
|
function getPersistor(backend) {
|
||||||
case 'aws-sdk':
|
switch (backend) {
|
||||||
case 's3':
|
case 'aws-sdk':
|
||||||
module.exports = require('./S3PersistorManager')
|
case 's3':
|
||||||
break
|
return require('./S3Persistor')
|
||||||
case 'fs':
|
case 'fs':
|
||||||
module.exports = require('./FSPersistorManager')
|
return require('./FSPersistor')
|
||||||
break
|
default:
|
||||||
default:
|
throw new Error(`unknown filestore backend: ${backend}`)
|
||||||
throw new Error(`unknown filestore backend: ${settings.filestore.backend}`)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let persistor = getPersistor(settings.filestore.backend)
|
||||||
|
|
||||||
|
if (settings.filestore.fallback && settings.filestore.fallback.backend) {
|
||||||
|
const migrationPersistor = require('./MigrationPersistor')
|
||||||
|
persistor = migrationPersistor(
|
||||||
|
persistor,
|
||||||
|
getPersistor(settings.filestore.fallback.backend)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = persistor
|
||||||
|
|||||||
+126
-55
@@ -6,7 +6,8 @@ https.globalAgent.maxSockets = 300
|
|||||||
const settings = require('settings-sharelatex')
|
const settings = require('settings-sharelatex')
|
||||||
const metrics = require('metrics-sharelatex')
|
const metrics = require('metrics-sharelatex')
|
||||||
|
|
||||||
const meter = require('stream-meter')
|
const PersistorHelper = require('./PersistorHelper')
|
||||||
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const S3 = require('aws-sdk/clients/s3')
|
const S3 = require('aws-sdk/clients/s3')
|
||||||
const { URL } = require('url')
|
const { URL } = require('url')
|
||||||
@@ -18,10 +19,11 @@ const {
|
|||||||
SettingsError
|
SettingsError
|
||||||
} = require('./Errors')
|
} = require('./Errors')
|
||||||
|
|
||||||
module.exports = {
|
const S3Persistor = {
|
||||||
sendFile: callbackify(sendFile),
|
sendFile: callbackify(sendFile),
|
||||||
sendStream: callbackify(sendStream),
|
sendStream: callbackify(sendStream),
|
||||||
getFileStream: callbackify(getFileStream),
|
getFileStream: callbackify(getFileStream),
|
||||||
|
getFileMd5Hash: callbackify(getFileMd5Hash),
|
||||||
deleteDirectory: callbackify(deleteDirectory),
|
deleteDirectory: callbackify(deleteDirectory),
|
||||||
getFileSize: callbackify(getFileSize),
|
getFileSize: callbackify(getFileSize),
|
||||||
deleteFile: callbackify(deleteFile),
|
deleteFile: callbackify(deleteFile),
|
||||||
@@ -32,6 +34,7 @@ module.exports = {
|
|||||||
sendFile,
|
sendFile,
|
||||||
sendStream,
|
sendStream,
|
||||||
getFileStream,
|
getFileStream,
|
||||||
|
getFileMd5Hash,
|
||||||
deleteDirectory,
|
deleteDirectory,
|
||||||
getFileSize,
|
getFileSize,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
@@ -41,12 +44,18 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = S3Persistor
|
||||||
|
|
||||||
|
function hexToBase64(hex) {
|
||||||
|
return Buffer.from(hex, 'hex').toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
async function sendFile(bucketName, key, fsPath) {
|
async function sendFile(bucketName, key, fsPath) {
|
||||||
let readStream
|
let readStream
|
||||||
try {
|
try {
|
||||||
readStream = fs.createReadStream(fsPath)
|
readStream = fs.createReadStream(fsPath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'error reading file from disk',
|
'error reading file from disk',
|
||||||
{ bucketName, key, fsPath },
|
{ bucketName, key, fsPath },
|
||||||
@@ -56,22 +65,65 @@ async function sendFile(bucketName, key, fsPath) {
|
|||||||
return sendStream(bucketName, key, readStream)
|
return sendStream(bucketName, key, readStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendStream(bucketName, key, readStream) {
|
async function sendStream(bucketName, key, readStream, sourceMd5) {
|
||||||
try {
|
try {
|
||||||
const meteredStream = meter()
|
// if there is no supplied md5 hash, we calculate the hash as the data passes through
|
||||||
meteredStream.on('finish', () => {
|
let hashPromise
|
||||||
metrics.count('s3.egress', meteredStream.bytes)
|
let b64Hash
|
||||||
})
|
|
||||||
|
|
||||||
await _getClientForBucket(bucketName)
|
if (sourceMd5) {
|
||||||
.upload({
|
b64Hash = hexToBase64(sourceMd5)
|
||||||
Bucket: bucketName,
|
} else {
|
||||||
Key: key,
|
hashPromise = PersistorHelper.calculateStreamMd5(readStream)
|
||||||
Body: readStream.pipe(meteredStream)
|
}
|
||||||
})
|
|
||||||
|
const meteredStream = PersistorHelper.getMeteredStream(
|
||||||
|
readStream,
|
||||||
|
(_, byteCount) => {
|
||||||
|
// ignore the error parameter and just log the byte count
|
||||||
|
metrics.count('s3.egress', byteCount)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// if we have an md5 hash, pass this to S3 to verify the upload
|
||||||
|
const uploadOptions = {
|
||||||
|
Bucket: bucketName,
|
||||||
|
Key: key,
|
||||||
|
Body: meteredStream
|
||||||
|
}
|
||||||
|
if (b64Hash) {
|
||||||
|
uploadOptions.ContentMD5 = b64Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await _getClientForBucket(bucketName)
|
||||||
|
.upload(uploadOptions, { partSize: settings.filestore.s3.partSize })
|
||||||
.promise()
|
.promise()
|
||||||
|
let destMd5 = _md5FromResponse(response)
|
||||||
|
if (!destMd5) {
|
||||||
|
// the eTag isn't in md5 format so we need to calculate it ourselves
|
||||||
|
const verifyStream = await getFileStream(
|
||||||
|
response.Bucket,
|
||||||
|
response.Key,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
destMd5 = await PersistorHelper.calculateStreamMd5(verifyStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we didn't have an md5 hash, we should compare our computed one with S3's
|
||||||
|
// as we couldn't tell S3 about it beforehand
|
||||||
|
if (hashPromise) {
|
||||||
|
sourceMd5 = await hashPromise
|
||||||
|
// throws on mismatch
|
||||||
|
await PersistorHelper.verifyMd5(
|
||||||
|
S3Persistor,
|
||||||
|
bucketName,
|
||||||
|
key,
|
||||||
|
sourceMd5,
|
||||||
|
destMd5
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'upload to S3 failed',
|
'upload to S3 failed',
|
||||||
{ bucketName, key },
|
{ bucketName, key },
|
||||||
@@ -91,25 +143,29 @@ async function getFileStream(bucketName, key, opts) {
|
|||||||
params.Range = `bytes=${opts.start}-${opts.end}`
|
params.Range = `bytes=${opts.start}-${opts.end}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
const stream = _getClientForBucket(bucketName)
|
||||||
const stream = _getClientForBucket(bucketName)
|
.getObject(params)
|
||||||
.getObject(params)
|
.createReadStream()
|
||||||
.createReadStream()
|
|
||||||
|
|
||||||
const meteredStream = meter()
|
const meteredStream = PersistorHelper.getMeteredStream(
|
||||||
meteredStream.on('finish', () => {
|
stream,
|
||||||
metrics.count('s3.ingress', meteredStream.bytes)
|
(_, byteCount) => {
|
||||||
})
|
// ignore the error parameter and just log the byte count
|
||||||
|
metrics.count('s3.ingress', byteCount)
|
||||||
const onStreamReady = function() {
|
|
||||||
stream.removeListener('readable', onStreamReady)
|
|
||||||
resolve(stream.pipe(meteredStream))
|
|
||||||
}
|
}
|
||||||
stream.on('readable', onStreamReady)
|
)
|
||||||
stream.on('error', err => {
|
|
||||||
reject(_wrapError(err, 'error reading from S3', params, ReadError))
|
try {
|
||||||
})
|
await PersistorHelper.waitForStreamReady(stream)
|
||||||
})
|
return meteredStream
|
||||||
|
} catch (err) {
|
||||||
|
throw PersistorHelper.wrapError(
|
||||||
|
err,
|
||||||
|
'error reading file from S3',
|
||||||
|
{ bucketName, key, opts },
|
||||||
|
ReadError
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteDirectory(bucketName, key) {
|
async function deleteDirectory(bucketName, key) {
|
||||||
@@ -120,7 +176,7 @@ async function deleteDirectory(bucketName, key) {
|
|||||||
.listObjects({ Bucket: bucketName, Prefix: key })
|
.listObjects({ Bucket: bucketName, Prefix: key })
|
||||||
.promise()
|
.promise()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to list objects in S3',
|
'failed to list objects in S3',
|
||||||
{ bucketName, key },
|
{ bucketName, key },
|
||||||
@@ -141,7 +197,7 @@ async function deleteDirectory(bucketName, key) {
|
|||||||
})
|
})
|
||||||
.promise()
|
.promise()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to delete objects in S3',
|
'failed to delete objects in S3',
|
||||||
{ bucketName, key },
|
{ bucketName, key },
|
||||||
@@ -158,7 +214,7 @@ async function getFileSize(bucketName, key) {
|
|||||||
.promise()
|
.promise()
|
||||||
return response.ContentLength
|
return response.ContentLength
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'error getting size of s3 object',
|
'error getting size of s3 object',
|
||||||
{ bucketName, key },
|
{ bucketName, key },
|
||||||
@@ -167,13 +223,30 @@ async function getFileSize(bucketName, key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getFileMd5Hash(bucketName, key) {
|
||||||
|
try {
|
||||||
|
const response = await _getClientForBucket(bucketName)
|
||||||
|
.headObject({ Bucket: bucketName, Key: key })
|
||||||
|
.promise()
|
||||||
|
return _md5FromResponse(response)
|
||||||
|
} catch (err) {
|
||||||
|
throw PersistorHelper.wrapError(
|
||||||
|
err,
|
||||||
|
'error getting hash of s3 object',
|
||||||
|
{ bucketName, key },
|
||||||
|
ReadError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteFile(bucketName, key) {
|
async function deleteFile(bucketName, key) {
|
||||||
try {
|
try {
|
||||||
await _getClientForBucket(bucketName)
|
await _getClientForBucket(bucketName)
|
||||||
.deleteObject({ Bucket: bucketName, Key: key })
|
.deleteObject({ Bucket: bucketName, Key: key })
|
||||||
.promise()
|
.promise()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
// s3 does not give us a NotFoundError here
|
||||||
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'failed to delete file in S3',
|
'failed to delete file in S3',
|
||||||
{ bucketName, key },
|
{ bucketName, key },
|
||||||
@@ -193,7 +266,12 @@ async function copyFile(bucketName, sourceKey, destKey) {
|
|||||||
.copyObject(params)
|
.copyObject(params)
|
||||||
.promise()
|
.promise()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(err, 'failed to copy file in S3', params, WriteError)
|
throw PersistorHelper.wrapError(
|
||||||
|
err,
|
||||||
|
'failed to copy file in S3',
|
||||||
|
params,
|
||||||
|
WriteError
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +283,7 @@ async function checkIfFileExists(bucketName, key) {
|
|||||||
if (err instanceof NotFoundError) {
|
if (err instanceof NotFoundError) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'error checking whether S3 object exists',
|
'error checking whether S3 object exists',
|
||||||
{ bucketName, key },
|
{ bucketName, key },
|
||||||
@@ -222,7 +300,7 @@ async function directorySize(bucketName, key) {
|
|||||||
|
|
||||||
return response.Contents.reduce((acc, item) => item.Size + acc, 0)
|
return response.Contents.reduce((acc, item) => item.Size + acc, 0)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw _wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
'error getting directory size in S3',
|
'error getting directory size in S3',
|
||||||
{ bucketName, key },
|
{ bucketName, key },
|
||||||
@@ -231,22 +309,6 @@ async function directorySize(bucketName, key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _wrapError(error, message, params, ErrorType) {
|
|
||||||
if (
|
|
||||||
['NoSuchKey', 'NotFound', 'AccessDenied', 'ENOENT'].includes(error.code)
|
|
||||||
) {
|
|
||||||
return new NotFoundError({
|
|
||||||
message: 'no such file',
|
|
||||||
info: params
|
|
||||||
}).withCause(error)
|
|
||||||
} else {
|
|
||||||
return new ErrorType({
|
|
||||||
message: message,
|
|
||||||
info: params
|
|
||||||
}).withCause(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _clients = new Map()
|
const _clients = new Map()
|
||||||
let _defaultClient
|
let _defaultClient
|
||||||
|
|
||||||
@@ -309,3 +371,12 @@ function _buildClientOptions(bucketCredentials) {
|
|||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _md5FromResponse(response) {
|
||||||
|
const md5 = (response.ETag || '').replace(/[ "]/g, '')
|
||||||
|
if (!md5.match(/^[a-f0-9]{32}$/)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return md5
|
||||||
|
}
|
||||||
@@ -7,6 +7,19 @@ if process.env['AWS_KEY'] && !process.env['AWS_ACCESS_KEY_ID']
|
|||||||
if process.env['AWS_SECRET'] && !process.env['AWS_SECRET_ACCESS_KEY']
|
if process.env['AWS_SECRET'] && !process.env['AWS_SECRET_ACCESS_KEY']
|
||||||
process.env['AWS_SECRET_ACCESS_KEY'] = process.env['AWS_SECRET']
|
process.env['AWS_SECRET_ACCESS_KEY'] = process.env['AWS_SECRET']
|
||||||
|
|
||||||
|
# pre-backend setting, fall back to old behaviour
|
||||||
|
unless process.env['BACKEND']?
|
||||||
|
if process.env['AWS_ACCESS_KEY_ID']? or process.env['S3_BUCKET_CREDENTIALS']?
|
||||||
|
process.env['BACKEND'] = "s3"
|
||||||
|
process.env['USER_FILES_BUCKET_NAME'] = process.env['AWS_S3_USER_FILES_BUCKET_NAME']
|
||||||
|
process.env['TEMPLATE_FILES_BUCKET_NAME'] = process.env['AWS_S3_TEMPLATE_FILES_BUCKET_NAME']
|
||||||
|
process.env['PUBLIC_FILES_BUCKET_NAME'] = process.env['AWS_S3_PUBLIC_FILES_BUCKET_NAME']
|
||||||
|
else
|
||||||
|
process.env['BACKEND'] = "fs"
|
||||||
|
process.env['USER_FILES_BUCKET_NAME'] = Path.resolve(__dirname + "/../user_files")
|
||||||
|
process.env['TEMPLATE_FILES_BUCKET_NAME'] = Path.resolve(__dirname + "/../template_files")
|
||||||
|
process.env['PUBLIC_FILES_BUCKET_NAME'] = Path.resolve(__dirname + "/../public_files")
|
||||||
|
|
||||||
settings =
|
settings =
|
||||||
internal:
|
internal:
|
||||||
filestore:
|
filestore:
|
||||||
@@ -18,38 +31,29 @@ settings =
|
|||||||
# Choices are
|
# Choices are
|
||||||
# s3 - Amazon S3
|
# s3 - Amazon S3
|
||||||
# fs - local filesystem
|
# fs - local filesystem
|
||||||
if process.env['AWS_ACCESS_KEY_ID']? or process.env['S3_BUCKET_CREDENTIALS']?
|
backend: process.env['BACKEND']
|
||||||
backend: "s3"
|
|
||||||
s3:
|
s3:
|
||||||
|
if process.env['AWS_ACCESS_KEY_ID']? or process.env['S3_BUCKET_CREDENTIALS']?
|
||||||
key: process.env['AWS_ACCESS_KEY_ID']
|
key: process.env['AWS_ACCESS_KEY_ID']
|
||||||
secret: process.env['AWS_SECRET_ACCESS_KEY']
|
secret: process.env['AWS_SECRET_ACCESS_KEY']
|
||||||
endpoint: process.env['AWS_S3_ENDPOINT']
|
endpoint: process.env['AWS_S3_ENDPOINT']
|
||||||
stores:
|
partSize: process.env['AWS_S3_PARTSIZE'] or (100 * 1024 * 1024)
|
||||||
user_files: process.env['AWS_S3_USER_FILES_BUCKET_NAME']
|
|
||||||
template_files: process.env['AWS_S3_TEMPLATE_FILES_BUCKET_NAME']
|
stores:
|
||||||
public_files: process.env['AWS_S3_PUBLIC_FILES_BUCKET_NAME']
|
user_files: process.env['USER_FILES_BUCKET_NAME']
|
||||||
# if you are using S3, then fill in your S3 details below,
|
template_files: process.env['TEMPLATE_FILES_BUCKET_NAME']
|
||||||
# or use env var with the same structure.
|
public_files: process.env['PUBLIC_FILES_BUCKET_NAME']
|
||||||
# s3:
|
|
||||||
# key: "" # default
|
s3BucketCreds: JSON.parse process.env['S3_BUCKET_CREDENTIALS'] if process.env['S3_BUCKET_CREDENTIALS']?
|
||||||
# secret: "" # default
|
|
||||||
#
|
fallback:
|
||||||
# s3BucketCreds:
|
if process.env['FALLBACK_BACKEND']?
|
||||||
# bucketname1: # secrets for bucketname1
|
backend: process.env['FALLBACK_BACKEND']
|
||||||
# auth_key: ""
|
# mapping of bucket names on the fallback, to bucket names on the primary.
|
||||||
# auth_secret: ""
|
# e.g. { myS3UserFilesBucketName: 'myGoogleUserFilesBucketName' }
|
||||||
# bucketname2: # secrets for bucketname2...
|
buckets: JSON.parse(process.env['FALLBACK_BUCKET_MAPPING'] || '{}')
|
||||||
s3BucketCreds: JSON.parse process.env['S3_BUCKET_CREDENTIALS'] if process.env['S3_BUCKET_CREDENTIALS']?
|
copyOnMiss: process.env['COPY_ON_MISS'] == 'true'
|
||||||
else
|
|
||||||
backend: "fs"
|
|
||||||
stores:
|
|
||||||
#
|
|
||||||
# For local filesystem this is the directory to store the files in.
|
|
||||||
# Must contain full path, e.g. "/var/lib/sharelatex/data".
|
|
||||||
# This path must exist, not be tmpfs and be writable to by the user sharelatex is run as.
|
|
||||||
user_files: Path.resolve(__dirname + "/../user_files")
|
|
||||||
public_files: Path.resolve(__dirname + "/../public_files")
|
|
||||||
template_files: Path.resolve(__dirname + "/../template_files")
|
|
||||||
|
|
||||||
path:
|
path:
|
||||||
uploadFolder: Path.resolve(__dirname + "/../uploads")
|
uploadFolder: Path.resolve(__dirname + "/../uploads")
|
||||||
|
|||||||
Generated
+6
-276
@@ -586,11 +586,6 @@
|
|||||||
"event-target-shim": "^5.0.0"
|
"event-target-shim": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"accept-encoding": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/accept-encoding/-/accept-encoding-0.1.0.tgz",
|
|
||||||
"integrity": "sha1-XdiLjfcfHcLlzGuVZezOHjmaMz4="
|
|
||||||
},
|
|
||||||
"accepts": {
|
"accepts": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
|
||||||
@@ -770,11 +765,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aws-sign": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/aws-sign/-/aws-sign-0.2.1.tgz",
|
|
||||||
"integrity": "sha1-uWGyLwuqTxXsJBFA83dtbBQoVtA="
|
|
||||||
},
|
|
||||||
"aws-sign2": {
|
"aws-sign2": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||||
@@ -837,14 +827,6 @@
|
|||||||
"tweetnacl": "^0.14.3"
|
"tweetnacl": "^0.14.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"best-encoding": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/best-encoding/-/best-encoding-0.1.1.tgz",
|
|
||||||
"integrity": "sha1-GVIT2rysBFgYuAe3ox+Dn63cl04=",
|
|
||||||
"requires": {
|
|
||||||
"accept-encoding": "~0.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bignumber.js": {
|
"bignumber.js": {
|
||||||
"version": "7.2.1",
|
"version": "7.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
|
||||||
@@ -863,14 +845,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
|
||||||
"integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
|
"integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
|
||||||
},
|
},
|
||||||
"bl": {
|
|
||||||
"version": "0.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-0.7.0.tgz",
|
|
||||||
"integrity": "sha1-P7BnBgKsKHjrdw3CA58YNr5irls=",
|
|
||||||
"requires": {
|
|
||||||
"readable-stream": "~1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"body-parser": {
|
"body-parser": {
|
||||||
"version": "1.18.3",
|
"version": "1.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
|
||||||
@@ -894,14 +868,6 @@
|
|||||||
"integrity": "sha1-tcCeF8rNET0Rt7s+04TMASmU2Gs=",
|
"integrity": "sha1-tcCeF8rNET0Rt7s+04TMASmU2Gs=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"boom": {
|
|
||||||
"version": "0.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/boom/-/boom-0.3.8.tgz",
|
|
||||||
"integrity": "sha1-yM2wQUNZEnQWKMBE7Mcy0dF8Ceo=",
|
|
||||||
"requires": {
|
|
||||||
"hoek": "0.7.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@@ -975,11 +941,6 @@
|
|||||||
"quick-lru": "^4.0.1"
|
"quick-lru": "^4.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caseless": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.3.0.tgz",
|
|
||||||
"integrity": "sha1-U06XkWOH07cGtk/eu6xGQ4RQk08="
|
|
||||||
},
|
|
||||||
"chai": {
|
"chai": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
|
||||||
@@ -1097,14 +1058,6 @@
|
|||||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"combined-stream": {
|
|
||||||
"version": "0.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
|
|
||||||
"integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=",
|
|
||||||
"requires": {
|
|
||||||
"delayed-stream": "0.0.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"common-tags": {
|
"common-tags": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
|
||||||
@@ -1151,11 +1104,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
|
||||||
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
|
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
|
||||||
},
|
},
|
||||||
"cookie-jar": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie-jar/-/cookie-jar-0.2.0.tgz",
|
|
||||||
"integrity": "sha1-ZOzAasl423leS1KQy+SLo3gUAPo="
|
|
||||||
},
|
|
||||||
"cookie-signature": {
|
"cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
@@ -1193,14 +1141,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cryptiles": {
|
|
||||||
"version": "0.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.1.3.tgz",
|
|
||||||
"integrity": "sha1-GlVnNPBtJLo0hirpy55wmjr7/xw=",
|
|
||||||
"requires": {
|
|
||||||
"boom": "0.3.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dashdash": {
|
"dashdash": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||||
@@ -1252,11 +1192,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz",
|
||||||
"integrity": "sha1-7+6/uPVFV5yzlrOnIkQ+yW0UxQ4="
|
"integrity": "sha1-7+6/uPVFV5yzlrOnIkQ+yW0UxQ4="
|
||||||
},
|
},
|
||||||
"delayed-stream": {
|
|
||||||
"version": "0.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz",
|
|
||||||
"integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8="
|
|
||||||
},
|
|
||||||
"depd": {
|
"depd": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||||
@@ -2052,28 +1987,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"forever-agent": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.2.0.tgz",
|
|
||||||
"integrity": "sha1-4cJcetROCcOPIzh2x2/MJP+EOx8="
|
|
||||||
},
|
|
||||||
"form-data": {
|
|
||||||
"version": "0.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-0.0.10.tgz",
|
|
||||||
"integrity": "sha1-2zRaU3jYau6x7V1VO4aawZLS9e0=",
|
|
||||||
"requires": {
|
|
||||||
"async": "~0.2.7",
|
|
||||||
"combined-stream": "~0.0.4",
|
|
||||||
"mime": "~1.2.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"mime": {
|
|
||||||
"version": "1.2.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz",
|
|
||||||
"integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"forwarded": {
|
"forwarded": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||||
@@ -2297,17 +2210,6 @@
|
|||||||
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
|
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"hawk": {
|
|
||||||
"version": "0.10.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/hawk/-/hawk-0.10.2.tgz",
|
|
||||||
"integrity": "sha1-mzYd7pWpMWQObVBOBWCaj8OsRdI=",
|
|
||||||
"requires": {
|
|
||||||
"boom": "0.3.x",
|
|
||||||
"cryptiles": "0.1.x",
|
|
||||||
"hoek": "0.7.x",
|
|
||||||
"sntp": "0.1.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"he": {
|
"he": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
||||||
@@ -2323,11 +2225,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.2.tgz",
|
||||||
"integrity": "sha1-jhzkvvNqdPfVcjw/swkMKGAHczg="
|
"integrity": "sha1-jhzkvvNqdPfVcjw/swkMKGAHczg="
|
||||||
},
|
},
|
||||||
"hoek": {
|
|
||||||
"version": "0.7.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-0.7.6.tgz",
|
|
||||||
"integrity": "sha1-YPvZBFV1Qc0rh5Wr8wihs3cOFVo="
|
|
||||||
},
|
|
||||||
"hosted-git-info": {
|
"hosted-git-info": {
|
||||||
"version": "2.8.5",
|
"version": "2.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz",
|
||||||
@@ -2667,28 +2564,6 @@
|
|||||||
"graceful-fs": "^4.1.9"
|
"graceful-fs": "^4.1.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"knox": {
|
|
||||||
"version": "0.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/knox/-/knox-0.9.2.tgz",
|
|
||||||
"integrity": "sha1-NzZZNmniTwJP2vcjtqHcSv2DmnE=",
|
|
||||||
"requires": {
|
|
||||||
"debug": "^1.0.2",
|
|
||||||
"mime": "*",
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"stream-counter": "^1.0.0",
|
|
||||||
"xml2js": "^0.4.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"debug": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-1.0.5.tgz",
|
|
||||||
"integrity": "sha1-9yQSF0MPmd7EwrRz6rkiKOh0wqw=",
|
|
||||||
"requires": {
|
|
||||||
"ms": "2.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"levn": {
|
"levn": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
|
||||||
@@ -3318,55 +3193,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.4.tgz",
|
||||||
"integrity": "sha1-1nOGYrZhvhnicR7wGqOxghLxMDA="
|
"integrity": "sha1-1nOGYrZhvhnicR7wGqOxghLxMDA="
|
||||||
},
|
},
|
||||||
"node-transloadit": {
|
|
||||||
"version": "0.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-transloadit/-/node-transloadit-0.0.4.tgz",
|
|
||||||
"integrity": "sha1-4ZoHheON94NblO2AANHjXmg7zsE=",
|
|
||||||
"requires": {
|
|
||||||
"request": "~2.16.6",
|
|
||||||
"underscore": "1.2.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"json-stringify-safe": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-3.0.0.tgz",
|
|
||||||
"integrity": "sha1-nbew5TDH8onF6MhDKvGRwv91pbM="
|
|
||||||
},
|
|
||||||
"mime": {
|
|
||||||
"version": "1.2.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz",
|
|
||||||
"integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA="
|
|
||||||
},
|
|
||||||
"qs": {
|
|
||||||
"version": "0.5.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz",
|
|
||||||
"integrity": "sha1-MbGtBYVnZRxSaSFQa5qHk5EaA4Q="
|
|
||||||
},
|
|
||||||
"request": {
|
|
||||||
"version": "2.16.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/request/-/request-2.16.6.tgz",
|
|
||||||
"integrity": "sha1-hy/kRa5y3iZrN4edatfclI+gHK0=",
|
|
||||||
"requires": {
|
|
||||||
"aws-sign": "~0.2.0",
|
|
||||||
"cookie-jar": "~0.2.0",
|
|
||||||
"forever-agent": "~0.2.0",
|
|
||||||
"form-data": "~0.0.3",
|
|
||||||
"hawk": "~0.10.2",
|
|
||||||
"json-stringify-safe": "~3.0.0",
|
|
||||||
"mime": "~1.2.7",
|
|
||||||
"node-uuid": "~1.4.0",
|
|
||||||
"oauth-sign": "~0.2.0",
|
|
||||||
"qs": "~0.5.4",
|
|
||||||
"tunnel-agent": "~0.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"underscore": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.2.1.tgz",
|
|
||||||
"integrity": "sha1-/FxrB2VnPZKi1KyLTcCqiHAuK9Q="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node-uuid": {
|
"node-uuid": {
|
||||||
"version": "1.4.8",
|
"version": "1.4.8",
|
||||||
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
|
||||||
@@ -3392,11 +3218,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth-sign": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.2.0.tgz",
|
|
||||||
"integrity": "sha1-oOahcV2u0GLzIrYit/5a/RA1tuI="
|
|
||||||
},
|
|
||||||
"object-inspect": {
|
"object-inspect": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
|
||||||
@@ -4399,29 +4220,6 @@
|
|||||||
"read-pkg": "^2.0.0"
|
"read-pkg": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"readable-stream": {
|
|
||||||
"version": "1.0.34",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
|
|
||||||
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
|
|
||||||
"requires": {
|
|
||||||
"core-util-is": "~1.0.0",
|
|
||||||
"inherits": "~2.0.1",
|
|
||||||
"isarray": "0.0.1",
|
|
||||||
"string_decoder": "~0.10.x"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"isarray": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
|
||||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"recluster": {
|
|
||||||
"version": "0.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/recluster/-/recluster-0.3.7.tgz",
|
|
||||||
"integrity": "sha1-aKRx3ZC2obl3ZjTPdpZAWutWeJU="
|
|
||||||
},
|
|
||||||
"regexpp": {
|
"regexpp": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
|
||||||
@@ -4594,24 +4392,6 @@
|
|||||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"response": {
|
|
||||||
"version": "0.14.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/response/-/response-0.14.0.tgz",
|
|
||||||
"integrity": "sha1-BmNS/z5rAm0EdYCUB2Y7Rob9JpY=",
|
|
||||||
"requires": {
|
|
||||||
"best-encoding": "^0.1.1",
|
|
||||||
"bl": "~0.7.0",
|
|
||||||
"caseless": "^0.3.0",
|
|
||||||
"mime": "~1.2.11"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"mime": {
|
|
||||||
"version": "1.2.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz",
|
|
||||||
"integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"restore-cursor": {
|
"restore-cursor": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
|
||||||
@@ -4826,14 +4606,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sntp": {
|
|
||||||
"version": "0.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-0.1.4.tgz",
|
|
||||||
"integrity": "sha1-XvSBuVGnspr/30r9fyaDj8ESD4Q=",
|
|
||||||
"requires": {
|
|
||||||
"hoek": "0.7.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"source-map": {
|
"source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -4921,49 +4693,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||||
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
|
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
|
||||||
},
|
},
|
||||||
"stream-browserify": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
|
|
||||||
"integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
|
|
||||||
"requires": {
|
|
||||||
"inherits": "~2.0.1",
|
|
||||||
"readable-stream": "^2.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"readable-stream": {
|
|
||||||
"version": "2.3.6",
|
|
||||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
|
||||||
"integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=",
|
|
||||||
"requires": {
|
|
||||||
"core-util-is": "~1.0.0",
|
|
||||||
"inherits": "~2.0.3",
|
|
||||||
"isarray": "~1.0.0",
|
|
||||||
"process-nextick-args": "~2.0.0",
|
|
||||||
"safe-buffer": "~5.1.1",
|
|
||||||
"string_decoder": "~1.1.1",
|
|
||||||
"util-deprecate": "~1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"string_decoder": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
|
||||||
"integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
|
|
||||||
"requires": {
|
|
||||||
"safe-buffer": "~5.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stream-buffers": {
|
"stream-buffers": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-0.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-0.2.6.tgz",
|
||||||
"integrity": "sha1-GBwI1bs2kARfaUAbmuanoM8zE/w="
|
"integrity": "sha1-GBwI1bs2kARfaUAbmuanoM8zE/w="
|
||||||
},
|
},
|
||||||
"stream-counter": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/stream-counter/-/stream-counter-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-kc8lac5NxQYf6816yyY5SloRR1E="
|
|
||||||
},
|
|
||||||
"stream-meter": {
|
"stream-meter": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz",
|
||||||
@@ -5001,6 +4735,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
|
||||||
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI="
|
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI="
|
||||||
},
|
},
|
||||||
|
"streamifier": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz",
|
||||||
|
"integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"string-width": {
|
"string-width": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||||
@@ -5042,11 +4782,6 @@
|
|||||||
"function-bind": "^1.1.1"
|
"function-bind": "^1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"string_decoder": {
|
|
||||||
"version": "0.10.31",
|
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
|
||||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
|
||||||
},
|
|
||||||
"strip-ansi": {
|
"strip-ansi": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||||
@@ -5240,11 +4975,6 @@
|
|||||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
|
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"tunnel-agent": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.2.0.tgz",
|
|
||||||
"integrity": "sha1-aFPCr7GyEJ5FYp5JK9419Fnqaeg="
|
|
||||||
},
|
|
||||||
"tweetnacl": {
|
"tweetnacl": {
|
||||||
"version": "0.14.5",
|
"version": "0.14.5",
|
||||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
|
|||||||
@@ -28,21 +28,16 @@
|
|||||||
"fs-extra": "^1.0.0",
|
"fs-extra": "^1.0.0",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"heapdump": "^0.3.2",
|
"heapdump": "^0.3.2",
|
||||||
"knox": "~0.9.1",
|
|
||||||
"logger-sharelatex": "^1.7.0",
|
"logger-sharelatex": "^1.7.0",
|
||||||
"metrics-sharelatex": "^2.2.0",
|
"metrics-sharelatex": "^2.2.0",
|
||||||
"mocha": "5.2.0",
|
"mocha": "5.2.0",
|
||||||
"node-transloadit": "0.0.4",
|
|
||||||
"node-uuid": "~1.4.1",
|
"node-uuid": "~1.4.1",
|
||||||
"pngcrush": "0.0.3",
|
"pngcrush": "0.0.3",
|
||||||
"range-parser": "^1.0.2",
|
"range-parser": "^1.0.2",
|
||||||
"recluster": "^0.3.7",
|
|
||||||
"request": "^2.88.0",
|
"request": "^2.88.0",
|
||||||
"request-promise-native": "^1.0.8",
|
"request-promise-native": "^1.0.8",
|
||||||
"response": "0.14.0",
|
|
||||||
"rimraf": "2.2.8",
|
"rimraf": "2.2.8",
|
||||||
"settings-sharelatex": "^1.1.0",
|
"settings-sharelatex": "^1.1.0",
|
||||||
"stream-browserify": "^2.0.1",
|
|
||||||
"stream-buffers": "~0.2.5",
|
"stream-buffers": "~0.2.5",
|
||||||
"stream-meter": "^1.0.4",
|
"stream-meter": "^1.0.4",
|
||||||
"underscore": "~1.5.2"
|
"underscore": "~1.5.2"
|
||||||
@@ -68,6 +63,7 @@
|
|||||||
"prettier-eslint-cli": "^5.0.0",
|
"prettier-eslint-cli": "^5.0.0",
|
||||||
"sandboxed-module": "2.0.3",
|
"sandboxed-module": "2.0.3",
|
||||||
"sinon": "7.1.1",
|
"sinon": "7.1.1",
|
||||||
"sinon-chai": "^3.3.0"
|
"sinon-chai": "^3.3.0",
|
||||||
|
"streamifier": "^0.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class FilestoreApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.initing = false
|
this.initing = false
|
||||||
|
this.persistor = require('../../../app/js/PersistorManager')
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForInit() {
|
async waitForInit() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const S3 = require('aws-sdk/clients/s3')
|
|||||||
const Stream = require('stream')
|
const Stream = require('stream')
|
||||||
const request = require('request')
|
const request = require('request')
|
||||||
const { promisify } = require('util')
|
const { promisify } = require('util')
|
||||||
|
const streamifier = require('streamifier')
|
||||||
chai.use(require('chai-as-promised'))
|
chai.use(require('chai-as-promised'))
|
||||||
|
|
||||||
const fsWriteFile = promisify(fs.writeFile)
|
const fsWriteFile = promisify(fs.writeFile)
|
||||||
@@ -25,6 +26,20 @@ async function getMetric(filestoreUrl, metric) {
|
|||||||
return parseInt(found ? found[1] : 0) || 0
|
return parseInt(found ? found[1] : 0) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!process.env.AWS_ACCESS_KEY_ID) {
|
||||||
|
throw new Error('please provide credentials for the AWS S3 test server')
|
||||||
|
}
|
||||||
|
|
||||||
|
function streamToString(stream) {
|
||||||
|
const chunks = []
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.on('data', chunk => chunks.push(chunk))
|
||||||
|
stream.on('error', reject)
|
||||||
|
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
||||||
|
stream.resume()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// store settings for multiple backends, so that we can test each one.
|
// store settings for multiple backends, so that we can test each one.
|
||||||
// fs will always be available - add others if they are configured
|
// fs will always be available - add others if they are configured
|
||||||
const BackendSettings = {
|
const BackendSettings = {
|
||||||
@@ -35,23 +50,79 @@ const BackendSettings = {
|
|||||||
public_files: Path.resolve(__dirname, '../../../public_files'),
|
public_files: Path.resolve(__dirname, '../../../public_files'),
|
||||||
template_files: Path.resolve(__dirname, '../../../template_files')
|
template_files: Path.resolve(__dirname, '../../../template_files')
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
S3Persistor: {
|
||||||
|
|
||||||
if (process.env.AWS_ACCESS_KEY_ID) {
|
|
||||||
BackendSettings.S3Persistor = {
|
|
||||||
backend: 's3',
|
backend: 's3',
|
||||||
s3: {
|
s3: {
|
||||||
key: process.env.AWS_ACCESS_KEY_ID,
|
key: process.env.AWS_ACCESS_KEY_ID,
|
||||||
secret: process.env.AWS_SECRET_ACCESS_KEY,
|
secret: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
endpoint: process.env.AWS_S3_ENDPOINT,
|
endpoint: process.env.AWS_S3_ENDPOINT,
|
||||||
pathStyle: true
|
pathStyle: true,
|
||||||
|
partSize: 100 * 1024 * 1024
|
||||||
},
|
},
|
||||||
stores: {
|
stores: {
|
||||||
user_files: process.env.AWS_S3_USER_FILES_BUCKET_NAME,
|
user_files: process.env.AWS_S3_USER_FILES_BUCKET_NAME,
|
||||||
template_files: process.env.AWS_S3_TEMPLATE_FILES_BUCKET_NAME,
|
template_files: process.env.AWS_S3_TEMPLATE_FILES_BUCKET_NAME,
|
||||||
public_files: process.env.AWS_S3_PUBLIC_FILES_BUCKET_NAME
|
public_files: process.env.AWS_S3_PUBLIC_FILES_BUCKET_NAME
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
FallbackS3ToFSPersistor: {
|
||||||
|
backend: 's3',
|
||||||
|
s3: {
|
||||||
|
key: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secret: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
endpoint: process.env.AWS_S3_ENDPOINT,
|
||||||
|
pathStyle: true,
|
||||||
|
partSize: 100 * 1024 * 1024
|
||||||
|
},
|
||||||
|
stores: {
|
||||||
|
user_files: process.env.AWS_S3_USER_FILES_BUCKET_NAME,
|
||||||
|
template_files: process.env.AWS_S3_TEMPLATE_FILES_BUCKET_NAME,
|
||||||
|
public_files: process.env.AWS_S3_PUBLIC_FILES_BUCKET_NAME
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
backend: 'fs',
|
||||||
|
buckets: {
|
||||||
|
[process.env.AWS_S3_USER_FILES_BUCKET_NAME]: Path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../../user_files'
|
||||||
|
),
|
||||||
|
[process.env.AWS_S3_PUBLIC_FILES_BUCKET_NAME]: Path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../../public_files'
|
||||||
|
),
|
||||||
|
[process.env.AWS_S3_TEMPLATE_FILES_BUCKET_NAME]: Path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../../template_files'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FallbackFSToS3Persistor: {
|
||||||
|
backend: 'fs',
|
||||||
|
s3: {
|
||||||
|
key: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secret: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
endpoint: process.env.AWS_S3_ENDPOINT,
|
||||||
|
pathStyle: true,
|
||||||
|
partSize: 100 * 1024 * 1024
|
||||||
|
},
|
||||||
|
stores: {
|
||||||
|
user_files: Path.resolve(__dirname, '../../../user_files'),
|
||||||
|
public_files: Path.resolve(__dirname, '../../../public_files'),
|
||||||
|
template_files: Path.resolve(__dirname, '../../../template_files')
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
backend: 's3',
|
||||||
|
buckets: {
|
||||||
|
[Path.resolve(__dirname, '../../../user_files')]: process.env
|
||||||
|
.AWS_S3_USER_FILES_BUCKET_NAME,
|
||||||
|
[Path.resolve(__dirname, '../../../public_files')]: process.env
|
||||||
|
.AWS_S3_PUBLIC_FILES_BUCKET_NAME,
|
||||||
|
[Path.resolve(__dirname, '../../../template_files')]: process.env
|
||||||
|
.AWS_S3_TEMPLATE_FILES_BUCKET_NAME
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +134,7 @@ describe('Filestore', function() {
|
|||||||
// redefine the test suite for every available backend
|
// redefine the test suite for every available backend
|
||||||
Object.keys(BackendSettings).forEach(backend => {
|
Object.keys(BackendSettings).forEach(backend => {
|
||||||
describe(backend, function() {
|
describe(backend, function() {
|
||||||
let app, previousEgress, previousIngress
|
let app, previousEgress, previousIngress, projectId
|
||||||
|
|
||||||
before(async function() {
|
before(async function() {
|
||||||
// create the app with the relevant filestore settings
|
// create the app with the relevant filestore settings
|
||||||
@@ -84,6 +155,7 @@ describe('Filestore', function() {
|
|||||||
getMetric(filestoreUrl, 's3_ingress')
|
getMetric(filestoreUrl, 's3_ingress')
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
projectId = `acceptance_tests_${Math.random()}`
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should send a 200 for the status endpoint', async function() {
|
it('should send a 200 for the status endpoint', async function() {
|
||||||
@@ -100,23 +172,21 @@ describe('Filestore', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('with a file on the server', function() {
|
describe('with a file on the server', function() {
|
||||||
let fileId, fileUrl
|
let fileId, fileUrl, constantFileContent
|
||||||
|
|
||||||
const localFileReadPath =
|
const localFileReadPath =
|
||||||
'/tmp/filestore_acceptance_tests_file_read.txt'
|
'/tmp/filestore_acceptance_tests_file_read.txt'
|
||||||
const constantFileContent = [
|
|
||||||
'hello world',
|
|
||||||
`line 2 goes here ${Math.random()}`,
|
|
||||||
'there are 3 lines in all'
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
before(async function() {
|
|
||||||
await fsWriteFile(localFileReadPath, constantFileContent)
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
fileId = Math.random()
|
fileId = Math.random()
|
||||||
fileUrl = `${filestoreUrl}/project/acceptance_tests/file/${directoryName}%2F${fileId}`
|
fileUrl = `${filestoreUrl}/project/${projectId}/file/${directoryName}%2F${fileId}`
|
||||||
|
constantFileContent = [
|
||||||
|
'hello world',
|
||||||
|
`line 2 goes here ${Math.random()}`,
|
||||||
|
'there are 3 lines in all'
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
await fsWriteFile(localFileReadPath, constantFileContent)
|
||||||
|
|
||||||
const writeStream = request.post(fileUrl)
|
const writeStream = request.post(fileUrl)
|
||||||
const readStream = fs.createReadStream(localFileReadPath)
|
const readStream = fs.createReadStream(localFileReadPath)
|
||||||
@@ -177,7 +247,7 @@ describe('Filestore', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should be able to copy files', async function() {
|
it('should be able to copy files', async function() {
|
||||||
const newProjectID = 'acceptance_tests_copyied_project'
|
const newProjectID = `acceptance_tests_copied_project_${Math.random()}`
|
||||||
const newFileId = Math.random()
|
const newFileId = Math.random()
|
||||||
const newFileUrl = `${filestoreUrl}/project/${newProjectID}/file/${directoryName}%2F${newFileId}`
|
const newFileUrl = `${filestoreUrl}/project/${newProjectID}/file/${directoryName}%2F${newFileId}`
|
||||||
const opts = {
|
const opts = {
|
||||||
@@ -185,7 +255,7 @@ describe('Filestore', function() {
|
|||||||
uri: newFileUrl,
|
uri: newFileUrl,
|
||||||
json: {
|
json: {
|
||||||
source: {
|
source: {
|
||||||
project_id: 'acceptance_tests',
|
project_id: projectId,
|
||||||
file_id: `${directoryName}/${fileId}`
|
file_id: `${directoryName}/${fileId}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,6 +268,18 @@ describe('Filestore', function() {
|
|||||||
expect(response.body).to.equal(constantFileContent)
|
expect(response.body).to.equal(constantFileContent)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should be able to overwrite the file', async function() {
|
||||||
|
const newContent = `here is some different content, ${Math.random()}`
|
||||||
|
const writeStream = request.post(fileUrl)
|
||||||
|
const readStream = streamifier.createReadStream(newContent)
|
||||||
|
// hack to consume the result to ensure the http request has been fully processed
|
||||||
|
const resultStream = fs.createWriteStream('/dev/null')
|
||||||
|
await pipeline(readStream, writeStream, resultStream)
|
||||||
|
|
||||||
|
const response = await rp.get(fileUrl)
|
||||||
|
expect(response.body).to.equal(newContent)
|
||||||
|
})
|
||||||
|
|
||||||
if (backend === 'S3Persistor') {
|
if (backend === 'S3Persistor') {
|
||||||
it('should record an egress metric for the upload', async function() {
|
it('should record an egress metric for the upload', async function() {
|
||||||
const metric = await getMetric(filestoreUrl, 's3_egress')
|
const metric = await getMetric(filestoreUrl, 's3_egress')
|
||||||
@@ -227,7 +309,7 @@ describe('Filestore', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('with multiple files', function() {
|
describe('with multiple files', function() {
|
||||||
let fileIds, fileUrls, project
|
let fileIds, fileUrls
|
||||||
const directoryName = 'directory'
|
const directoryName = 'directory'
|
||||||
const localFileReadPaths = [
|
const localFileReadPaths = [
|
||||||
'/tmp/filestore_acceptance_tests_file_read_1.txt',
|
'/tmp/filestore_acceptance_tests_file_read_1.txt',
|
||||||
@@ -254,11 +336,10 @@ describe('Filestore', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
project = `acceptance_tests_${Math.random()}`
|
|
||||||
fileIds = [Math.random(), Math.random()]
|
fileIds = [Math.random(), Math.random()]
|
||||||
fileUrls = [
|
fileUrls = [
|
||||||
`${filestoreUrl}/project/${project}/file/${directoryName}%2F${fileIds[0]}`,
|
`${filestoreUrl}/project/${projectId}/file/${directoryName}%2F${fileIds[0]}`,
|
||||||
`${filestoreUrl}/project/${project}/file/${directoryName}%2F${fileIds[1]}`
|
`${filestoreUrl}/project/${projectId}/file/${directoryName}%2F${fileIds[1]}`
|
||||||
]
|
]
|
||||||
|
|
||||||
const writeStreams = [
|
const writeStreams = [
|
||||||
@@ -282,7 +363,7 @@ describe('Filestore', function() {
|
|||||||
|
|
||||||
it('should get the directory size', async function() {
|
it('should get the directory size', async function() {
|
||||||
const response = await rp.get(
|
const response = await rp.get(
|
||||||
`${filestoreUrl}/project/${project}/size`
|
`${filestoreUrl}/project/${projectId}/size`
|
||||||
)
|
)
|
||||||
expect(parseInt(JSON.parse(response.body)['total bytes'])).to.equal(
|
expect(parseInt(JSON.parse(response.body)['total bytes'])).to.equal(
|
||||||
constantFileContents[0].length + constantFileContents[1].length
|
constantFileContents[0].length + constantFileContents[1].length
|
||||||
@@ -290,12 +371,44 @@ describe('Filestore', function() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('with a large file', function() {
|
||||||
|
let fileId, fileUrl, largeFileContent, error
|
||||||
|
|
||||||
|
beforeEach(async function() {
|
||||||
|
fileId = Math.random()
|
||||||
|
fileUrl = `${filestoreUrl}/project/${projectId}/file/${directoryName}%2F${fileId}`
|
||||||
|
|
||||||
|
largeFileContent = '_wombat_'.repeat(1024 * 1024) // 8 megabytes
|
||||||
|
largeFileContent += Math.random()
|
||||||
|
|
||||||
|
const writeStream = request.post(fileUrl)
|
||||||
|
const readStream = streamifier.createReadStream(largeFileContent)
|
||||||
|
// hack to consume the result to ensure the http request has been fully processed
|
||||||
|
const resultStream = fs.createWriteStream('/dev/null')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pipeline(readStream, writeStream, resultStream)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to get the file back', async function() {
|
||||||
|
const response = await rp.get(fileUrl)
|
||||||
|
expect(response.body).to.equal(largeFileContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not throw an error', function() {
|
||||||
|
expect(error).not.to.exist
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
if (backend === 'S3Persistor') {
|
if (backend === 'S3Persistor') {
|
||||||
describe('with a file in a specific bucket', function() {
|
describe('with a file in a specific bucket', function() {
|
||||||
let constantFileContents, fileId, fileUrl, bucketName
|
let constantFileContent, fileId, fileUrl, bucketName
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
constantFileContents = `This is a file in a different S3 bucket ${Math.random()}`
|
constantFileContent = `This is a file in a different S3 bucket ${Math.random()}`
|
||||||
fileId = Math.random().toString()
|
fileId = Math.random().toString()
|
||||||
bucketName = Math.random().toString()
|
bucketName = Math.random().toString()
|
||||||
fileUrl = `${filestoreUrl}/bucket/${bucketName}/key/${fileId}`
|
fileUrl = `${filestoreUrl}/bucket/${bucketName}/key/${fileId}`
|
||||||
@@ -320,14 +433,368 @@ describe('Filestore', function() {
|
|||||||
.upload({
|
.upload({
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Key: fileId,
|
Key: fileId,
|
||||||
Body: constantFileContents
|
Body: constantFileContent
|
||||||
})
|
})
|
||||||
.promise()
|
.promise()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get the file from the specified bucket', async function() {
|
it('should get the file from the specified bucket', async function() {
|
||||||
const response = await rp.get(fileUrl)
|
const response = await rp.get(fileUrl)
|
||||||
expect(response.body).to.equal(constantFileContents)
|
expect(response.body).to.equal(constantFileContent)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BackendSettings[backend].fallback) {
|
||||||
|
describe('with a fallback', function() {
|
||||||
|
async function uploadStringToPersistor(
|
||||||
|
persistor,
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
content
|
||||||
|
) {
|
||||||
|
const fileStream = streamifier.createReadStream(content)
|
||||||
|
await persistor.promises.sendStream(bucket, key, fileStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStringFromPersistor(persistor, bucket, key) {
|
||||||
|
const stream = await persistor.promises.getFileStream(
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
return streamToString(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectPersistorToHaveFile(
|
||||||
|
persistor,
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
content
|
||||||
|
) {
|
||||||
|
const foundContent = await getStringFromPersistor(
|
||||||
|
persistor,
|
||||||
|
bucket,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
expect(foundContent).to.equal(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectPersistorNotToHaveFile(persistor, bucket, key) {
|
||||||
|
await expect(
|
||||||
|
getStringFromPersistor(persistor, bucket, key)
|
||||||
|
).to.eventually.have.been.rejected.with.property(
|
||||||
|
'name',
|
||||||
|
'NotFoundError'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let constantFileContent,
|
||||||
|
fileId,
|
||||||
|
fileKey,
|
||||||
|
fileUrl,
|
||||||
|
bucket,
|
||||||
|
fallbackBucket
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
constantFileContent = `This is yet more file content ${Math.random()}`
|
||||||
|
fileId = Math.random().toString()
|
||||||
|
fileKey = `${projectId}/${directoryName}/${fileId}`
|
||||||
|
fileUrl = `${filestoreUrl}/project/${projectId}/file/${directoryName}%2F${fileId}`
|
||||||
|
|
||||||
|
bucket = Settings.filestore.stores.user_files
|
||||||
|
fallbackBucket = Settings.filestore.fallback.buckets[bucket]
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with a file in the fallback bucket', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
await uploadStringToPersistor(
|
||||||
|
app.persistor.fallbackPersistor,
|
||||||
|
fallbackBucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not find file in the primary', async function() {
|
||||||
|
await expectPersistorNotToHaveFile(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
fileKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should find the file in the fallback', async function() {
|
||||||
|
await expectPersistorToHaveFile(
|
||||||
|
app.persistor.fallbackPersistor,
|
||||||
|
fallbackBucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when copyOnMiss is disabled', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
Settings.filestore.fallback.copyOnMiss = false
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch the file', async function() {
|
||||||
|
const res = await rp.get(fileUrl)
|
||||||
|
expect(res.body).to.equal(constantFileContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not copy the file to the primary', async function() {
|
||||||
|
await rp.get(fileUrl)
|
||||||
|
|
||||||
|
await expectPersistorNotToHaveFile(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
fileKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when copyOnMiss is enabled', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
Settings.filestore.fallback.copyOnMiss = true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch the file', async function() {
|
||||||
|
const res = await rp.get(fileUrl)
|
||||||
|
expect(res.body).to.equal(constantFileContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('copies the file to the primary', async function() {
|
||||||
|
await rp.get(fileUrl)
|
||||||
|
// wait for the file to copy in the background
|
||||||
|
await promisify(setTimeout)(1000)
|
||||||
|
|
||||||
|
await expectPersistorToHaveFile(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when copying a file', function() {
|
||||||
|
let newFileId, newFileUrl, newFileKey, opts
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
const newProjectID = `acceptance_tests_copied_project_${Math.random()}`
|
||||||
|
newFileId = Math.random()
|
||||||
|
newFileUrl = `${filestoreUrl}/project/${newProjectID}/file/${directoryName}%2F${newFileId}`
|
||||||
|
newFileKey = `${newProjectID}/${directoryName}/${newFileId}`
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
method: 'put',
|
||||||
|
uri: newFileUrl,
|
||||||
|
json: {
|
||||||
|
source: {
|
||||||
|
project_id: projectId,
|
||||||
|
file_id: `${directoryName}/${fileId}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when copyOnMiss is false', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
Settings.filestore.fallback.copyOnMiss = false
|
||||||
|
|
||||||
|
const response = await rp(opts)
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should leave the old file in the old bucket', async function() {
|
||||||
|
await expectPersistorToHaveFile(
|
||||||
|
app.persistor.fallbackPersistor,
|
||||||
|
fallbackBucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create a new file in the old bucket', async function() {
|
||||||
|
await expectPersistorNotToHaveFile(
|
||||||
|
app.persistor.fallbackPersistor,
|
||||||
|
fallbackBucket,
|
||||||
|
newFileKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create a new file in the new bucket', async function() {
|
||||||
|
await expectPersistorToHaveFile(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
newFileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not copy the old file to the primary with the old key', async function() {
|
||||||
|
// wait for the file to copy in the background
|
||||||
|
await promisify(setTimeout)(1000)
|
||||||
|
|
||||||
|
await expectPersistorNotToHaveFile(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
fileKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when copyOnMiss is true', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
Settings.filestore.fallback.copyOnMiss = true
|
||||||
|
|
||||||
|
const response = await rp(opts)
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should leave the old file in the old bucket', async function() {
|
||||||
|
await expectPersistorToHaveFile(
|
||||||
|
app.persistor.fallbackPersistor,
|
||||||
|
fallbackBucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create a new file in the old bucket', async function() {
|
||||||
|
await expectPersistorNotToHaveFile(
|
||||||
|
app.persistor.fallbackPersistor,
|
||||||
|
fallbackBucket,
|
||||||
|
newFileKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create a new file in the new bucket', async function() {
|
||||||
|
await expectPersistorToHaveFile(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
newFileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy the old file to the primary with the old key', async function() {
|
||||||
|
// wait for the file to copy in the background
|
||||||
|
await promisify(setTimeout)(1000)
|
||||||
|
|
||||||
|
await expectPersistorToHaveFile(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when sending a file', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
const writeStream = request.post(fileUrl)
|
||||||
|
const readStream = streamifier.createReadStream(
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
// hack to consume the result to ensure the http request has been fully processed
|
||||||
|
const resultStream = fs.createWriteStream('/dev/null')
|
||||||
|
await pipeline(readStream, writeStream, resultStream)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should store the file on the primary', async function() {
|
||||||
|
await expectPersistorToHaveFile(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not store the file on the fallback', async function() {
|
||||||
|
await expectPersistorNotToHaveFile(
|
||||||
|
app.persistor.fallbackPersistor,
|
||||||
|
fallbackBucket,
|
||||||
|
`${projectId}/${directoryName}/${fileId}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when deleting a file', function() {
|
||||||
|
describe('when the file exists on the primary', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
await uploadStringToPersistor(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the file', async function() {
|
||||||
|
const response = await rp.del(fileUrl)
|
||||||
|
expect(response.statusCode).to.equal(204)
|
||||||
|
await expect(
|
||||||
|
rp.get(fileUrl)
|
||||||
|
).to.eventually.be.rejected.and.have.property('statusCode', 404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the file exists on the fallback', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
await uploadStringToPersistor(
|
||||||
|
app.persistor.fallbackPersistor,
|
||||||
|
fallbackBucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the file', async function() {
|
||||||
|
const response = await rp.del(fileUrl)
|
||||||
|
expect(response.statusCode).to.equal(204)
|
||||||
|
await expect(
|
||||||
|
rp.get(fileUrl)
|
||||||
|
).to.eventually.be.rejected.and.have.property('statusCode', 404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the file exists on both the primary and the fallback', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
await uploadStringToPersistor(
|
||||||
|
app.persistor.primaryPersistor,
|
||||||
|
bucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
await uploadStringToPersistor(
|
||||||
|
app.persistor.fallbackPersistor,
|
||||||
|
fallbackBucket,
|
||||||
|
fileKey,
|
||||||
|
constantFileContent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the files', async function() {
|
||||||
|
const response = await rp.del(fileUrl)
|
||||||
|
expect(response.statusCode).to.equal(204)
|
||||||
|
await expect(
|
||||||
|
rp.get(fileUrl)
|
||||||
|
).to.eventually.be.rejected.and.have.property('statusCode', 404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the file does not exist', function() {
|
||||||
|
it('should return return 204', async function() {
|
||||||
|
// S3 doesn't give us a 404 when the object doesn't exist, so to stay
|
||||||
|
// consistent we merrily return 204 ourselves here as well
|
||||||
|
const response = await rp.del(fileUrl)
|
||||||
|
expect(response.statusCode).to.equal(204)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -341,7 +808,7 @@ describe('Filestore', function() {
|
|||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
fileId = Math.random()
|
fileId = Math.random()
|
||||||
fileUrl = `${filestoreUrl}/project/acceptance_tests/file/${directoryName}%2F${fileId}`
|
fileUrl = `${filestoreUrl}/project/${projectId}/file/${directoryName}%2F${fileId}`
|
||||||
const stat = await fsStat(localFileReadPath)
|
const stat = await fsStat(localFileReadPath)
|
||||||
localFileSize = stat.size
|
localFileSize = stat.size
|
||||||
const writeStream = request.post(fileUrl)
|
const writeStream = request.post(fileUrl)
|
||||||
|
|||||||
+87
-60
@@ -7,24 +7,37 @@ const Errors = require('../../../app/js/Errors')
|
|||||||
chai.use(require('sinon-chai'))
|
chai.use(require('sinon-chai'))
|
||||||
chai.use(require('chai-as-promised'))
|
chai.use(require('chai-as-promised'))
|
||||||
|
|
||||||
const modulePath = '../../../app/js/FSPersistorManager.js'
|
const modulePath = '../../../app/js/FSPersistor.js'
|
||||||
|
|
||||||
describe('FSPersistorManagerTests', function() {
|
describe('FSPersistorTests', function() {
|
||||||
const stat = { size: 4, isFile: sinon.stub().returns(true) }
|
const stat = { size: 4, isFile: sinon.stub().returns(true) }
|
||||||
const fd = 1234
|
const fd = 1234
|
||||||
const readStream = 'readStream'
|
|
||||||
const writeStream = 'writeStream'
|
const writeStream = 'writeStream'
|
||||||
const remoteStream = 'remoteStream'
|
const remoteStream = 'remoteStream'
|
||||||
const tempFile = '/tmp/potato.txt'
|
const tempFile = '/tmp/potato.txt'
|
||||||
const location = '/foo'
|
const location = '/foo'
|
||||||
const error = new Error('guru meditation error')
|
const error = new Error('guru meditation error')
|
||||||
|
const md5 = 'ffffffff'
|
||||||
|
|
||||||
const files = ['animals/wombat.tex', 'vegetables/potato.tex']
|
const files = ['animals/wombat.tex', 'vegetables/potato.tex']
|
||||||
const globs = [`${location}/${files[0]}`, `${location}/${files[1]}`]
|
const globs = [`${location}/${files[0]}`, `${location}/${files[1]}`]
|
||||||
const filteredFilenames = ['animals_wombat.tex', 'vegetables_potato.tex']
|
const filteredFilenames = ['animals_wombat.tex', 'vegetables_potato.tex']
|
||||||
let fs, rimraf, stream, LocalFileWriter, FSPersistorManager, glob
|
let fs,
|
||||||
|
rimraf,
|
||||||
|
stream,
|
||||||
|
LocalFileWriter,
|
||||||
|
FSPersistor,
|
||||||
|
glob,
|
||||||
|
readStream,
|
||||||
|
crypto,
|
||||||
|
Hash
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
|
readStream = {
|
||||||
|
name: 'readStream',
|
||||||
|
on: sinon.stub().yields(),
|
||||||
|
pipe: sinon.stub()
|
||||||
|
}
|
||||||
fs = {
|
fs = {
|
||||||
createReadStream: sinon.stub().returns(readStream),
|
createReadStream: sinon.stub().returns(readStream),
|
||||||
createWriteStream: sinon.stub().returns(writeStream),
|
createWriteStream: sinon.stub().returns(writeStream),
|
||||||
@@ -41,14 +54,26 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
deleteFile: sinon.stub().resolves()
|
deleteFile: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FSPersistorManager = SandboxedModule.require(modulePath, {
|
Hash = {
|
||||||
|
end: sinon.stub(),
|
||||||
|
read: sinon.stub().returns(md5),
|
||||||
|
setEncoding: sinon.stub()
|
||||||
|
}
|
||||||
|
crypto = {
|
||||||
|
createHash: sinon.stub().returns(Hash)
|
||||||
|
}
|
||||||
|
FSPersistor = SandboxedModule.require(modulePath, {
|
||||||
requires: {
|
requires: {
|
||||||
'./LocalFileWriter': LocalFileWriter,
|
'./LocalFileWriter': LocalFileWriter,
|
||||||
'./Errors': Errors,
|
'./Errors': Errors,
|
||||||
fs,
|
fs,
|
||||||
glob,
|
glob,
|
||||||
rimraf,
|
rimraf,
|
||||||
stream
|
stream,
|
||||||
|
crypto,
|
||||||
|
// imported by PersistorHelper but otherwise unused here
|
||||||
|
'stream-meter': {},
|
||||||
|
'logger-sharelatex': {}
|
||||||
},
|
},
|
||||||
globals: { console }
|
globals: { console }
|
||||||
})
|
})
|
||||||
@@ -57,7 +82,7 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
describe('sendFile', function() {
|
describe('sendFile', function() {
|
||||||
const localFilesystemPath = '/path/to/local/file'
|
const localFilesystemPath = '/path/to/local/file'
|
||||||
it('should copy the file', async function() {
|
it('should copy the file', async function() {
|
||||||
await FSPersistorManager.promises.sendFile(
|
await FSPersistor.promises.sendFile(
|
||||||
location,
|
location,
|
||||||
files[0],
|
files[0],
|
||||||
localFilesystemPath
|
localFilesystemPath
|
||||||
@@ -72,33 +97,21 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
it('should return an error if the file cannot be stored', async function() {
|
it('should return an error if the file cannot be stored', async function() {
|
||||||
stream.pipeline.yields(error)
|
stream.pipeline.yields(error)
|
||||||
await expect(
|
await expect(
|
||||||
FSPersistorManager.promises.sendFile(
|
FSPersistor.promises.sendFile(location, files[0], localFilesystemPath)
|
||||||
location,
|
|
||||||
files[0],
|
|
||||||
localFilesystemPath
|
|
||||||
)
|
|
||||||
).to.eventually.be.rejected.and.have.property('cause', error)
|
).to.eventually.be.rejected.and.have.property('cause', error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sendStream', function() {
|
describe('sendStream', function() {
|
||||||
it('should send the stream to LocalFileWriter', async function() {
|
it('should send the stream to LocalFileWriter', async function() {
|
||||||
await FSPersistorManager.promises.sendStream(
|
await FSPersistor.promises.sendStream(location, files[0], remoteStream)
|
||||||
location,
|
|
||||||
files[0],
|
|
||||||
remoteStream
|
|
||||||
)
|
|
||||||
expect(LocalFileWriter.promises.writeStream).to.have.been.calledWith(
|
expect(LocalFileWriter.promises.writeStream).to.have.been.calledWith(
|
||||||
remoteStream
|
remoteStream
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should delete the temporary file', async function() {
|
it('should delete the temporary file', async function() {
|
||||||
await FSPersistorManager.promises.sendStream(
|
await FSPersistor.promises.sendStream(location, files[0], remoteStream)
|
||||||
location,
|
|
||||||
files[0],
|
|
||||||
remoteStream
|
|
||||||
)
|
|
||||||
expect(LocalFileWriter.promises.deleteFile).to.have.been.calledWith(
|
expect(LocalFileWriter.promises.deleteFile).to.have.been.calledWith(
|
||||||
tempFile
|
tempFile
|
||||||
)
|
)
|
||||||
@@ -107,30 +120,55 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
it('should return the error from LocalFileWriter', async function() {
|
it('should return the error from LocalFileWriter', async function() {
|
||||||
LocalFileWriter.promises.writeStream.rejects(error)
|
LocalFileWriter.promises.writeStream.rejects(error)
|
||||||
await expect(
|
await expect(
|
||||||
FSPersistorManager.promises.sendStream(location, files[0], remoteStream)
|
FSPersistor.promises.sendStream(location, files[0], remoteStream)
|
||||||
).to.eventually.be.rejectedWith(error)
|
).to.eventually.be.rejectedWith(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should send the temporary file to the filestore', async function() {
|
it('should send the temporary file to the filestore', async function() {
|
||||||
await FSPersistorManager.promises.sendStream(
|
await FSPersistor.promises.sendStream(location, files[0], remoteStream)
|
||||||
location,
|
|
||||||
files[0],
|
|
||||||
remoteStream
|
|
||||||
)
|
|
||||||
expect(fs.createReadStream).to.have.been.calledWith(tempFile)
|
expect(fs.createReadStream).to.have.been.calledWith(tempFile)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('when the md5 hash does not match', function() {
|
||||||
|
it('should return a write error', async function() {
|
||||||
|
await expect(
|
||||||
|
FSPersistor.promises.sendStream(
|
||||||
|
location,
|
||||||
|
files[0],
|
||||||
|
remoteStream,
|
||||||
|
'00000000'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.WriteError)
|
||||||
|
.and.have.property('message', 'md5 hash mismatch')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deletes the copied file', async function() {
|
||||||
|
try {
|
||||||
|
await FSPersistor.promises.sendStream(
|
||||||
|
location,
|
||||||
|
files[0],
|
||||||
|
remoteStream,
|
||||||
|
'00000000'
|
||||||
|
)
|
||||||
|
} catch (_) {}
|
||||||
|
expect(LocalFileWriter.promises.deleteFile).to.have.been.calledWith(
|
||||||
|
`${location}/${filteredFilenames[0]}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getFileStream', function() {
|
describe('getFileStream', function() {
|
||||||
it('should use correct file location', async function() {
|
it('should use correct file location', async function() {
|
||||||
await FSPersistorManager.promises.getFileStream(location, files[0], {})
|
await FSPersistor.promises.getFileStream(location, files[0], {})
|
||||||
expect(fs.open).to.have.been.calledWith(
|
expect(fs.open).to.have.been.calledWith(
|
||||||
`${location}/${filteredFilenames[0]}`
|
`${location}/${filteredFilenames[0]}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should pass the options to createReadStream', async function() {
|
it('should pass the options to createReadStream', async function() {
|
||||||
await FSPersistorManager.promises.getFileStream(location, files[0], {
|
await FSPersistor.promises.getFileStream(location, files[0], {
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 8
|
end: 8
|
||||||
})
|
})
|
||||||
@@ -146,18 +184,14 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
err.code = 'ENOENT'
|
err.code = 'ENOENT'
|
||||||
fs.open.yields(err)
|
fs.open.yields(err)
|
||||||
|
|
||||||
await expect(
|
await expect(FSPersistor.promises.getFileStream(location, files[0], {}))
|
||||||
FSPersistorManager.promises.getFileStream(location, files[0], {})
|
|
||||||
)
|
|
||||||
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
|
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
|
||||||
.and.have.property('cause', err)
|
.and.have.property('cause', err)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should wrap any other error', async function() {
|
it('should wrap any other error', async function() {
|
||||||
fs.open.yields(error)
|
fs.open.yields(error)
|
||||||
await expect(
|
await expect(FSPersistor.promises.getFileStream(location, files[0], {}))
|
||||||
FSPersistorManager.promises.getFileStream(location, files[0], {})
|
|
||||||
)
|
|
||||||
.to.eventually.be.rejectedWith('failed to open file for streaming')
|
.to.eventually.be.rejectedWith('failed to open file for streaming')
|
||||||
.and.be.an.instanceOf(Errors.ReadError)
|
.and.be.an.instanceOf(Errors.ReadError)
|
||||||
.and.have.property('cause', error)
|
.and.have.property('cause', error)
|
||||||
@@ -181,18 +215,18 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
|
|
||||||
it('should return the file size', async function() {
|
it('should return the file size', async function() {
|
||||||
expect(
|
expect(
|
||||||
await FSPersistorManager.promises.getFileSize(location, files[0])
|
await FSPersistor.promises.getFileSize(location, files[0])
|
||||||
).to.equal(size)
|
).to.equal(size)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw a NotFoundError if the file does not exist', async function() {
|
it('should throw a NotFoundError if the file does not exist', async function() {
|
||||||
await expect(
|
await expect(
|
||||||
FSPersistorManager.promises.getFileSize(location, badFilename)
|
FSPersistor.promises.getFileSize(location, badFilename)
|
||||||
).to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
|
).to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should wrap any other error', async function() {
|
it('should wrap any other error', async function() {
|
||||||
await expect(FSPersistorManager.promises.getFileSize(location, 'raccoon'))
|
await expect(FSPersistor.promises.getFileSize(location, 'raccoon'))
|
||||||
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
|
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
|
||||||
.and.have.property('cause', error)
|
.and.have.property('cause', error)
|
||||||
})
|
})
|
||||||
@@ -200,28 +234,28 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
|
|
||||||
describe('copyFile', function() {
|
describe('copyFile', function() {
|
||||||
it('Should open the source for reading', async function() {
|
it('Should open the source for reading', async function() {
|
||||||
await FSPersistorManager.promises.copyFile(location, files[0], files[1])
|
await FSPersistor.promises.copyFile(location, files[0], files[1])
|
||||||
expect(fs.createReadStream).to.have.been.calledWith(
|
expect(fs.createReadStream).to.have.been.calledWith(
|
||||||
`${location}/${filteredFilenames[0]}`
|
`${location}/${filteredFilenames[0]}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should open the target for writing', async function() {
|
it('Should open the target for writing', async function() {
|
||||||
await FSPersistorManager.promises.copyFile(location, files[0], files[1])
|
await FSPersistor.promises.copyFile(location, files[0], files[1])
|
||||||
expect(fs.createWriteStream).to.have.been.calledWith(
|
expect(fs.createWriteStream).to.have.been.calledWith(
|
||||||
`${location}/${filteredFilenames[1]}`
|
`${location}/${filteredFilenames[1]}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should pipe the source to the target', async function() {
|
it('Should pipe the source to the target', async function() {
|
||||||
await FSPersistorManager.promises.copyFile(location, files[0], files[1])
|
await FSPersistor.promises.copyFile(location, files[0], files[1])
|
||||||
expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
|
expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deleteFile', function() {
|
describe('deleteFile', function() {
|
||||||
it('Should call unlink with correct options', async function() {
|
it('Should call unlink with correct options', async function() {
|
||||||
await FSPersistorManager.promises.deleteFile(location, files[0])
|
await FSPersistor.promises.deleteFile(location, files[0])
|
||||||
expect(fs.unlink).to.have.been.calledWith(
|
expect(fs.unlink).to.have.been.calledWith(
|
||||||
`${location}/${filteredFilenames[0]}`
|
`${location}/${filteredFilenames[0]}`
|
||||||
)
|
)
|
||||||
@@ -230,14 +264,14 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
it('Should propagate the error', async function() {
|
it('Should propagate the error', async function() {
|
||||||
fs.unlink.yields(error)
|
fs.unlink.yields(error)
|
||||||
await expect(
|
await expect(
|
||||||
FSPersistorManager.promises.deleteFile(location, files[0])
|
FSPersistor.promises.deleteFile(location, files[0])
|
||||||
).to.eventually.be.rejected.and.have.property('cause', error)
|
).to.eventually.be.rejected.and.have.property('cause', error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deleteDirectory', function() {
|
describe('deleteDirectory', function() {
|
||||||
it('Should call rmdir(rimraf) with correct options', async function() {
|
it('Should call rmdir(rimraf) with correct options', async function() {
|
||||||
await FSPersistorManager.promises.deleteDirectory(location, files[0])
|
await FSPersistor.promises.deleteDirectory(location, files[0])
|
||||||
expect(rimraf).to.have.been.calledWith(
|
expect(rimraf).to.have.been.calledWith(
|
||||||
`${location}/${filteredFilenames[0]}`
|
`${location}/${filteredFilenames[0]}`
|
||||||
)
|
)
|
||||||
@@ -246,7 +280,7 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
it('Should propagate the error', async function() {
|
it('Should propagate the error', async function() {
|
||||||
rimraf.yields(error)
|
rimraf.yields(error)
|
||||||
await expect(
|
await expect(
|
||||||
FSPersistorManager.promises.deleteDirectory(location, files[0])
|
FSPersistor.promises.deleteDirectory(location, files[0])
|
||||||
).to.eventually.be.rejected.and.have.property('cause', error)
|
).to.eventually.be.rejected.and.have.property('cause', error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -266,7 +300,7 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should call stat with correct options', async function() {
|
it('Should call stat with correct options', async function() {
|
||||||
await FSPersistorManager.promises.checkIfFileExists(location, files[0])
|
await FSPersistor.promises.checkIfFileExists(location, files[0])
|
||||||
expect(fs.stat).to.have.been.calledWith(
|
expect(fs.stat).to.have.been.calledWith(
|
||||||
`${location}/${filteredFilenames[0]}`
|
`${location}/${filteredFilenames[0]}`
|
||||||
)
|
)
|
||||||
@@ -274,23 +308,18 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
|
|
||||||
it('Should return true for existing files', async function() {
|
it('Should return true for existing files', async function() {
|
||||||
expect(
|
expect(
|
||||||
await FSPersistorManager.promises.checkIfFileExists(location, files[0])
|
await FSPersistor.promises.checkIfFileExists(location, files[0])
|
||||||
).to.equal(true)
|
).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should return false for non-existing files', async function() {
|
it('Should return false for non-existing files', async function() {
|
||||||
expect(
|
expect(
|
||||||
await FSPersistorManager.promises.checkIfFileExists(
|
await FSPersistor.promises.checkIfFileExists(location, badFilename)
|
||||||
location,
|
|
||||||
badFilename
|
|
||||||
)
|
|
||||||
).to.equal(false)
|
).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should wrap the error if there is a problem', async function() {
|
it('should wrap the error if there is a problem', async function() {
|
||||||
await expect(
|
await expect(FSPersistor.promises.checkIfFileExists(location, 'llama'))
|
||||||
FSPersistorManager.promises.checkIfFileExists(location, 'llama')
|
|
||||||
)
|
|
||||||
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
|
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
|
||||||
.and.have.property('cause', error)
|
.and.have.property('cause', error)
|
||||||
})
|
})
|
||||||
@@ -299,9 +328,7 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
describe('directorySize', function() {
|
describe('directorySize', function() {
|
||||||
it('should wrap the error', async function() {
|
it('should wrap the error', async function() {
|
||||||
glob.yields(error)
|
glob.yields(error)
|
||||||
await expect(
|
await expect(FSPersistor.promises.directorySize(location, files[0]))
|
||||||
FSPersistorManager.promises.directorySize(location, files[0])
|
|
||||||
)
|
|
||||||
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
|
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
|
||||||
.and.include({ cause: error })
|
.and.include({ cause: error })
|
||||||
.and.have.property('info')
|
.and.have.property('info')
|
||||||
@@ -309,7 +336,7 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should filter the directory name', async function() {
|
it('should filter the directory name', async function() {
|
||||||
await FSPersistorManager.promises.directorySize(location, files[0])
|
await FSPersistor.promises.directorySize(location, files[0])
|
||||||
expect(glob).to.have.been.calledWith(
|
expect(glob).to.have.been.calledWith(
|
||||||
`${location}/${filteredFilenames[0]}_*`
|
`${location}/${filteredFilenames[0]}_*`
|
||||||
)
|
)
|
||||||
@@ -317,7 +344,7 @@ describe('FSPersistorManagerTests', function() {
|
|||||||
|
|
||||||
it('should sum directory files size', async function() {
|
it('should sum directory files size', async function() {
|
||||||
expect(
|
expect(
|
||||||
await FSPersistorManager.promises.directorySize(location, files[0])
|
await FSPersistor.promises.directorySize(location, files[0])
|
||||||
).to.equal(stat.size * files.length)
|
).to.equal(stat.size * files.length)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,519 @@
|
|||||||
|
const sinon = require('sinon')
|
||||||
|
const chai = require('chai')
|
||||||
|
const { expect } = chai
|
||||||
|
const modulePath = '../../../app/js/MigrationPersistor.js'
|
||||||
|
const SandboxedModule = require('sandboxed-module')
|
||||||
|
|
||||||
|
const Errors = require('../../../app/js/Errors')
|
||||||
|
|
||||||
|
// Not all methods are tested here, but a method with each type of wrapping has
|
||||||
|
// tests. Specifically, the following wrapping methods are tested here:
|
||||||
|
// getFileStream: _wrapFallbackMethod
|
||||||
|
// sendStream: forward-to-primary
|
||||||
|
// deleteFile: _wrapMethodOnBothPersistors
|
||||||
|
// copyFile: copyFileWithFallback
|
||||||
|
|
||||||
|
describe('MigrationPersistorTests', function() {
|
||||||
|
const bucket = 'womBucket'
|
||||||
|
const fallbackBucket = 'bucKangaroo'
|
||||||
|
const key = 'monKey'
|
||||||
|
const destKey = 'donKey'
|
||||||
|
const genericError = new Error('guru meditation error')
|
||||||
|
const notFoundError = new Errors.NotFoundError('not found')
|
||||||
|
const size = 33
|
||||||
|
const md5 = 'ffffffff'
|
||||||
|
|
||||||
|
let Metrics,
|
||||||
|
Settings,
|
||||||
|
Logger,
|
||||||
|
Stream,
|
||||||
|
MigrationPersistor,
|
||||||
|
fileStream,
|
||||||
|
newPersistor
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
fileStream = {
|
||||||
|
name: 'fileStream',
|
||||||
|
on: sinon
|
||||||
|
.stub()
|
||||||
|
.withArgs('end')
|
||||||
|
.yields(),
|
||||||
|
pipe: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
newPersistor = function(hasFile) {
|
||||||
|
return {
|
||||||
|
promises: {
|
||||||
|
sendFile: sinon.stub().resolves(),
|
||||||
|
sendStream: sinon.stub().resolves(),
|
||||||
|
getFileStream: hasFile
|
||||||
|
? sinon.stub().resolves(fileStream)
|
||||||
|
: sinon.stub().rejects(notFoundError),
|
||||||
|
deleteDirectory: sinon.stub().resolves(),
|
||||||
|
getFileSize: hasFile
|
||||||
|
? sinon.stub().resolves(size)
|
||||||
|
: sinon.stub().rejects(notFoundError),
|
||||||
|
deleteFile: sinon.stub().resolves(),
|
||||||
|
copyFile: hasFile
|
||||||
|
? sinon.stub().resolves()
|
||||||
|
: sinon.stub().rejects(notFoundError),
|
||||||
|
checkIfFileExists: sinon.stub().resolves(hasFile),
|
||||||
|
directorySize: hasFile
|
||||||
|
? sinon.stub().resolves(size)
|
||||||
|
: sinon.stub().rejects(notFoundError),
|
||||||
|
getFileMd5Hash: hasFile
|
||||||
|
? sinon.stub().resolves(md5)
|
||||||
|
: sinon.stub().rejects(notFoundError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings = {
|
||||||
|
filestore: {
|
||||||
|
fallback: {
|
||||||
|
buckets: {
|
||||||
|
[bucket]: fallbackBucket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Metrics = {
|
||||||
|
inc: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream = {
|
||||||
|
pipeline: sinon.stub().yields(),
|
||||||
|
PassThrough: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger = {
|
||||||
|
warn: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
MigrationPersistor = SandboxedModule.require(modulePath, {
|
||||||
|
requires: {
|
||||||
|
'settings-sharelatex': Settings,
|
||||||
|
stream: Stream,
|
||||||
|
'./Errors': Errors,
|
||||||
|
'metrics-sharelatex': Metrics,
|
||||||
|
'logger-sharelatex': Logger
|
||||||
|
},
|
||||||
|
globals: { console }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getFileStream', function() {
|
||||||
|
const options = { wombat: 'potato' }
|
||||||
|
describe('when the primary persistor has the file', function() {
|
||||||
|
let primaryPersistor, fallbackPersistor, migrationPersistor, response
|
||||||
|
beforeEach(async function() {
|
||||||
|
primaryPersistor = newPersistor(true)
|
||||||
|
fallbackPersistor = newPersistor(false)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
response = await migrationPersistor.promises.getFileStream(
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the file stream', function() {
|
||||||
|
expect(response).to.equal(fileStream)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch the file from the primary persistor, with the correct options', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.getFileStream
|
||||||
|
).to.have.been.calledWithExactly(bucket, key, options)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not query the fallback persistor', function() {
|
||||||
|
expect(fallbackPersistor.promises.getFileStream).not.to.have.been.called
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the fallback persistor has the file', function() {
|
||||||
|
let primaryPersistor, fallbackPersistor, migrationPersistor, response
|
||||||
|
beforeEach(async function() {
|
||||||
|
primaryPersistor = newPersistor(false)
|
||||||
|
fallbackPersistor = newPersistor(true)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
response = await migrationPersistor.promises.getFileStream(
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the file stream', function() {
|
||||||
|
expect(response).to.be.an.instanceOf(Stream.PassThrough)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch the file from the primary persistor with the correct options', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.getFileStream
|
||||||
|
).to.have.been.calledWithExactly(bucket, key, options)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch the file from the fallback persistor with the fallback bucket with the correct options', function() {
|
||||||
|
expect(
|
||||||
|
fallbackPersistor.promises.getFileStream
|
||||||
|
).to.have.been.calledWithExactly(fallbackBucket, key, options)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create one read stream', function() {
|
||||||
|
expect(fallbackPersistor.promises.getFileStream).to.have.been.calledOnce
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not send the file to the primary', function() {
|
||||||
|
expect(primaryPersistor.promises.sendStream).not.to.have.been.called
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the file should be copied to the primary', function() {
|
||||||
|
let primaryPersistor,
|
||||||
|
fallbackPersistor,
|
||||||
|
migrationPersistor,
|
||||||
|
returnedStream
|
||||||
|
beforeEach(async function() {
|
||||||
|
primaryPersistor = newPersistor(false)
|
||||||
|
fallbackPersistor = newPersistor(true)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
Settings.filestore.fallback.copyOnMiss = true
|
||||||
|
returnedStream = await migrationPersistor.promises.getFileStream(
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create one read stream', function() {
|
||||||
|
expect(fallbackPersistor.promises.getFileStream).to.have.been.calledOnce
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get the md5 hash from the source', function() {
|
||||||
|
expect(
|
||||||
|
fallbackPersistor.promises.getFileMd5Hash
|
||||||
|
).to.have.been.calledWith(fallbackBucket, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should send a stream to the primary', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.sendStream
|
||||||
|
).to.have.been.calledWithExactly(
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
sinon.match.instanceOf(Stream.PassThrough),
|
||||||
|
md5
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should send a stream to the client', function() {
|
||||||
|
expect(returnedStream).to.be.an.instanceOf(Stream.PassThrough)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when neither persistor has the file', function() {
|
||||||
|
it('rejects with a NotFoundError', async function() {
|
||||||
|
const migrationPersistor = MigrationPersistor(
|
||||||
|
newPersistor(false),
|
||||||
|
newPersistor(false)
|
||||||
|
)
|
||||||
|
return expect(
|
||||||
|
migrationPersistor.promises.getFileStream(bucket, key)
|
||||||
|
).to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the primary persistor throws an unexpected error', function() {
|
||||||
|
let primaryPersistor, fallbackPersistor, migrationPersistor, error
|
||||||
|
beforeEach(async function() {
|
||||||
|
primaryPersistor = newPersistor(false)
|
||||||
|
fallbackPersistor = newPersistor(true)
|
||||||
|
primaryPersistor.promises.getFileStream = sinon
|
||||||
|
.stub()
|
||||||
|
.rejects(genericError)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
await migrationPersistor.promises.getFileStream(bucket, key, options)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects with the error', function() {
|
||||||
|
expect(error).to.equal(genericError)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not call the fallback', function() {
|
||||||
|
expect(fallbackPersistor.promises.getFileStream).not.to.have.been.called
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the fallback persistor throws an unexpected error', function() {
|
||||||
|
let primaryPersistor, fallbackPersistor, migrationPersistor, error
|
||||||
|
beforeEach(async function() {
|
||||||
|
primaryPersistor = newPersistor(false)
|
||||||
|
fallbackPersistor = newPersistor(false)
|
||||||
|
fallbackPersistor.promises.getFileStream = sinon
|
||||||
|
.stub()
|
||||||
|
.rejects(genericError)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
await migrationPersistor.promises.getFileStream(bucket, key, options)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects with the error', function() {
|
||||||
|
expect(error).to.equal(genericError)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have called the fallback', function() {
|
||||||
|
expect(
|
||||||
|
fallbackPersistor.promises.getFileStream
|
||||||
|
).to.have.been.calledWith(fallbackBucket, key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sendStream', function() {
|
||||||
|
let primaryPersistor, fallbackPersistor, migrationPersistor
|
||||||
|
beforeEach(function() {
|
||||||
|
primaryPersistor = newPersistor(false)
|
||||||
|
fallbackPersistor = newPersistor(false)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when it works', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
return migrationPersistor.promises.sendStream(bucket, key, fileStream)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should send the file to the primary persistor', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.sendStream
|
||||||
|
).to.have.been.calledWithExactly(bucket, key, fileStream)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not send the file to the fallback persistor', function() {
|
||||||
|
expect(fallbackPersistor.promises.sendStream).not.to.have.been.called
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the primary persistor throws an error', function() {
|
||||||
|
it('returns the error', async function() {
|
||||||
|
primaryPersistor.promises.sendStream.rejects(notFoundError)
|
||||||
|
return expect(
|
||||||
|
migrationPersistor.promises.sendStream(bucket, key, fileStream)
|
||||||
|
).to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deleteFile', function() {
|
||||||
|
let primaryPersistor, fallbackPersistor, migrationPersistor
|
||||||
|
beforeEach(function() {
|
||||||
|
primaryPersistor = newPersistor(false)
|
||||||
|
fallbackPersistor = newPersistor(false)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when it works', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
return migrationPersistor.promises.deleteFile(bucket, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the file from the primary', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.deleteFile
|
||||||
|
).to.have.been.calledWithExactly(bucket, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the file from the fallback', function() {
|
||||||
|
expect(
|
||||||
|
fallbackPersistor.promises.deleteFile
|
||||||
|
).to.have.been.calledWithExactly(fallbackBucket, key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the primary persistor throws an error', function() {
|
||||||
|
let error
|
||||||
|
beforeEach(async function() {
|
||||||
|
primaryPersistor.promises.deleteFile.rejects(genericError)
|
||||||
|
try {
|
||||||
|
await migrationPersistor.promises.deleteFile(bucket, key)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the error', function() {
|
||||||
|
expect(error).to.equal(genericError)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the file from the primary', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.deleteFile
|
||||||
|
).to.have.been.calledWithExactly(bucket, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the file from the fallback', function() {
|
||||||
|
expect(
|
||||||
|
fallbackPersistor.promises.deleteFile
|
||||||
|
).to.have.been.calledWithExactly(fallbackBucket, key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the fallback persistor throws an error', function() {
|
||||||
|
let error
|
||||||
|
beforeEach(async function() {
|
||||||
|
fallbackPersistor.promises.deleteFile.rejects(genericError)
|
||||||
|
try {
|
||||||
|
await migrationPersistor.promises.deleteFile(bucket, key)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the error', function() {
|
||||||
|
expect(error).to.equal(genericError)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the file from the primary', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.deleteFile
|
||||||
|
).to.have.been.calledWithExactly(bucket, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete the file from the fallback', function() {
|
||||||
|
expect(
|
||||||
|
fallbackPersistor.promises.deleteFile
|
||||||
|
).to.have.been.calledWithExactly(fallbackBucket, key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('copyFile', function() {
|
||||||
|
describe('when the file exists on the primary', function() {
|
||||||
|
let primaryPersistor, fallbackPersistor, migrationPersistor
|
||||||
|
beforeEach(async function() {
|
||||||
|
primaryPersistor = newPersistor(true)
|
||||||
|
fallbackPersistor = newPersistor(false)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
return migrationPersistor.promises.copyFile(bucket, key, destKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call copyFile to copy the file', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.copyFile
|
||||||
|
).to.have.been.calledWithExactly(bucket, key, destKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not try to read from the fallback', function() {
|
||||||
|
expect(fallbackPersistor.promises.getFileStream).not.to.have.been.called
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the file does not exist on the primary', function() {
|
||||||
|
let primaryPersistor, fallbackPersistor, migrationPersistor
|
||||||
|
beforeEach(async function() {
|
||||||
|
primaryPersistor = newPersistor(false)
|
||||||
|
fallbackPersistor = newPersistor(true)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
return migrationPersistor.promises.copyFile(bucket, key, destKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call copyFile to copy the file', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.copyFile
|
||||||
|
).to.have.been.calledWithExactly(bucket, key, destKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch the file from the fallback', function() {
|
||||||
|
expect(
|
||||||
|
fallbackPersistor.promises.getFileStream
|
||||||
|
).not.to.have.been.calledWithExactly(fallbackBucket, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get the md5 hash from the source', function() {
|
||||||
|
expect(
|
||||||
|
fallbackPersistor.promises.getFileMd5Hash
|
||||||
|
).to.have.been.calledWith(fallbackBucket, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should send the file to the primary', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.sendStream
|
||||||
|
).to.have.been.calledWithExactly(
|
||||||
|
bucket,
|
||||||
|
destKey,
|
||||||
|
sinon.match.instanceOf(Stream.PassThrough),
|
||||||
|
md5
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the file does not exist on the fallback', function() {
|
||||||
|
let primaryPersistor, fallbackPersistor, migrationPersistor, error
|
||||||
|
beforeEach(async function() {
|
||||||
|
primaryPersistor = newPersistor(false)
|
||||||
|
fallbackPersistor = newPersistor(false)
|
||||||
|
migrationPersistor = MigrationPersistor(
|
||||||
|
primaryPersistor,
|
||||||
|
fallbackPersistor
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
await migrationPersistor.promises.copyFile(bucket, key, destKey)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call copyFile to copy the file', function() {
|
||||||
|
expect(
|
||||||
|
primaryPersistor.promises.copyFile
|
||||||
|
).to.have.been.calledWithExactly(bucket, key, destKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch the file from the fallback', function() {
|
||||||
|
expect(
|
||||||
|
fallbackPersistor.promises.getFileStream
|
||||||
|
).not.to.have.been.calledWithExactly(fallbackBucket, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return a not-found error', function() {
|
||||||
|
expect(error).to.be.an.instanceOf(Errors.NotFoundError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -6,18 +6,14 @@ const SandboxedModule = require('sandboxed-module')
|
|||||||
const modulePath = '../../../app/js/PersistorManager.js'
|
const modulePath = '../../../app/js/PersistorManager.js'
|
||||||
|
|
||||||
describe('PersistorManager', function() {
|
describe('PersistorManager', function() {
|
||||||
let PersistorManager,
|
let PersistorManager, FSPersistor, S3Persistor, settings, requires
|
||||||
FSPersistorManager,
|
|
||||||
S3PersistorManager,
|
|
||||||
settings,
|
|
||||||
requires
|
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
FSPersistorManager = {
|
FSPersistor = {
|
||||||
wrappedMethod: sinon.stub().returns('FSPersistorManager')
|
wrappedMethod: sinon.stub().returns('FSPersistor')
|
||||||
}
|
}
|
||||||
S3PersistorManager = {
|
S3Persistor = {
|
||||||
wrappedMethod: sinon.stub().returns('S3PersistorManager')
|
wrappedMethod: sinon.stub().returns('S3Persistor')
|
||||||
}
|
}
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
@@ -25,8 +21,8 @@ describe('PersistorManager', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requires = {
|
requires = {
|
||||||
'./S3PersistorManager': S3PersistorManager,
|
'./S3Persistor': S3Persistor,
|
||||||
'./FSPersistorManager': FSPersistorManager,
|
'./FSPersistor': FSPersistor,
|
||||||
'settings-sharelatex': settings,
|
'settings-sharelatex': settings,
|
||||||
'logger-sharelatex': {
|
'logger-sharelatex': {
|
||||||
log() {},
|
log() {},
|
||||||
@@ -40,7 +36,7 @@ describe('PersistorManager', function() {
|
|||||||
PersistorManager = SandboxedModule.require(modulePath, { requires })
|
PersistorManager = SandboxedModule.require(modulePath, { requires })
|
||||||
|
|
||||||
expect(PersistorManager).to.respondTo('wrappedMethod')
|
expect(PersistorManager).to.respondTo('wrappedMethod')
|
||||||
expect(PersistorManager.wrappedMethod()).to.equal('S3PersistorManager')
|
expect(PersistorManager.wrappedMethod()).to.equal('S3Persistor')
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should implement the S3 wrapped method when 'aws-sdk' is configured", function() {
|
it("should implement the S3 wrapped method when 'aws-sdk' is configured", function() {
|
||||||
@@ -48,7 +44,7 @@ describe('PersistorManager', function() {
|
|||||||
PersistorManager = SandboxedModule.require(modulePath, { requires })
|
PersistorManager = SandboxedModule.require(modulePath, { requires })
|
||||||
|
|
||||||
expect(PersistorManager).to.respondTo('wrappedMethod')
|
expect(PersistorManager).to.respondTo('wrappedMethod')
|
||||||
expect(PersistorManager.wrappedMethod()).to.equal('S3PersistorManager')
|
expect(PersistorManager.wrappedMethod()).to.equal('S3Persistor')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should implement the FS wrapped method when FS is configured', function() {
|
it('should implement the FS wrapped method when FS is configured', function() {
|
||||||
@@ -56,7 +52,7 @@ describe('PersistorManager', function() {
|
|||||||
PersistorManager = SandboxedModule.require(modulePath, { requires })
|
PersistorManager = SandboxedModule.require(modulePath, { requires })
|
||||||
|
|
||||||
expect(PersistorManager).to.respondTo('wrappedMethod')
|
expect(PersistorManager).to.respondTo('wrappedMethod')
|
||||||
expect(PersistorManager.wrappedMethod()).to.equal('FSPersistorManager')
|
expect(PersistorManager.wrappedMethod()).to.equal('FSPersistor')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw an error when the backend is not configured', function() {
|
it('should throw an error when the backend is not configured', function() {
|
||||||
|
|||||||
+176
-78
@@ -1,12 +1,12 @@
|
|||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const chai = require('chai')
|
const chai = require('chai')
|
||||||
const { expect } = chai
|
const { expect } = chai
|
||||||
const modulePath = '../../../app/js/S3PersistorManager.js'
|
const modulePath = '../../../app/js/S3Persistor.js'
|
||||||
const SandboxedModule = require('sandboxed-module')
|
const SandboxedModule = require('sandboxed-module')
|
||||||
|
|
||||||
const Errors = require('../../../app/js/Errors')
|
const Errors = require('../../../app/js/Errors')
|
||||||
|
|
||||||
describe('S3PersistorManagerTests', function() {
|
describe('S3PersistorTests', function() {
|
||||||
const defaultS3Key = 'frog'
|
const defaultS3Key = 'frog'
|
||||||
const defaultS3Secret = 'prince'
|
const defaultS3Secret = 'prince'
|
||||||
const defaultS3Credentials = {
|
const defaultS3Credentials = {
|
||||||
@@ -26,21 +26,26 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
{ Key: 'hippo', Size: 22 }
|
{ Key: 'hippo', Size: 22 }
|
||||||
]
|
]
|
||||||
const filesSize = 33
|
const filesSize = 33
|
||||||
|
const md5 = 'ffffffff00000000ffffffff00000000'
|
||||||
|
|
||||||
let Metrics,
|
let Metrics,
|
||||||
|
Logger,
|
||||||
S3,
|
S3,
|
||||||
Fs,
|
Fs,
|
||||||
Meter,
|
Meter,
|
||||||
MeteredStream,
|
MeteredStream,
|
||||||
ReadStream,
|
ReadStream,
|
||||||
S3PersistorManager,
|
Stream,
|
||||||
|
S3Persistor,
|
||||||
S3Client,
|
S3Client,
|
||||||
S3ReadStream,
|
S3ReadStream,
|
||||||
S3NotFoundError,
|
S3NotFoundError,
|
||||||
S3AccessDeniedError,
|
S3AccessDeniedError,
|
||||||
FileNotFoundError,
|
FileNotFoundError,
|
||||||
EmptyPromise,
|
EmptyPromise,
|
||||||
settings
|
settings,
|
||||||
|
Hash,
|
||||||
|
crypto
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
settings = {
|
settings = {
|
||||||
@@ -48,7 +53,8 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
backend: 's3',
|
backend: 's3',
|
||||||
s3: {
|
s3: {
|
||||||
secret: defaultS3Secret,
|
secret: defaultS3Secret,
|
||||||
key: defaultS3Key
|
key: defaultS3Key,
|
||||||
|
partSize: 100 * 1024 * 1024
|
||||||
},
|
},
|
||||||
stores: {
|
stores: {
|
||||||
user_files: 'sl_user_files'
|
user_files: 'sl_user_files'
|
||||||
@@ -56,6 +62,10 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream = {
|
||||||
|
pipeline: sinon.stub().yields()
|
||||||
|
}
|
||||||
|
|
||||||
EmptyPromise = {
|
EmptyPromise = {
|
||||||
promise: sinon.stub().resolves()
|
promise: sinon.stub().resolves()
|
||||||
}
|
}
|
||||||
@@ -65,7 +75,11 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ReadStream = {
|
ReadStream = {
|
||||||
pipe: sinon.stub().returns('readStream')
|
pipe: sinon.stub().returns('readStream'),
|
||||||
|
on: sinon
|
||||||
|
.stub()
|
||||||
|
.withArgs('end')
|
||||||
|
.yields()
|
||||||
}
|
}
|
||||||
|
|
||||||
FileNotFoundError = new Error('File not found')
|
FileNotFoundError = new Error('File not found')
|
||||||
@@ -76,6 +90,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MeteredStream = {
|
MeteredStream = {
|
||||||
|
type: 'metered',
|
||||||
on: sinon.stub(),
|
on: sinon.stub(),
|
||||||
bytes: objectSize
|
bytes: objectSize
|
||||||
}
|
}
|
||||||
@@ -90,7 +105,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
|
|
||||||
S3ReadStream = {
|
S3ReadStream = {
|
||||||
on: sinon.stub(),
|
on: sinon.stub(),
|
||||||
pipe: sinon.stub().returns('s3Stream'),
|
pipe: sinon.stub(),
|
||||||
removeListener: sinon.stub()
|
removeListener: sinon.stub()
|
||||||
}
|
}
|
||||||
S3ReadStream.on.withArgs('readable').yields()
|
S3ReadStream.on.withArgs('readable').yields()
|
||||||
@@ -100,7 +115,8 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
}),
|
}),
|
||||||
headObject: sinon.stub().returns({
|
headObject: sinon.stub().returns({
|
||||||
promise: sinon.stub().resolves({
|
promise: sinon.stub().resolves({
|
||||||
ContentLength: objectSize
|
ContentLength: objectSize,
|
||||||
|
ETag: md5
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
listObjects: sinon.stub().returns({
|
listObjects: sinon.stub().returns({
|
||||||
@@ -108,21 +124,39 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
Contents: files
|
Contents: files
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
upload: sinon.stub().returns(EmptyPromise),
|
upload: sinon
|
||||||
|
.stub()
|
||||||
|
.returns({ promise: sinon.stub().resolves({ ETag: `"${md5}"` }) }),
|
||||||
copyObject: sinon.stub().returns(EmptyPromise),
|
copyObject: sinon.stub().returns(EmptyPromise),
|
||||||
deleteObject: sinon.stub().returns(EmptyPromise),
|
deleteObject: sinon.stub().returns(EmptyPromise),
|
||||||
deleteObjects: sinon.stub().returns(EmptyPromise)
|
deleteObjects: sinon.stub().returns(EmptyPromise)
|
||||||
}
|
}
|
||||||
S3 = sinon.stub().returns(S3Client)
|
S3 = sinon.stub().returns(S3Client)
|
||||||
|
|
||||||
S3PersistorManager = SandboxedModule.require(modulePath, {
|
Hash = {
|
||||||
|
end: sinon.stub(),
|
||||||
|
read: sinon.stub().returns(md5),
|
||||||
|
setEncoding: sinon.stub()
|
||||||
|
}
|
||||||
|
crypto = {
|
||||||
|
createHash: sinon.stub().returns(Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger = {
|
||||||
|
warn: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
S3Persistor = SandboxedModule.require(modulePath, {
|
||||||
requires: {
|
requires: {
|
||||||
'aws-sdk/clients/s3': S3,
|
'aws-sdk/clients/s3': S3,
|
||||||
'settings-sharelatex': settings,
|
'settings-sharelatex': settings,
|
||||||
|
'logger-sharelatex': Logger,
|
||||||
'./Errors': Errors,
|
'./Errors': Errors,
|
||||||
fs: Fs,
|
fs: Fs,
|
||||||
'stream-meter': Meter,
|
'stream-meter': Meter,
|
||||||
'metrics-sharelatex': Metrics
|
stream: Stream,
|
||||||
|
'metrics-sharelatex': Metrics,
|
||||||
|
crypto
|
||||||
},
|
},
|
||||||
globals: { console }
|
globals: { console }
|
||||||
})
|
})
|
||||||
@@ -133,11 +167,11 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
let stream
|
let stream
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
stream = await S3PersistorManager.promises.getFileStream(bucket, key)
|
stream = await S3Persistor.promises.getFileStream(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a stream', function() {
|
it('returns a metered stream', function() {
|
||||||
expect(stream).to.equal('s3Stream')
|
expect(stream).to.equal(MeteredStream)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets the AWS client up with credentials from settings', function() {
|
it('sets the AWS client up with credentials from settings', function() {
|
||||||
@@ -152,7 +186,10 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('pipes the stream through the meter', function() {
|
it('pipes the stream through the meter', function() {
|
||||||
expect(S3ReadStream.pipe).to.have.been.calledWith(MeteredStream)
|
expect(Stream.pipeline).to.have.been.calledWith(
|
||||||
|
S3ReadStream,
|
||||||
|
MeteredStream
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('records an ingress metric', function() {
|
it('records an ingress metric', function() {
|
||||||
@@ -164,14 +201,14 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
let stream
|
let stream
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
stream = await S3PersistorManager.promises.getFileStream(bucket, key, {
|
stream = await S3Persistor.promises.getFileStream(bucket, key, {
|
||||||
start: 5,
|
start: 5,
|
||||||
end: 10
|
end: 10
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a stream', function() {
|
it('returns a metered stream', function() {
|
||||||
expect(stream).to.equal('s3Stream')
|
expect(stream).to.equal(MeteredStream)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('passes the byte range on to S3', function() {
|
it('passes the byte range on to S3', function() {
|
||||||
@@ -201,11 +238,11 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
auth_secret: alternativeSecret
|
auth_secret: alternativeSecret
|
||||||
}
|
}
|
||||||
|
|
||||||
stream = await S3PersistorManager.promises.getFileStream(bucket, key)
|
stream = await S3Persistor.promises.getFileStream(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a stream', function() {
|
it('returns a metered stream', function() {
|
||||||
expect(stream).to.equal('s3Stream')
|
expect(stream).to.equal(MeteredStream)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets the AWS client up with the alternative credentials', function() {
|
it('sets the AWS client up with the alternative credentials', function() {
|
||||||
@@ -220,16 +257,13 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('caches the credentials', async function() {
|
it('caches the credentials', async function() {
|
||||||
stream = await S3PersistorManager.promises.getFileStream(bucket, key)
|
stream = await S3Persistor.promises.getFileStream(bucket, key)
|
||||||
|
|
||||||
expect(S3).to.have.been.calledOnceWith(alternativeS3Credentials)
|
expect(S3).to.have.been.calledOnceWith(alternativeS3Credentials)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the default credentials for an unknown bucket', async function() {
|
it('uses the default credentials for an unknown bucket', async function() {
|
||||||
stream = await S3PersistorManager.promises.getFileStream(
|
stream = await S3Persistor.promises.getFileStream('anotherBucket', key)
|
||||||
'anotherBucket',
|
|
||||||
key
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(S3).to.have.been.calledTwice
|
expect(S3).to.have.been.calledTwice
|
||||||
expect(S3.firstCall).to.have.been.calledWith(alternativeS3Credentials)
|
expect(S3.firstCall).to.have.been.calledWith(alternativeS3Credentials)
|
||||||
@@ -237,14 +271,8 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('caches the default credentials', async function() {
|
it('caches the default credentials', async function() {
|
||||||
stream = await S3PersistorManager.promises.getFileStream(
|
stream = await S3Persistor.promises.getFileStream('anotherBucket', key)
|
||||||
'anotherBucket',
|
stream = await S3Persistor.promises.getFileStream('anotherBucket', key)
|
||||||
key
|
|
||||||
)
|
|
||||||
stream = await S3PersistorManager.promises.getFileStream(
|
|
||||||
'anotherBucket',
|
|
||||||
key
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(S3).to.have.been.calledTwice
|
expect(S3).to.have.been.calledTwice
|
||||||
expect(S3.firstCall).to.have.been.calledWith(alternativeS3Credentials)
|
expect(S3.firstCall).to.have.been.calledWith(alternativeS3Credentials)
|
||||||
@@ -256,7 +284,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
delete settings.filestore.s3.secret
|
delete settings.filestore.s3.secret
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
S3PersistorManager.promises.getFileStream('anotherBucket', key)
|
S3Persistor.promises.getFileStream('anotherBucket', key)
|
||||||
).to.eventually.be.rejected.and.be.an.instanceOf(Errors.SettingsError)
|
).to.eventually.be.rejected.and.be.an.instanceOf(Errors.SettingsError)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -268,7 +296,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
S3ReadStream.on = sinon.stub()
|
S3ReadStream.on = sinon.stub()
|
||||||
S3ReadStream.on.withArgs('error').yields(S3NotFoundError)
|
S3ReadStream.on.withArgs('error').yields(S3NotFoundError)
|
||||||
try {
|
try {
|
||||||
stream = await S3PersistorManager.promises.getFileStream(bucket, key)
|
stream = await S3Persistor.promises.getFileStream(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -282,12 +310,12 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
expect(error).to.be.an.instanceOf(Errors.NotFoundError)
|
expect(error).to.be.an.instanceOf(Errors.NotFoundError)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('wraps the error from S3', function() {
|
it('wraps the error', function() {
|
||||||
expect(error.cause).to.equal(S3NotFoundError)
|
expect(error.cause).to.exist
|
||||||
})
|
})
|
||||||
|
|
||||||
it('stores the bucket and key in the error', function() {
|
it('stores the bucket and key in the error', function() {
|
||||||
expect(error.info).to.deep.equal({ Bucket: bucket, Key: key })
|
expect(error.info).to.include({ bucketName: bucket, key: key })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -298,7 +326,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
S3ReadStream.on = sinon.stub()
|
S3ReadStream.on = sinon.stub()
|
||||||
S3ReadStream.on.withArgs('error').yields(S3AccessDeniedError)
|
S3ReadStream.on.withArgs('error').yields(S3AccessDeniedError)
|
||||||
try {
|
try {
|
||||||
stream = await S3PersistorManager.promises.getFileStream(bucket, key)
|
stream = await S3Persistor.promises.getFileStream(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -312,12 +340,12 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
expect(error).to.be.an.instanceOf(Errors.NotFoundError)
|
expect(error).to.be.an.instanceOf(Errors.NotFoundError)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('wraps the error from S3', function() {
|
it('wraps the error', function() {
|
||||||
expect(error.cause).to.equal(S3AccessDeniedError)
|
expect(error.cause).to.exist
|
||||||
})
|
})
|
||||||
|
|
||||||
it('stores the bucket and key in the error', function() {
|
it('stores the bucket and key in the error', function() {
|
||||||
expect(error.info).to.deep.equal({ Bucket: bucket, Key: key })
|
expect(error.info).to.include({ bucketName: bucket, key: key })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -328,7 +356,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
S3ReadStream.on = sinon.stub()
|
S3ReadStream.on = sinon.stub()
|
||||||
S3ReadStream.on.withArgs('error').yields(genericError)
|
S3ReadStream.on.withArgs('error').yields(genericError)
|
||||||
try {
|
try {
|
||||||
stream = await S3PersistorManager.promises.getFileStream(bucket, key)
|
stream = await S3Persistor.promises.getFileStream(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -342,12 +370,12 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
expect(error).to.be.an.instanceOf(Errors.ReadError)
|
expect(error).to.be.an.instanceOf(Errors.ReadError)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('wraps the error from S3', function() {
|
it('wraps the error', function() {
|
||||||
expect(error.cause).to.equal(genericError)
|
expect(error.cause).to.exist
|
||||||
})
|
})
|
||||||
|
|
||||||
it('stores the bucket and key in the error', function() {
|
it('stores the bucket and key in the error', function() {
|
||||||
expect(error.info).to.deep.equal({ Bucket: bucket, Key: key })
|
expect(error.info).to.include({ bucketName: bucket, key: key })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -357,7 +385,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
let size
|
let size
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
size = await S3PersistorManager.promises.getFileSize(bucket, key)
|
size = await S3Persistor.promises.getFileSize(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return the object size', function() {
|
it('should return the object size', function() {
|
||||||
@@ -380,7 +408,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
promise: sinon.stub().rejects(S3NotFoundError)
|
promise: sinon.stub().rejects(S3NotFoundError)
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.getFileSize(bucket, key)
|
await S3Persistor.promises.getFileSize(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -403,7 +431,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
promise: sinon.stub().rejects(genericError)
|
promise: sinon.stub().rejects(genericError)
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.getFileSize(bucket, key)
|
await S3Persistor.promises.getFileSize(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -422,24 +450,68 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
describe('sendStream', function() {
|
describe('sendStream', function() {
|
||||||
describe('with valid parameters', function() {
|
describe('with valid parameters', function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
return S3PersistorManager.promises.sendStream(bucket, key, ReadStream)
|
return S3Persistor.promises.sendStream(bucket, key, ReadStream)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should upload the stream', function() {
|
it('should upload the stream', function() {
|
||||||
expect(S3Client.upload).to.have.been.calledWith({
|
expect(S3Client.upload).to.have.been.calledWith({
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
Body: 'readStream'
|
Body: MeteredStream
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should upload files in a single part', function() {
|
||||||
|
expect(S3Client.upload).to.have.been.calledWith(sinon.match.any, {
|
||||||
|
partSize: 100 * 1024 * 1024
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should meter the stream', function() {
|
it('should meter the stream', function() {
|
||||||
expect(ReadStream.pipe).to.have.been.calledWith(MeteredStream)
|
expect(Stream.pipeline).to.have.been.calledWith(
|
||||||
|
ReadStream,
|
||||||
|
MeteredStream
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should record an egress metric', function() {
|
it('should record an egress metric', function() {
|
||||||
expect(Metrics.count).to.have.been.calledWith('s3.egress', objectSize)
|
expect(Metrics.count).to.have.been.calledWith('s3.egress', objectSize)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('calculates the md5 hash of the file', function() {
|
||||||
|
expect(Stream.pipeline).to.have.been.calledWith(ReadStream, Hash)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when a hash is supploed', function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
return S3Persistor.promises.sendStream(
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
ReadStream,
|
||||||
|
'aaaaaaaabbbbbbbbaaaaaaaabbbbbbbb'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not calculate the md5 hash of the file', function() {
|
||||||
|
expect(Stream.pipeline).not.to.have.been.calledWith(
|
||||||
|
sinon.match.any,
|
||||||
|
Hash
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends the hash in base64', function() {
|
||||||
|
expect(S3Client.upload).to.have.been.calledWith({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: key,
|
||||||
|
Body: MeteredStream,
|
||||||
|
ContentMD5: 'qqqqqru7u7uqqqqqu7u7uw=='
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not fetch the md5 hash of the uploaded file', function() {
|
||||||
|
expect(S3Client.headObject).not.to.have.been.called
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the upload fails', function() {
|
describe('when the upload fails', function() {
|
||||||
@@ -449,7 +521,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
promise: sinon.stub().rejects(genericError)
|
promise: sinon.stub().rejects(genericError)
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.sendStream(bucket, key, ReadStream)
|
await S3Persistor.promises.sendStream(bucket, key, ReadStream)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -459,12 +531,44 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
expect(error).to.be.an.instanceOf(Errors.WriteError)
|
expect(error).to.be.an.instanceOf(Errors.WriteError)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("when the etag isn't a valid md5 hash", function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
S3Client.upload = sinon.stub().returns({
|
||||||
|
promise: sinon.stub().resolves({
|
||||||
|
ETag: 'somethingthatisntanmd5',
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: key
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await S3Persistor.promises.sendStream(bucket, key, ReadStream)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should re-fetch the file to verify it', function() {
|
||||||
|
expect(S3Client.getObject).to.have.been.calledWith({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: key
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should meter the download', function() {
|
||||||
|
expect(Stream.pipeline).to.have.been.calledWith(
|
||||||
|
S3ReadStream,
|
||||||
|
MeteredStream
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should calculate the md5 hash from the file', function() {
|
||||||
|
expect(Stream.pipeline).to.have.been.calledWith(MeteredStream, Hash)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sendFile', function() {
|
describe('sendFile', function() {
|
||||||
describe('with valid parameters', function() {
|
describe('with valid parameters', function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
return S3PersistorManager.promises.sendFile(bucket, key, filename)
|
return S3Persistor.promises.sendFile(bucket, key, filename)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should create a read stream for the file', function() {
|
it('should create a read stream for the file', function() {
|
||||||
@@ -475,7 +579,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
expect(S3Client.upload).to.have.been.calledWith({
|
expect(S3Client.upload).to.have.been.calledWith({
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
Body: 'readStream'
|
Body: MeteredStream
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -486,7 +590,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
Fs.createReadStream = sinon.stub().throws(FileNotFoundError)
|
Fs.createReadStream = sinon.stub().throws(FileNotFoundError)
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.sendFile(bucket, key, filename)
|
await S3Persistor.promises.sendFile(bucket, key, filename)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -507,7 +611,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
Fs.createReadStream = sinon.stub().throws(genericError)
|
Fs.createReadStream = sinon.stub().throws(genericError)
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.sendFile(bucket, key, filename)
|
await S3Persistor.promises.sendFile(bucket, key, filename)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -526,7 +630,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
describe('copyFile', function() {
|
describe('copyFile', function() {
|
||||||
describe('with valid parameters', function() {
|
describe('with valid parameters', function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
return S3PersistorManager.promises.copyFile(bucket, key, destKey)
|
return S3Persistor.promises.copyFile(bucket, key, destKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should copy the object', function() {
|
it('should copy the object', function() {
|
||||||
@@ -546,7 +650,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
promise: sinon.stub().rejects(S3NotFoundError)
|
promise: sinon.stub().rejects(S3NotFoundError)
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.copyFile(bucket, key, destKey)
|
await S3Persistor.promises.copyFile(bucket, key, destKey)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -561,7 +665,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
describe('deleteFile', function() {
|
describe('deleteFile', function() {
|
||||||
describe('with valid parameters', function() {
|
describe('with valid parameters', function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
return S3PersistorManager.promises.deleteFile(bucket, key)
|
return S3Persistor.promises.deleteFile(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should delete the object', function() {
|
it('should delete the object', function() {
|
||||||
@@ -580,7 +684,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
promise: sinon.stub().rejects(S3NotFoundError)
|
promise: sinon.stub().rejects(S3NotFoundError)
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.deleteFile(bucket, key)
|
await S3Persistor.promises.deleteFile(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -595,7 +699,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
describe('deleteDirectory', function() {
|
describe('deleteDirectory', function() {
|
||||||
describe('with valid parameters', function() {
|
describe('with valid parameters', function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
return S3PersistorManager.promises.deleteDirectory(bucket, key)
|
return S3Persistor.promises.deleteDirectory(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should list the objects in the directory', function() {
|
it('should list the objects in the directory', function() {
|
||||||
@@ -621,7 +725,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
S3Client.listObjects = sinon
|
S3Client.listObjects = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.returns({ promise: sinon.stub().resolves({ Contents: [] }) })
|
.returns({ promise: sinon.stub().resolves({ Contents: [] }) })
|
||||||
return S3PersistorManager.promises.deleteDirectory(bucket, key)
|
return S3Persistor.promises.deleteDirectory(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should list the objects in the directory', function() {
|
it('should list the objects in the directory', function() {
|
||||||
@@ -644,7 +748,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
.stub()
|
.stub()
|
||||||
.returns({ promise: sinon.stub().rejects(genericError) })
|
.returns({ promise: sinon.stub().rejects(genericError) })
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.deleteDirectory(bucket, key)
|
await S3Persistor.promises.deleteDirectory(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -671,7 +775,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
.stub()
|
.stub()
|
||||||
.returns({ promise: sinon.stub().rejects(genericError) })
|
.returns({ promise: sinon.stub().rejects(genericError) })
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.deleteDirectory(bucket, key)
|
await S3Persistor.promises.deleteDirectory(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -692,7 +796,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
let size
|
let size
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
size = await S3PersistorManager.promises.directorySize(bucket, key)
|
size = await S3Persistor.promises.directorySize(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should list the objects in the directory', function() {
|
it('should list the objects in the directory', function() {
|
||||||
@@ -714,7 +818,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
S3Client.listObjects = sinon
|
S3Client.listObjects = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.returns({ promise: sinon.stub().resolves({ Contents: [] }) })
|
.returns({ promise: sinon.stub().resolves({ Contents: [] }) })
|
||||||
size = await S3PersistorManager.promises.directorySize(bucket, key)
|
size = await S3Persistor.promises.directorySize(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should list the objects in the directory', function() {
|
it('should list the objects in the directory', function() {
|
||||||
@@ -737,7 +841,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
.stub()
|
.stub()
|
||||||
.returns({ promise: sinon.stub().rejects(genericError) })
|
.returns({ promise: sinon.stub().rejects(genericError) })
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.directorySize(bucket, key)
|
await S3Persistor.promises.directorySize(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
@@ -758,10 +862,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
let exists
|
let exists
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
exists = await S3PersistorManager.promises.checkIfFileExists(
|
exists = await S3Persistor.promises.checkIfFileExists(bucket, key)
|
||||||
bucket,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get the object header', function() {
|
it('should get the object header', function() {
|
||||||
@@ -783,10 +884,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
S3Client.headObject = sinon
|
S3Client.headObject = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.returns({ promise: sinon.stub().rejects(S3NotFoundError) })
|
.returns({ promise: sinon.stub().rejects(S3NotFoundError) })
|
||||||
exists = await S3PersistorManager.promises.checkIfFileExists(
|
exists = await S3Persistor.promises.checkIfFileExists(bucket, key)
|
||||||
bucket,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get the object header', function() {
|
it('should get the object header', function() {
|
||||||
@@ -809,7 +907,7 @@ describe('S3PersistorManagerTests', function() {
|
|||||||
.stub()
|
.stub()
|
||||||
.returns({ promise: sinon.stub().rejects(genericError) })
|
.returns({ promise: sinon.stub().rejects(genericError) })
|
||||||
try {
|
try {
|
||||||
await S3PersistorManager.promises.checkIfFileExists(bucket, key)
|
await S3Persistor.promises.checkIfFileExists(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user