2019-12-16 06:20:29 -05:00
|
|
|
const sinon = require('sinon')
|
|
|
|
const chai = require('chai')
|
|
|
|
const { expect } = chai
|
|
|
|
const SandboxedModule = require('sandboxed-module')
|
2020-01-02 06:29:28 -05:00
|
|
|
const Errors = require('../../../app/js/Errors')
|
|
|
|
|
|
|
|
chai.use(require('sinon-chai'))
|
|
|
|
chai.use(require('chai-as-promised'))
|
|
|
|
|
2020-02-17 09:04:42 -05:00
|
|
|
const modulePath = '../../../app/js/FSPersistor.js'
|
2019-12-16 06:20:29 -05:00
|
|
|
|
2020-02-17 09:04:42 -05:00
|
|
|
describe('FSPersistorTests', function() {
|
2020-01-06 09:09:28 -05:00
|
|
|
const stat = { size: 4, isFile: sinon.stub().returns(true) }
|
2020-01-02 06:29:28 -05:00
|
|
|
const fd = 1234
|
|
|
|
const writeStream = 'writeStream'
|
|
|
|
const remoteStream = 'remoteStream'
|
|
|
|
const tempFile = '/tmp/potato.txt'
|
|
|
|
const location = '/foo'
|
|
|
|
const error = new Error('guru meditation error')
|
2020-02-17 09:04:42 -05:00
|
|
|
const md5 = 'ffffffff'
|
2020-01-02 06:29:28 -05:00
|
|
|
|
2020-01-06 09:09:28 -05:00
|
|
|
const files = ['animals/wombat.tex', 'vegetables/potato.tex']
|
2020-01-06 10:58:57 -05:00
|
|
|
const globs = [`${location}/${files[0]}`, `${location}/${files[1]}`]
|
2020-01-06 09:09:28 -05:00
|
|
|
const filteredFilenames = ['animals_wombat.tex', 'vegetables_potato.tex']
|
2020-02-17 09:04:42 -05:00
|
|
|
let fs,
|
|
|
|
rimraf,
|
|
|
|
stream,
|
|
|
|
LocalFileWriter,
|
|
|
|
FSPersistor,
|
|
|
|
glob,
|
|
|
|
readStream,
|
|
|
|
crypto,
|
|
|
|
Hash
|
2020-01-02 06:29:28 -05:00
|
|
|
|
2019-12-16 06:20:22 -05:00
|
|
|
beforeEach(function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
readStream = {
|
|
|
|
name: 'readStream',
|
|
|
|
on: sinon.stub().yields(),
|
|
|
|
pipe: sinon.stub()
|
|
|
|
}
|
2020-01-02 06:29:28 -05:00
|
|
|
fs = {
|
|
|
|
createReadStream: sinon.stub().returns(readStream),
|
|
|
|
createWriteStream: sinon.stub().returns(writeStream),
|
|
|
|
unlink: sinon.stub().yields(),
|
|
|
|
open: sinon.stub().yields(null, fd),
|
|
|
|
stat: sinon.stub().yields(null, stat)
|
2019-12-16 06:20:29 -05:00
|
|
|
}
|
2020-01-06 10:58:57 -05:00
|
|
|
glob = sinon.stub().yields(null, globs)
|
2020-01-02 06:29:28 -05:00
|
|
|
rimraf = sinon.stub().yields()
|
|
|
|
stream = { pipeline: sinon.stub().yields() }
|
|
|
|
LocalFileWriter = {
|
|
|
|
promises: {
|
|
|
|
writeStream: sinon.stub().resolves(tempFile),
|
|
|
|
deleteFile: sinon.stub().resolves()
|
|
|
|
}
|
2019-12-16 06:20:29 -05:00
|
|
|
}
|
2020-02-17 09:04:42 -05:00
|
|
|
Hash = {
|
|
|
|
end: sinon.stub(),
|
|
|
|
read: sinon.stub().returns(md5),
|
|
|
|
setEncoding: sinon.stub()
|
|
|
|
}
|
|
|
|
crypto = {
|
|
|
|
createHash: sinon.stub().returns(Hash)
|
|
|
|
}
|
|
|
|
FSPersistor = SandboxedModule.require(modulePath, {
|
2020-01-02 06:29:28 -05:00
|
|
|
requires: {
|
|
|
|
'./LocalFileWriter': LocalFileWriter,
|
2020-01-06 09:09:28 -05:00
|
|
|
'./Errors': Errors,
|
|
|
|
fs,
|
|
|
|
glob,
|
|
|
|
rimraf,
|
2020-02-17 09:04:42 -05:00
|
|
|
stream,
|
|
|
|
crypto,
|
|
|
|
// imported by PersistorHelper but otherwise unused here
|
|
|
|
'stream-meter': {},
|
2020-03-04 11:17:36 -05:00
|
|
|
'logger-sharelatex': {},
|
|
|
|
'metrics-sharelatex': {}
|
2019-12-16 06:20:22 -05:00
|
|
|
},
|
2020-01-02 06:29:28 -05:00
|
|
|
globals: { console }
|
|
|
|
})
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
describe('sendFile', function() {
|
2020-01-06 09:09:28 -05:00
|
|
|
const localFilesystemPath = '/path/to/local/file'
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should copy the file', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.sendFile(
|
2020-01-06 09:09:28 -05:00
|
|
|
location,
|
|
|
|
files[0],
|
|
|
|
localFilesystemPath
|
|
|
|
)
|
|
|
|
expect(fs.createReadStream).to.have.been.calledWith(localFilesystemPath)
|
2020-01-02 06:29:28 -05:00
|
|
|
expect(fs.createWriteStream).to.have.been.calledWith(
|
2020-01-06 09:09:28 -05:00
|
|
|
`${location}/${filteredFilenames[0]}`
|
2019-12-16 06:20:29 -05:00
|
|
|
)
|
2020-01-02 06:29:28 -05:00
|
|
|
expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
2019-12-16 06:20:22 -05:00
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should return an error if the file cannot be stored', async function() {
|
|
|
|
stream.pipeline.yields(error)
|
|
|
|
await expect(
|
2020-02-17 09:04:42 -05:00
|
|
|
FSPersistor.promises.sendFile(location, files[0], localFilesystemPath)
|
2020-01-06 10:11:35 -05:00
|
|
|
).to.eventually.be.rejected.and.have.property('cause', error)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
})
|
2019-12-16 06:20:22 -05:00
|
|
|
|
2019-12-16 06:20:29 -05:00
|
|
|
describe('sendStream', function() {
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should send the stream to LocalFileWriter', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.sendStream(location, files[0], remoteStream)
|
2020-01-02 06:29:28 -05:00
|
|
|
expect(LocalFileWriter.promises.writeStream).to.have.been.calledWith(
|
|
|
|
remoteStream
|
|
|
|
)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should delete the temporary file', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.sendStream(location, files[0], remoteStream)
|
2020-01-02 06:29:28 -05:00
|
|
|
expect(LocalFileWriter.promises.deleteFile).to.have.been.calledWith(
|
|
|
|
tempFile
|
2019-12-16 06:20:29 -05:00
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should return the error from LocalFileWriter', async function() {
|
|
|
|
LocalFileWriter.promises.writeStream.rejects(error)
|
|
|
|
await expect(
|
2020-02-17 09:04:42 -05:00
|
|
|
FSPersistor.promises.sendStream(location, files[0], remoteStream)
|
2020-01-02 06:29:28 -05:00
|
|
|
).to.eventually.be.rejectedWith(error)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should send the temporary file to the filestore', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.sendStream(location, files[0], remoteStream)
|
2020-01-02 06:29:28 -05:00
|
|
|
expect(fs.createReadStream).to.have.been.calledWith(tempFile)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
2020-02-17 09:04:42 -05:00
|
|
|
|
|
|
|
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]}`
|
|
|
|
)
|
|
|
|
})
|
|
|
|
})
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
2019-12-16 06:20:22 -05:00
|
|
|
|
2019-12-16 06:20:29 -05:00
|
|
|
describe('getFileStream', function() {
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should use correct file location', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.getFileStream(location, files[0], {})
|
2020-01-06 09:09:28 -05:00
|
|
|
expect(fs.open).to.have.been.calledWith(
|
|
|
|
`${location}/${filteredFilenames[0]}`
|
|
|
|
)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should pass the options to createReadStream', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.getFileStream(location, files[0], {
|
2020-01-02 06:29:28 -05:00
|
|
|
start: 0,
|
|
|
|
end: 8
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
2020-01-02 06:29:28 -05:00
|
|
|
expect(fs.createReadStream).to.have.been.calledWith(null, {
|
|
|
|
start: 0,
|
|
|
|
end: 8,
|
|
|
|
fd
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should give a NotFoundError if the file does not exist', async function() {
|
|
|
|
const err = new Error()
|
|
|
|
err.code = 'ENOENT'
|
|
|
|
fs.open.yields(err)
|
2019-12-16 06:20:29 -05:00
|
|
|
|
2020-02-17 09:04:42 -05:00
|
|
|
await expect(FSPersistor.promises.getFileStream(location, files[0], {}))
|
2020-01-06 10:11:35 -05:00
|
|
|
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
|
|
|
|
.and.have.property('cause', err)
|
2020-01-02 06:29:28 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
it('should wrap any other error', async function() {
|
|
|
|
fs.open.yields(error)
|
2020-02-17 09:04:42 -05:00
|
|
|
await expect(FSPersistor.promises.getFileStream(location, files[0], {}))
|
2020-01-02 06:29:28 -05:00
|
|
|
.to.eventually.be.rejectedWith('failed to open file for streaming')
|
|
|
|
.and.be.an.instanceOf(Errors.ReadError)
|
|
|
|
.and.have.property('cause', error)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('getFileSize', function() {
|
2020-01-02 06:29:28 -05:00
|
|
|
const badFilename = 'neenaw.tex'
|
|
|
|
const size = 65536
|
|
|
|
const noentError = new Error('not found')
|
|
|
|
noentError.code = 'ENOENT'
|
|
|
|
|
|
|
|
beforeEach(function() {
|
|
|
|
fs.stat
|
|
|
|
.yields(error)
|
2020-01-06 09:09:28 -05:00
|
|
|
.withArgs(`${location}/${filteredFilenames[0]}`)
|
2020-01-02 06:29:28 -05:00
|
|
|
.yields(null, { size })
|
|
|
|
.withArgs(`${location}/${badFilename}`)
|
|
|
|
.yields(noentError)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should return the file size', async function() {
|
|
|
|
expect(
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.getFileSize(location, files[0])
|
2020-01-02 06:29:28 -05:00
|
|
|
).to.equal(size)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should throw a NotFoundError if the file does not exist', async function() {
|
|
|
|
await expect(
|
2020-02-17 09:04:42 -05:00
|
|
|
FSPersistor.promises.getFileSize(location, badFilename)
|
2020-01-02 06:29:28 -05:00
|
|
|
).to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should wrap any other error', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await expect(FSPersistor.promises.getFileSize(location, 'raccoon'))
|
2020-01-02 06:29:28 -05:00
|
|
|
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
|
|
|
|
.and.have.property('cause', error)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
2020-01-02 06:29:28 -05:00
|
|
|
})
|
2019-12-16 06:20:29 -05:00
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
describe('copyFile', function() {
|
|
|
|
it('Should open the source for reading', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.copyFile(location, files[0], files[1])
|
2020-01-02 06:29:28 -05:00
|
|
|
expect(fs.createReadStream).to.have.been.calledWith(
|
2020-01-06 09:09:28 -05:00
|
|
|
`${location}/${filteredFilenames[0]}`
|
2019-12-16 06:20:29 -05:00
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('Should open the target for writing', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.copyFile(location, files[0], files[1])
|
2020-01-02 06:29:28 -05:00
|
|
|
expect(fs.createWriteStream).to.have.been.calledWith(
|
2020-01-06 09:09:28 -05:00
|
|
|
`${location}/${filteredFilenames[1]}`
|
2019-12-16 06:20:29 -05:00
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('Should pipe the source to the target', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.copyFile(location, files[0], files[1])
|
2020-01-02 06:29:28 -05:00
|
|
|
expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('deleteFile', function() {
|
2020-01-02 06:29:28 -05:00
|
|
|
it('Should call unlink with correct options', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.deleteFile(location, files[0])
|
2020-01-06 09:09:28 -05:00
|
|
|
expect(fs.unlink).to.have.been.calledWith(
|
|
|
|
`${location}/${filteredFilenames[0]}`
|
|
|
|
)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('Should propagate the error', async function() {
|
|
|
|
fs.unlink.yields(error)
|
|
|
|
await expect(
|
2020-02-17 09:04:42 -05:00
|
|
|
FSPersistor.promises.deleteFile(location, files[0])
|
2020-01-06 10:11:35 -05:00
|
|
|
).to.eventually.be.rejected.and.have.property('cause', error)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('deleteDirectory', function() {
|
2020-01-02 06:29:28 -05:00
|
|
|
it('Should call rmdir(rimraf) with correct options', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.deleteDirectory(location, files[0])
|
2020-01-06 09:09:28 -05:00
|
|
|
expect(rimraf).to.have.been.calledWith(
|
|
|
|
`${location}/${filteredFilenames[0]}`
|
|
|
|
)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('Should propagate the error', async function() {
|
|
|
|
rimraf.yields(error)
|
|
|
|
await expect(
|
2020-02-17 09:04:42 -05:00
|
|
|
FSPersistor.promises.deleteDirectory(location, files[0])
|
2020-01-06 10:11:35 -05:00
|
|
|
).to.eventually.be.rejected.and.have.property('cause', error)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
})
|
2019-12-16 06:20:22 -05:00
|
|
|
|
2019-12-16 06:20:29 -05:00
|
|
|
describe('checkIfFileExists', function() {
|
2020-01-06 09:09:28 -05:00
|
|
|
const badFilename = 'pototo'
|
2020-01-02 06:29:28 -05:00
|
|
|
const noentError = new Error('not found')
|
|
|
|
noentError.code = 'ENOENT'
|
|
|
|
|
2019-12-16 06:20:29 -05:00
|
|
|
beforeEach(function() {
|
2020-01-02 06:29:28 -05:00
|
|
|
fs.stat
|
|
|
|
.yields(error)
|
2020-01-06 09:09:28 -05:00
|
|
|
.withArgs(`${location}/${filteredFilenames[0]}`)
|
2020-01-02 06:29:28 -05:00
|
|
|
.yields(null, {})
|
|
|
|
.withArgs(`${location}/${badFilename}`)
|
|
|
|
.yields(noentError)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('Should call stat with correct options', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.checkIfFileExists(location, files[0])
|
2020-01-06 09:09:28 -05:00
|
|
|
expect(fs.stat).to.have.been.calledWith(
|
|
|
|
`${location}/${filteredFilenames[0]}`
|
|
|
|
)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
2019-12-16 06:20:22 -05:00
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('Should return true for existing files', async function() {
|
|
|
|
expect(
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.checkIfFileExists(location, files[0])
|
2020-01-02 06:29:28 -05:00
|
|
|
).to.equal(true)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('Should return false for non-existing files', async function() {
|
|
|
|
expect(
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.checkIfFileExists(location, badFilename)
|
2020-01-02 06:29:28 -05:00
|
|
|
).to.equal(false)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should wrap the error if there is a problem', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await expect(FSPersistor.promises.checkIfFileExists(location, 'llama'))
|
2020-01-02 06:29:28 -05:00
|
|
|
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
|
|
|
|
.and.have.property('cause', error)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
describe('directorySize', function() {
|
|
|
|
it('should wrap the error', async function() {
|
2020-01-06 09:09:28 -05:00
|
|
|
glob.yields(error)
|
2020-02-17 09:04:42 -05:00
|
|
|
await expect(FSPersistor.promises.directorySize(location, files[0]))
|
2020-01-02 06:29:28 -05:00
|
|
|
.to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
|
|
|
|
.and.include({ cause: error })
|
|
|
|
.and.have.property('info')
|
2020-01-06 09:09:28 -05:00
|
|
|
.which.includes({ location, name: files[0] })
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should filter the directory name', async function() {
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.directorySize(location, files[0])
|
2020-01-06 09:09:28 -05:00
|
|
|
expect(glob).to.have.been.calledWith(
|
|
|
|
`${location}/${filteredFilenames[0]}_*`
|
|
|
|
)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
|
2020-01-02 06:29:28 -05:00
|
|
|
it('should sum directory files size', async function() {
|
|
|
|
expect(
|
2020-02-17 09:04:42 -05:00
|
|
|
await FSPersistor.promises.directorySize(location, files[0])
|
2020-01-02 06:29:28 -05:00
|
|
|
).to.equal(stat.size * files.length)
|
2019-12-16 06:20:29 -05:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|