| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- const sinon = require('sinon')
- const chai = require('chai')
- const { expect } = chai
- const SandboxedModule = require('sandboxed-module')
- const Errors = require('../../src/Errors')
- const StreamModule = require('stream')
- const modulePath = '../../src/FSPersistor.js'
- describe('FSPersistorTests', function () {
- const stat = { size: 4, isFile: sinon.stub().returns(true) }
- const fd = 1234
- const writeStream = 'writeStream'
- const remoteStream = 'remoteStream'
- const location = '/foo'
- const error = new Error('guru meditation error')
- const md5 = 'ffffffff'
- const files = ['animals/wombat.tex', 'vegetables/potato.tex']
- const globs = [`${location}/${files[0]}`, `${location}/${files[1]}`]
- const filteredFilenames = ['animals_wombat.tex', 'vegetables_potato.tex']
- let fs, stream, FSPersistor, glob, readStream, crypto, Hash, uuid, tempFile
- beforeEach(function () {
- const randomNumber = Math.random().toString()
- readStream = {
- name: 'readStream',
- on: sinon.stub().yields(),
- pipe: sinon.stub()
- }
- uuid = {
- v1: () => randomNumber
- }
- tempFile = `/tmp/${randomNumber}`
- 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)
- }
- glob = sinon.stub().yields(null, globs)
- stream = {
- pipeline: sinon.stub().yields(),
- Transform: StreamModule.Transform
- }
- Hash = {
- end: sinon.stub(),
- read: sinon.stub().returns(md5),
- digest: sinon.stub().returns(md5),
- setEncoding: sinon.stub()
- }
- crypto = {
- createHash: sinon.stub().returns(Hash)
- }
- FSPersistor = new (SandboxedModule.require(modulePath, {
- requires: {
- './Errors': Errors,
- fs,
- glob,
- stream,
- crypto,
- 'node-uuid': uuid,
- // imported by PersistorHelper but otherwise unused here
- 'logger-sharelatex': {}
- },
- globals: { console }
- }))({ paths: { uploadFolder: '/tmp' } })
- })
- describe('sendFile', function () {
- const localFilesystemPath = '/path/to/local/file'
- it('should copy the file', async function () {
- await FSPersistor.sendFile(location, files[0], localFilesystemPath)
- expect(fs.createReadStream).to.have.been.calledWith(localFilesystemPath)
- expect(fs.createWriteStream).to.have.been.calledWith(
- `${location}/${filteredFilenames[0]}`
- )
- expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
- })
- it('should return an error if the file cannot be stored', async function () {
- stream.pipeline.yields(error)
- await expect(
- FSPersistor.sendFile(location, files[0], localFilesystemPath)
- ).to.eventually.be.rejected.and.have.property('cause', error)
- })
- })
- describe('sendStream', function () {
- it('should write the stream to disk', async function () {
- await FSPersistor.sendStream(location, files[0], remoteStream)
- expect(stream.pipeline).to.have.been.calledWith(remoteStream, writeStream)
- })
- it('should delete the temporary file', async function () {
- await FSPersistor.sendStream(location, files[0], remoteStream)
- expect(fs.unlink).to.have.been.calledWith(tempFile)
- })
- it('should wrap the error from the filesystem', async function () {
- stream.pipeline.yields(error)
- await expect(FSPersistor.sendStream(location, files[0], remoteStream))
- .to.eventually.be.rejected.and.be.instanceOf(Errors.WriteError)
- .and.have.property('cause', error)
- })
- it('should send the temporary file to the filestore', async function () {
- await FSPersistor.sendStream(location, files[0], remoteStream)
- 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.sendStream(location, files[0], remoteStream, {
- sourceMd5: '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.sendStream(location, files[0], remoteStream, {
- sourceMd5: '00000000'
- })
- } catch (_) {}
- expect(fs.unlink).to.have.been.calledWith(
- `${location}/${filteredFilenames[0]}`
- )
- })
- })
- })
- describe('getObjectStream', function () {
- it('should use correct file location', async function () {
- await FSPersistor.getObjectStream(location, files[0], {})
- expect(fs.open).to.have.been.calledWith(
- `${location}/${filteredFilenames[0]}`
- )
- })
- it('should pass the options to createReadStream', async function () {
- await FSPersistor.getObjectStream(location, files[0], {
- start: 0,
- end: 8
- })
- expect(fs.createReadStream).to.have.been.calledWith(null, {
- start: 0,
- end: 8,
- fd
- })
- })
- it('should give a NotFoundError if the file does not exist', async function () {
- const err = new Error()
- err.code = 'ENOENT'
- fs.open.yields(err)
- await expect(FSPersistor.getObjectStream(location, files[0], {}))
- .to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
- .and.have.property('cause', err)
- })
- it('should wrap any other error', async function () {
- fs.open.yields(error)
- await expect(FSPersistor.getObjectStream(location, files[0], {}))
- .to.eventually.be.rejectedWith('failed to open file for streaming')
- .and.be.an.instanceOf(Errors.ReadError)
- .and.have.property('cause', error)
- })
- })
- describe('getObjectSize', function () {
- const badFilename = 'neenaw.tex'
- const size = 65536
- const noentError = new Error('not found')
- noentError.code = 'ENOENT'
- beforeEach(function () {
- fs.stat
- .yields(error)
- .withArgs(`${location}/${filteredFilenames[0]}`)
- .yields(null, { size })
- .withArgs(`${location}/${badFilename}`)
- .yields(noentError)
- })
- it('should return the file size', async function () {
- expect(await FSPersistor.getObjectSize(location, files[0])).to.equal(size)
- })
- it('should throw a NotFoundError if the file does not exist', async function () {
- await expect(
- FSPersistor.getObjectSize(location, badFilename)
- ).to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
- })
- it('should wrap any other error', async function () {
- await expect(FSPersistor.getObjectSize(location, 'raccoon'))
- .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
- .and.have.property('cause', error)
- })
- })
- describe('copyObject', function () {
- it('Should open the source for reading', async function () {
- await FSPersistor.copyObject(location, files[0], files[1])
- expect(fs.createReadStream).to.have.been.calledWith(
- `${location}/${filteredFilenames[0]}`
- )
- })
- it('Should open the target for writing', async function () {
- await FSPersistor.copyObject(location, files[0], files[1])
- expect(fs.createWriteStream).to.have.been.calledWith(
- `${location}/${filteredFilenames[1]}`
- )
- })
- it('Should pipe the source to the target', async function () {
- await FSPersistor.copyObject(location, files[0], files[1])
- expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
- })
- })
- describe('deleteObject', function () {
- it('Should call unlink with correct options', async function () {
- await FSPersistor.deleteObject(location, files[0])
- expect(fs.unlink).to.have.been.calledWith(
- `${location}/${filteredFilenames[0]}`
- )
- })
- it('Should propagate the error', async function () {
- fs.unlink.yields(error)
- await expect(
- FSPersistor.deleteObject(location, files[0])
- ).to.eventually.be.rejected.and.have.property('cause', error)
- })
- })
- describe('deleteDirectory', function () {
- it('Should call glob with correct options', async function () {
- await FSPersistor.deleteDirectory(location, files[0])
- expect(glob).to.have.been.calledWith(
- `${location}/${filteredFilenames[0]}_*`
- )
- })
- it('Should call unlink on the returned files', async function () {
- await FSPersistor.deleteDirectory(location, files[0])
- for (const filename of globs) {
- expect(fs.unlink).to.have.been.calledWith(filename)
- }
- })
- it('Should propagate the error', async function () {
- glob.yields(error)
- await expect(
- FSPersistor.deleteDirectory(location, files[0])
- ).to.eventually.be.rejected.and.have.property('cause', error)
- })
- })
- describe('checkIfObjectExists', function () {
- const badFilename = 'pototo'
- const noentError = new Error('not found')
- noentError.code = 'ENOENT'
- beforeEach(function () {
- fs.stat
- .yields(error)
- .withArgs(`${location}/${filteredFilenames[0]}`)
- .yields(null, {})
- .withArgs(`${location}/${badFilename}`)
- .yields(noentError)
- })
- it('Should call stat with correct options', async function () {
- await FSPersistor.checkIfObjectExists(location, files[0])
- expect(fs.stat).to.have.been.calledWith(
- `${location}/${filteredFilenames[0]}`
- )
- })
- it('Should return true for existing files', async function () {
- expect(
- await FSPersistor.checkIfObjectExists(location, files[0])
- ).to.equal(true)
- })
- it('Should return false for non-existing files', async function () {
- expect(
- await FSPersistor.checkIfObjectExists(location, badFilename)
- ).to.equal(false)
- })
- it('should wrap the error if there is a problem', async function () {
- await expect(FSPersistor.checkIfObjectExists(location, 'llama'))
- .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
- .and.have.property('cause', error)
- })
- })
- describe('directorySize', function () {
- it('should wrap the error', async function () {
- glob.yields(error)
- await expect(FSPersistor.directorySize(location, files[0]))
- .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
- .and.include({ cause: error })
- .and.have.property('info')
- .which.includes({ location, name: files[0] })
- })
- it('should filter the directory name', async function () {
- await FSPersistor.directorySize(location, files[0])
- expect(glob).to.have.been.calledWith(
- `${location}/${filteredFilenames[0]}_*`
- )
- })
- it('should sum directory files size', async function () {
- expect(await FSPersistor.directorySize(location, files[0])).to.equal(
- stat.size * files.length
- )
- })
- })
- })
|