[misc] promisify FileHandler and remove dependency on async

Signed-off-by: Jakob Ackermann <das7pad@outlook.com>
This commit is contained in:
Jakob Ackermann 2020-02-23 22:34:40 +00:00
parent 428dcdf0ee
commit 2b15729658
4 changed files with 180 additions and 187 deletions

View file

@ -1,104 +1,87 @@
const { promisify } = require('util') const { callbackify } = require('util')
const fs = require('fs') const fs = require('fs')
const PersistorManager = require('./PersistorManager') const PersistorManager = require('./PersistorManager')
const LocalFileWriter = require('./LocalFileWriter') const LocalFileWriter = require('./LocalFileWriter')
const FileConverter = require('./FileConverter') const FileConverter = require('./FileConverter')
const KeyBuilder = require('./KeyBuilder') const KeyBuilder = require('./KeyBuilder')
const async = require('async')
const ImageOptimiser = require('./ImageOptimiser') const ImageOptimiser = require('./ImageOptimiser')
const { ConversionError } = require('./Errors') const { ConversionError } = require('./Errors')
module.exports = { module.exports = {
insertFile: callbackify(insertFile),
deleteFile: callbackify(deleteFile),
getFile: callbackify(getFile),
getFileSize: callbackify(getFileSize),
getDirectorySize: callbackify(getDirectorySize),
promises: {
getFile,
insertFile, insertFile,
deleteFile, deleteFile,
getFile,
getFileSize, getFileSize,
getDirectorySize, getDirectorySize
promises: {
getFile: promisify(getFile),
insertFile: promisify(insertFile),
deleteFile: promisify(deleteFile),
getFileSize: promisify(getFileSize),
getDirectorySize: promisify(getDirectorySize)
} }
} }
function insertFile(bucket, key, stream, callback) { async function insertFile(bucket, key, stream) {
const convertedKey = KeyBuilder.getConvertedFolderKey(key) const convertedKey = KeyBuilder.getConvertedFolderKey(key)
PersistorManager.deleteDirectory(bucket, convertedKey, function(error) { await PersistorManager.promises.deleteDirectory(bucket, convertedKey)
if (error) { await PersistorManager.promises.sendStream(bucket, key, stream)
return callback(error)
}
PersistorManager.sendStream(bucket, key, stream, callback)
})
} }
function deleteFile(bucket, key, callback) { async function deleteFile(bucket, key) {
const convertedKey = KeyBuilder.getConvertedFolderKey(key) const convertedKey = KeyBuilder.getConvertedFolderKey(key)
async.parallel( await Promise.all([
[ PersistorManager.promises.deleteFile(bucket, key),
done => PersistorManager.deleteFile(bucket, key, done), PersistorManager.promises.deleteDirectory(bucket, convertedKey)
done => PersistorManager.deleteDirectory(bucket, convertedKey, done) ])
],
callback
)
} }
function getFile(bucket, key, opts, callback) { async function getFile(bucket, key, opts) {
opts = opts || {} opts = opts || {}
if (!opts.format && !opts.style) { if (!opts.format && !opts.style) {
PersistorManager.getFileStream(bucket, key, opts, callback) return PersistorManager.promises.getFileStream(bucket, key, opts)
} else { } else {
_getConvertedFile(bucket, key, opts, callback) return _getConvertedFile(bucket, key, opts)
} }
} }
function getFileSize(bucket, key, callback) { async function getFileSize(bucket, key) {
PersistorManager.getFileSize(bucket, key, callback) return PersistorManager.promises.getFileSize(bucket, key)
} }
function getDirectorySize(bucket, projectId, callback) { async function getDirectorySize(bucket, projectId) {
PersistorManager.directorySize(bucket, projectId, callback) return PersistorManager.promises.directorySize(bucket, projectId)
} }
function _getConvertedFile(bucket, key, opts, callback) { async function _getConvertedFile(bucket, key, opts) {
const convertedKey = KeyBuilder.addCachingToKey(key, opts) const convertedKey = KeyBuilder.addCachingToKey(key, opts)
PersistorManager.checkIfFileExists(bucket, convertedKey, (err, exists) => { const exists = await PersistorManager.promises.checkIfFileExists(
if (err) { bucket,
return callback(err) convertedKey
} )
if (exists) { if (exists) {
PersistorManager.getFileStream(bucket, convertedKey, opts, callback) return PersistorManager.promises.getFileStream(bucket, convertedKey, opts)
} else { } else {
_getConvertedFileAndCache(bucket, key, convertedKey, opts, callback) return _getConvertedFileAndCache(bucket, key, convertedKey, opts)
} }
})
} }
function _getConvertedFileAndCache(bucket, key, convertedKey, opts, callback) { async function _getConvertedFileAndCache(bucket, key, convertedKey, opts) {
let convertedFsPath let convertedFsPath
try {
async.series( convertedFsPath = await _convertFile(bucket, key, opts)
[ await ImageOptimiser.promises.compressPng(convertedFsPath)
cb => { await PersistorManager.promises.sendFile(
_convertFile(bucket, key, opts, function(err, fileSystemPath) { bucket,
convertedFsPath = fileSystemPath convertedKey,
cb(err) convertedFsPath
}) )
}, } catch (err) {
cb => ImageOptimiser.compressPng(convertedFsPath, cb), LocalFileWriter.deleteFile(convertedFsPath, () => {})
cb => PersistorManager.sendFile(bucket, convertedKey, convertedFsPath, cb) throw new ConversionError({
],
function(err) {
if (err) {
LocalFileWriter.deleteFile(convertedFsPath, function() {})
return callback(
new ConversionError({
message: 'failed to convert file', message: 'failed to convert file',
info: { opts, bucket, key, convertedKey } info: { opts, bucket, key, convertedKey }
}).withCause(err) }).withCause(err)
)
} }
// Send back the converted file from the local copy to avoid problems // Send back the converted file from the local copy to avoid problems
// with the file not being present in S3 yet. As described in the // with the file not being present in S3 yet. As described in the
@ -116,44 +99,29 @@ function _getConvertedFileAndCache(bucket, key, convertedKey, opts, callback) {
readStream.on('end', function() { readStream.on('end', function() {
LocalFileWriter.deleteFile(convertedFsPath, function() {}) LocalFileWriter.deleteFile(convertedFsPath, function() {})
}) })
callback(null, readStream) return readStream
}
)
} }
function _convertFile(bucket, originalKey, opts, callback) { async function _convertFile(bucket, originalKey, opts) {
_writeFileToDisk(bucket, originalKey, opts, function(err, originalFsPath) { let originalFsPath
if (err) { try {
return callback( originalFsPath = await _writeFileToDisk(bucket, originalKey, opts)
new ConversionError({ } catch (err) {
throw new ConversionError({
message: 'unable to write file to disk', message: 'unable to write file to disk',
info: { bucket, originalKey, opts } info: { bucket, originalKey, opts }
}).withCause(err) }).withCause(err)
)
}
const done = function(err, destPath) {
if (err) {
return callback(
new ConversionError({
message: 'error converting file',
info: { bucket, originalKey, opts }
}).withCause(err)
)
}
LocalFileWriter.deleteFile(originalFsPath, function() {})
callback(err, destPath)
} }
let promise
if (opts.format) { if (opts.format) {
FileConverter.convert(originalFsPath, opts.format, done) promise = FileConverter.promises.convert(originalFsPath, opts.format)
} else if (opts.style === 'thumbnail') { } else if (opts.style === 'thumbnail') {
FileConverter.thumbnail(originalFsPath, done) promise = FileConverter.promises.thumbnail(originalFsPath)
} else if (opts.style === 'preview') { } else if (opts.style === 'preview') {
FileConverter.preview(originalFsPath, done) promise = FileConverter.promises.preview(originalFsPath)
} else { } else {
callback( throw new ConversionError({
new ConversionError({
message: 'invalid file conversion options', message: 'invalid file conversion options',
info: { info: {
bucket, bucket,
@ -161,16 +129,25 @@ function _convertFile(bucket, originalKey, opts, callback) {
opts opts
} }
}) })
)
} }
}) let destPath
try {
destPath = await promise
} catch (err) {
throw new ConversionError({
message: 'error converting file',
info: { bucket, originalKey, opts }
}).withCause(err)
}
LocalFileWriter.deleteFile(originalFsPath, function() {})
return destPath
} }
function _writeFileToDisk(bucket, key, opts, callback) { async function _writeFileToDisk(bucket, key, opts) {
PersistorManager.getFileStream(bucket, key, opts, function(err, fileStream) { const fileStream = await PersistorManager.promises.getFileStream(
if (err) { bucket,
return callback(err) key,
} opts
LocalFileWriter.writeStream(fileStream, key, callback) )
}) return LocalFileWriter.promises.writeStream(fileStream, key)
} }

View file

@ -1256,11 +1256,6 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
"dev": true "dev": true
}, },
"async": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
},
"async-listener": { "async-listener": {
"version": "0.6.10", "version": "0.6.10",
"resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz",

View file

@ -21,7 +21,6 @@
}, },
"dependencies": { "dependencies": {
"@overleaf/o-error": "^2.1.0", "@overleaf/o-error": "^2.1.0",
"async": "~0.2.10",
"aws-sdk": "^2.628.0", "aws-sdk": "^2.628.0",
"body-parser": "^1.2.0", "body-parser": "^1.2.0",
"express": "^4.2.0", "express": "^4.2.0",

View file

@ -4,6 +4,9 @@ const { expect } = chai
const modulePath = '../../../app/js/FileHandler.js' const modulePath = '../../../app/js/FileHandler.js'
const SandboxedModule = require('sandboxed-module') const SandboxedModule = require('sandboxed-module')
chai.use(require('sinon-chai'))
chai.use(require('chai-as-promised'))
describe('FileHandler', function() { describe('FileHandler', function() {
let PersistorManager, let PersistorManager,
LocalFileWriter, LocalFileWriter,
@ -32,29 +35,41 @@ describe('FileHandler', function() {
beforeEach(function() { beforeEach(function() {
PersistorManager = { PersistorManager = {
getFileStream: sinon.stub().yields(null, sourceStream), promises: {
checkIfFileExists: sinon.stub().yields(), getFileStream: sinon.stub().resolves(sourceStream),
deleteFile: sinon.stub().yields(), checkIfFileExists: sinon.stub().resolves(),
deleteDirectory: sinon.stub().yields(), deleteFile: sinon.stub().resolves(),
sendStream: sinon.stub().yields(), deleteDirectory: sinon.stub().resolves(),
insertFile: sinon.stub().yields(), sendStream: sinon.stub().resolves(),
sendFile: sinon.stub().yields(), insertFile: sinon.stub().resolves(),
directorySize: sinon.stub().yields() sendFile: sinon.stub().resolves(),
directorySize: sinon.stub().resolves()
}
} }
LocalFileWriter = { LocalFileWriter = {
writeStream: sinon.stub().yields(), // the callback style is used for detached cleanup calls
deleteFile: sinon.stub().yields() deleteFile: sinon.stub().yields(),
promises: {
writeStream: sinon.stub().resolves(),
deleteFile: sinon.stub().resolves()
}
} }
FileConverter = { FileConverter = {
convert: sinon.stub().yields(), promises: {
thumbnail: sinon.stub().yields(), convert: sinon.stub().resolves(),
preview: sinon.stub().yields() thumbnail: sinon.stub().resolves(),
preview: sinon.stub().resolves()
}
} }
KeyBuilder = { KeyBuilder = {
addCachingToKey: sinon.stub().returns(convertedKey), addCachingToKey: sinon.stub().returns(convertedKey),
getConvertedFolderKey: sinon.stub().returns(convertedFolderKey) getConvertedFolderKey: sinon.stub().returns(convertedFolderKey)
} }
ImageOptimiser = { compressPng: sinon.stub().yields() } ImageOptimiser = {
promises: {
compressPng: sinon.stub().resolves()
}
}
fs = { fs = {
createReadStream: sinon.stub().returns(readStream) createReadStream: sinon.stub().returns(readStream)
} }
@ -79,7 +94,7 @@ describe('FileHandler', function() {
it('should send file to the filestore', function(done) { it('should send file to the filestore', function(done) {
FileHandler.insertFile(bucket, key, stream, err => { FileHandler.insertFile(bucket, key, stream, err => {
expect(err).not.to.exist expect(err).not.to.exist
expect(PersistorManager.sendStream).to.have.been.calledWith( expect(PersistorManager.promises.sendStream).to.have.been.calledWith(
bucket, bucket,
key, key,
stream stream
@ -91,10 +106,9 @@ describe('FileHandler', function() {
it('should delete the convertedKey folder', function(done) { it('should delete the convertedKey folder', function(done) {
FileHandler.insertFile(bucket, key, stream, err => { FileHandler.insertFile(bucket, key, stream, err => {
expect(err).not.to.exist expect(err).not.to.exist
expect(PersistorManager.deleteDirectory).to.have.been.calledWith( expect(
bucket, PersistorManager.promises.deleteDirectory
convertedFolderKey ).to.have.been.calledWith(bucket, convertedFolderKey)
)
done() done()
}) })
}) })
@ -104,7 +118,10 @@ describe('FileHandler', function() {
it('should tell the filestore manager to delete the file', function(done) { it('should tell the filestore manager to delete the file', function(done) {
FileHandler.deleteFile(bucket, key, err => { FileHandler.deleteFile(bucket, key, err => {
expect(err).not.to.exist expect(err).not.to.exist
expect(PersistorManager.deleteFile).to.have.been.calledWith(bucket, key) expect(PersistorManager.promises.deleteFile).to.have.been.calledWith(
bucket,
key
)
done() done()
}) })
}) })
@ -112,10 +129,9 @@ describe('FileHandler', function() {
it('should tell the filestore manager to delete the cached folder', function(done) { it('should tell the filestore manager to delete the cached folder', function(done) {
FileHandler.deleteFile(bucket, key, err => { FileHandler.deleteFile(bucket, key, err => {
expect(err).not.to.exist expect(err).not.to.exist
expect(PersistorManager.deleteDirectory).to.have.been.calledWith( expect(
bucket, PersistorManager.promises.deleteDirectory
convertedFolderKey ).to.have.been.calledWith(bucket, convertedFolderKey)
)
done() done()
}) })
}) })
@ -134,7 +150,7 @@ describe('FileHandler', function() {
const options = { start: 0, end: 8 } const options = { start: 0, end: 8 }
FileHandler.getFile(bucket, key, options, err => { FileHandler.getFile(bucket, key, options, err => {
expect(err).not.to.exist expect(err).not.to.exist
expect(PersistorManager.getFileStream).to.have.been.calledWith( expect(PersistorManager.promises.getFileStream).to.have.been.calledWith(
bucket, bucket,
key, key,
options options
@ -155,23 +171,27 @@ describe('FileHandler', function() {
}) })
it('should convert the file', function() { it('should convert the file', function() {
expect(FileConverter.convert).to.have.been.called expect(FileConverter.promises.convert).to.have.been.called
expect(ImageOptimiser.compressPng).to.have.been.called })
it('should compress the converted file', function() {
expect(ImageOptimiser.promises.compressPng).to.have.been.called
}) })
it('should return the the converted stream', function() { it('should return the the converted stream', function() {
expect(result.err).not.to.exist expect(result.err).not.to.exist
expect(result.stream).to.equal(readStream) expect(result.stream).to.equal(readStream)
expect(PersistorManager.getFileStream).to.have.been.calledWith( expect(
bucket, PersistorManager.promises.getFileStream
key ).to.have.been.calledWith(bucket, key)
)
}) })
}) })
describe('when the file is cached', function() { describe('when the file is cached', function() {
beforeEach(function(done) { beforeEach(function(done) {
PersistorManager.checkIfFileExists = sinon.stub().yields(null, true) PersistorManager.promises.checkIfFileExists = sinon
.stub()
.resolves(true)
FileHandler.getFile(bucket, key, { format: 'png' }, (err, stream) => { FileHandler.getFile(bucket, key, { format: 'png' }, (err, stream) => {
result = { err, stream } result = { err, stream }
done() done()
@ -179,17 +199,19 @@ describe('FileHandler', function() {
}) })
it('should not convert the file', function() { it('should not convert the file', function() {
expect(FileConverter.convert).not.to.have.been.called expect(FileConverter.promises.convert).not.to.have.been.called
expect(ImageOptimiser.compressPng).not.to.have.been.called })
it('should not compress the converted file again', function() {
expect(ImageOptimiser.promises.compressPng).not.to.have.been.called
}) })
it('should return the cached stream', function() { it('should return the cached stream', function() {
expect(result.err).not.to.exist expect(result.err).not.to.exist
expect(result.stream).to.equal(sourceStream) expect(result.stream).to.equal(sourceStream)
expect(PersistorManager.getFileStream).to.have.been.calledWith( expect(
bucket, PersistorManager.promises.getFileStream
convertedKey ).to.have.been.calledWith(bucket, convertedKey)
)
}) })
}) })
}) })
@ -198,8 +220,8 @@ describe('FileHandler', function() {
it('generates a thumbnail when requested', function(done) { it('generates a thumbnail when requested', function(done) {
FileHandler.getFile(bucket, key, { style: 'thumbnail' }, err => { FileHandler.getFile(bucket, key, { style: 'thumbnail' }, err => {
expect(err).not.to.exist expect(err).not.to.exist
expect(FileConverter.thumbnail).to.have.been.called expect(FileConverter.promises.thumbnail).to.have.been.called
expect(FileConverter.preview).not.to.have.been.called expect(FileConverter.promises.preview).not.to.have.been.called
done() done()
}) })
}) })
@ -207,8 +229,8 @@ describe('FileHandler', function() {
it('generates a preview when requested', function(done) { it('generates a preview when requested', function(done) {
FileHandler.getFile(bucket, key, { style: 'preview' }, err => { FileHandler.getFile(bucket, key, { style: 'preview' }, err => {
expect(err).not.to.exist expect(err).not.to.exist
expect(FileConverter.thumbnail).not.to.have.been.called expect(FileConverter.promises.thumbnail).not.to.have.been.called
expect(FileConverter.preview).to.have.been.called expect(FileConverter.promises.preview).to.have.been.called
done() done()
}) })
}) })
@ -219,7 +241,7 @@ describe('FileHandler', function() {
it('should call the filestore manager to get directory size', function(done) { it('should call the filestore manager to get directory size', function(done) {
FileHandler.getDirectorySize(bucket, key, err => { FileHandler.getDirectorySize(bucket, key, err => {
expect(err).not.to.exist expect(err).not.to.exist
expect(PersistorManager.directorySize).to.have.been.calledWith( expect(PersistorManager.promises.directorySize).to.have.been.calledWith(
bucket, bucket,
key key
) )