2014-02-14 11:39:05 -05:00
|
|
|
assert = require("chai").assert
|
|
|
|
sinon = require('sinon')
|
|
|
|
chai = require('chai')
|
|
|
|
should = chai.should()
|
|
|
|
expect = chai.expect
|
2014-02-25 11:38:13 -05:00
|
|
|
modulePath = "../../../app/js/S3PersistorManager.js"
|
2014-02-14 11:39:05 -05:00
|
|
|
SandboxedModule = require('sandboxed-module')
|
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
describe "S3PersistorManagerTests", ->
|
2014-02-14 11:39:05 -05:00
|
|
|
|
|
|
|
beforeEach ->
|
2014-03-04 10:01:13 -05:00
|
|
|
@settings =
|
|
|
|
filestore:
|
|
|
|
backend: "s3"
|
|
|
|
s3:
|
|
|
|
secret: "secret"
|
|
|
|
key: "this_key"
|
|
|
|
stores:
|
|
|
|
user_files:"sl_user_files"
|
2019-06-18 08:25:14 -04:00
|
|
|
@knoxClient =
|
2014-02-14 11:39:05 -05:00
|
|
|
putFile:sinon.stub()
|
|
|
|
copyFile:sinon.stub()
|
|
|
|
list: sinon.stub()
|
|
|
|
deleteMultiple: sinon.stub()
|
2014-02-19 15:56:45 -05:00
|
|
|
get: sinon.stub()
|
2014-03-04 10:01:13 -05:00
|
|
|
@knox =
|
2019-06-18 08:25:14 -04:00
|
|
|
createClient: sinon.stub().returns(@knoxClient)
|
|
|
|
@s3EventHandlers = {}
|
|
|
|
@s3Request =
|
|
|
|
on: sinon.stub().callsFake (event, callback) =>
|
|
|
|
@s3EventHandlers[event] = callback
|
|
|
|
send: sinon.stub()
|
|
|
|
@s3Response =
|
|
|
|
httpResponse:
|
|
|
|
getUnbufferedStream: sinon.stub()
|
|
|
|
@s3Client =
|
2019-06-13 16:57:49 -04:00
|
|
|
copyObject: sinon.stub()
|
|
|
|
headObject: sinon.stub()
|
2019-06-18 08:25:14 -04:00
|
|
|
getObject: sinon.stub().returns(@s3Request)
|
|
|
|
@awsS3 = sinon.stub().returns(@s3Client)
|
2014-03-04 10:01:13 -05:00
|
|
|
@LocalFileWriter =
|
2014-02-14 11:39:05 -05:00
|
|
|
writeStream: sinon.stub()
|
|
|
|
deleteFile: sinon.stub()
|
2019-06-13 16:57:49 -04:00
|
|
|
@request = sinon.stub()
|
2014-03-04 10:01:13 -05:00
|
|
|
@requires =
|
2014-02-14 11:39:05 -05:00
|
|
|
"knox": @knox
|
2018-12-07 12:02:34 -05:00
|
|
|
"aws-sdk/clients/s3": @awsS3
|
2014-02-14 11:39:05 -05:00
|
|
|
"settings-sharelatex": @settings
|
|
|
|
"./LocalFileWriter":@LocalFileWriter
|
|
|
|
"logger-sharelatex":
|
|
|
|
log:->
|
|
|
|
err:->
|
2019-06-13 16:57:49 -04:00
|
|
|
"request": @request
|
2019-02-05 06:32:02 -05:00
|
|
|
"./Errors": @Errors =
|
|
|
|
NotFoundError: sinon.stub()
|
2014-02-14 11:39:05 -05:00
|
|
|
@key = "my/key"
|
|
|
|
@bucketName = "my-bucket"
|
|
|
|
@error = "my errror"
|
2019-06-13 16:57:49 -04:00
|
|
|
@S3PersistorManager = SandboxedModule.require modulePath, requires: @requires
|
2014-02-14 11:39:05 -05:00
|
|
|
|
2014-02-19 15:56:45 -05:00
|
|
|
describe "getFileStream", ->
|
2019-06-18 08:25:14 -04:00
|
|
|
describe "success", ->
|
|
|
|
beforeEach () ->
|
|
|
|
@expectedStream = { expectedStream: true }
|
|
|
|
@s3Request.send.callsFake () =>
|
|
|
|
@s3EventHandlers.httpHeaders(200, {}, @s3Response, "OK")
|
|
|
|
@s3Response.httpResponse.getUnbufferedStream.returns(@expectedStream)
|
|
|
|
|
|
|
|
it "returns a stream", (done) ->
|
|
|
|
@S3PersistorManager.getFileStream @bucketName, @key, {}, (err, stream) =>
|
|
|
|
if err?
|
|
|
|
return done(err)
|
|
|
|
expect(stream).to.equal(@expectedStream)
|
|
|
|
done()
|
2014-02-14 11:39:05 -05:00
|
|
|
|
2019-06-18 08:25:14 -04:00
|
|
|
it "sets the AWS client up with credentials from settings", (done) ->
|
|
|
|
@S3PersistorManager.getFileStream @bucketName, @key, {}, (err, stream) =>
|
|
|
|
if err?
|
|
|
|
return done(err)
|
|
|
|
expect(@awsS3.lastCall.args).to.deep.equal([{
|
|
|
|
credentials:
|
|
|
|
accessKeyId: @settings.filestore.s3.key
|
|
|
|
secretAccessKey: @settings.filestore.s3.secret
|
|
|
|
}])
|
|
|
|
done()
|
2018-07-04 06:18:55 -04:00
|
|
|
|
2019-06-18 08:25:14 -04:00
|
|
|
it "fetches the right key from the right bucket", (done) ->
|
|
|
|
@S3PersistorManager.getFileStream @bucketName, @key, {}, (err, stream) =>
|
|
|
|
if err?
|
|
|
|
return done(err)
|
|
|
|
expect(@s3Client.getObject.lastCall.args).to.deep.equal([{
|
|
|
|
Bucket: @bucketName,
|
|
|
|
Key: @key
|
|
|
|
}])
|
|
|
|
done()
|
2015-08-28 05:09:41 -04:00
|
|
|
|
2019-06-18 08:25:14 -04:00
|
|
|
it "accepts alternative credentials", (done) ->
|
|
|
|
accessKeyId = "that_key"
|
|
|
|
secret = "that_secret"
|
|
|
|
opts = {
|
|
|
|
credentials:
|
|
|
|
auth_key: accessKeyId
|
|
|
|
auth_secret: secret
|
|
|
|
}
|
|
|
|
@S3PersistorManager.getFileStream @bucketName, @key, opts, (err, stream) =>
|
|
|
|
if err?
|
|
|
|
return done(err)
|
|
|
|
expect(@awsS3.lastCall.args).to.deep.equal([{
|
|
|
|
credentials:
|
|
|
|
accessKeyId: accessKeyId
|
|
|
|
secretAccessKey: secret
|
|
|
|
}])
|
|
|
|
expect(stream).to.equal(@expectedStream)
|
|
|
|
done()
|
2015-08-28 05:09:41 -04:00
|
|
|
|
2019-06-18 08:25:14 -04:00
|
|
|
it "accepts byte range", (done) ->
|
|
|
|
start = 0
|
|
|
|
end = 8
|
|
|
|
opts = { start: start, end: end }
|
|
|
|
@S3PersistorManager.getFileStream @bucketName, @key, opts, (err, stream) =>
|
|
|
|
if err?
|
|
|
|
return done(err)
|
|
|
|
expect(@s3Client.getObject.lastCall.args).to.deep.equal([{
|
|
|
|
Bucket: @bucketName
|
|
|
|
Key: @key
|
|
|
|
Range: "bytes=#{start}-#{end}"
|
|
|
|
}])
|
|
|
|
expect(stream).to.equal(@expectedStream)
|
|
|
|
done()
|
2015-09-01 06:36:28 -04:00
|
|
|
|
2019-06-18 08:25:14 -04:00
|
|
|
describe "errors", ->
|
|
|
|
describe "when the file doesn't exist", ->
|
2015-09-01 06:36:28 -04:00
|
|
|
beforeEach ->
|
2019-06-18 08:25:14 -04:00
|
|
|
@s3Request.send.callsFake () =>
|
|
|
|
@s3EventHandlers.httpHeaders(404, {}, @s3Response, "Not found")
|
|
|
|
|
|
|
|
it "returns a NotFoundError that indicates the bucket and key", (done) ->
|
|
|
|
@S3PersistorManager.getFileStream @bucketName, @key, {}, (err, stream) =>
|
|
|
|
expect(err).to.be.instanceof(@Errors.NotFoundError)
|
|
|
|
errMsg = @Errors.NotFoundError.lastCall.args[0]
|
|
|
|
expect(errMsg).to.match(new RegExp(".*#{@bucketName}.*"))
|
|
|
|
expect(errMsg).to.match(new RegExp(".*#{@key}.*"))
|
2015-09-01 06:36:28 -04:00
|
|
|
done()
|
|
|
|
|
2019-06-18 08:25:14 -04:00
|
|
|
describe "when S3 encounters an unkown error", ->
|
|
|
|
beforeEach ->
|
|
|
|
@s3Request.send.callsFake () =>
|
|
|
|
@s3EventHandlers.httpHeaders(500, {}, @s3Response, "Internal server error")
|
|
|
|
|
|
|
|
it "returns an error", (done) ->
|
|
|
|
@S3PersistorManager.getFileStream @bucketName, @key, {}, (err, stream) =>
|
|
|
|
expect(err).to.be.instanceof(Error)
|
2015-09-01 06:36:28 -04:00
|
|
|
done()
|
|
|
|
|
2019-06-18 08:25:14 -04:00
|
|
|
describe "when the S3 request errors out before receiving HTTP headers", ->
|
2019-02-05 06:32:02 -05:00
|
|
|
beforeEach ->
|
2019-06-18 08:25:14 -04:00
|
|
|
@s3Request.send.callsFake () =>
|
|
|
|
@s3EventHandlers.error(new Error("connection failed"))
|
|
|
|
|
|
|
|
it "returns an error", (done) ->
|
|
|
|
@S3PersistorManager.getFileStream @bucketName, @key, {}, (err, stream) =>
|
|
|
|
expect(err).to.be.instanceof(Error)
|
2015-09-01 06:36:28 -04:00
|
|
|
done()
|
2015-08-28 05:09:41 -04:00
|
|
|
|
2019-06-13 16:57:49 -04:00
|
|
|
describe "getFileSize", ->
|
|
|
|
it "should obtain the file size from S3", (done) ->
|
|
|
|
expectedFileSize = 123
|
2019-06-18 08:25:14 -04:00
|
|
|
@s3Client.headObject.yields(new Error(
|
2019-06-13 16:57:49 -04:00
|
|
|
"s3Client.headObject got unexpected arguments"
|
|
|
|
))
|
2019-06-18 08:25:14 -04:00
|
|
|
@s3Client.headObject.withArgs({
|
2019-06-13 16:57:49 -04:00
|
|
|
Bucket: @bucketName
|
|
|
|
Key: @key
|
|
|
|
}).yields(null, { ContentLength: expectedFileSize })
|
|
|
|
|
|
|
|
@S3PersistorManager.getFileSize @bucketName, @key, (err, fileSize) =>
|
|
|
|
if err?
|
|
|
|
return done(err)
|
|
|
|
expect(fileSize).to.equal(expectedFileSize)
|
|
|
|
done()
|
|
|
|
|
|
|
|
[403, 404].forEach (statusCode) ->
|
|
|
|
it "should throw NotFoundError when S3 responds with #{statusCode}", (done) ->
|
|
|
|
error = new Error()
|
|
|
|
error.statusCode = statusCode
|
2019-06-18 08:25:14 -04:00
|
|
|
@s3Client.headObject.yields(error)
|
2019-06-13 16:57:49 -04:00
|
|
|
|
|
|
|
@S3PersistorManager.getFileSize @bucketName, @key, (err, fileSize) =>
|
|
|
|
expect(err).to.be.an.instanceof(@Errors.NotFoundError)
|
|
|
|
done()
|
|
|
|
|
|
|
|
it "should rethrow any other error", (done) ->
|
|
|
|
error = new Error()
|
2019-06-18 08:25:14 -04:00
|
|
|
@s3Client.headObject.yields(error)
|
|
|
|
@s3Client.headObject.yields(error)
|
2019-06-13 16:57:49 -04:00
|
|
|
|
|
|
|
@S3PersistorManager.getFileSize @bucketName, @key, (err, fileSize) =>
|
|
|
|
expect(err).to.equal(error)
|
|
|
|
done()
|
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
describe "sendFile", ->
|
2014-02-14 11:39:05 -05:00
|
|
|
|
|
|
|
beforeEach ->
|
2019-06-18 08:25:14 -04:00
|
|
|
@knoxClient.putFile.returns on:->
|
2014-02-14 11:39:05 -05:00
|
|
|
|
|
|
|
it "should put file with knox", (done)->
|
|
|
|
@LocalFileWriter.deleteFile.callsArgWith(1)
|
2019-06-18 08:25:14 -04:00
|
|
|
@knoxClient.putFile.callsArgWith(2, @error)
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.sendFile @bucketName, @key, @fsPath, (err)=>
|
2019-06-18 08:25:14 -04:00
|
|
|
@knoxClient.putFile.calledWith(@fsPath, @key).should.equal true
|
2014-02-14 11:39:05 -05:00
|
|
|
err.should.equal @error
|
|
|
|
done()
|
|
|
|
|
|
|
|
it "should delete the file and pass the error with it", (done)->
|
|
|
|
@LocalFileWriter.deleteFile.callsArgWith(1)
|
2019-06-18 08:25:14 -04:00
|
|
|
@knoxClient.putFile.callsArgWith(2, @error)
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.sendFile @bucketName, @key, @fsPath, (err)=>
|
2019-06-18 08:25:14 -04:00
|
|
|
@knoxClient.putFile.calledWith(@fsPath, @key).should.equal true
|
2014-02-14 11:39:05 -05:00
|
|
|
err.should.equal @error
|
|
|
|
done()
|
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
describe "sendStream", ->
|
2014-02-14 11:39:05 -05:00
|
|
|
beforeEach ->
|
|
|
|
@fsPath = "to/some/where"
|
2014-02-25 11:38:13 -05:00
|
|
|
@origin =
|
2014-02-14 11:39:05 -05:00
|
|
|
on:->
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.sendFile = sinon.stub().callsArgWith(3)
|
2014-02-14 11:39:05 -05:00
|
|
|
|
|
|
|
it "should send stream to LocalFileWriter", (done)->
|
|
|
|
@LocalFileWriter.deleteFile.callsArgWith(1)
|
|
|
|
@LocalFileWriter.writeStream.callsArgWith(2, null, @fsPath)
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.sendStream @bucketName, @key, @origin, =>
|
2014-02-14 11:39:05 -05:00
|
|
|
@LocalFileWriter.writeStream.calledWith(@origin).should.equal true
|
|
|
|
done()
|
|
|
|
|
|
|
|
it "should return the error from LocalFileWriter", (done)->
|
|
|
|
@LocalFileWriter.deleteFile.callsArgWith(1)
|
|
|
|
@LocalFileWriter.writeStream.callsArgWith(2, @error)
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.sendStream @bucketName, @key, @origin, (err)=>
|
2014-02-14 11:39:05 -05:00
|
|
|
err.should.equal @error
|
|
|
|
done()
|
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
it "should send the file to the filestore", (done)->
|
2014-02-14 11:39:05 -05:00
|
|
|
@LocalFileWriter.deleteFile.callsArgWith(1)
|
|
|
|
@LocalFileWriter.writeStream.callsArgWith(2)
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.sendStream @bucketName, @key, @origin, (err)=>
|
|
|
|
@S3PersistorManager.sendFile.called.should.equal true
|
2014-02-14 11:39:05 -05:00
|
|
|
done()
|
|
|
|
|
|
|
|
describe "copyFile", ->
|
|
|
|
beforeEach ->
|
|
|
|
@sourceKey = "my/key"
|
|
|
|
@destKey = "my/dest/key"
|
|
|
|
|
2018-12-07 12:02:34 -05:00
|
|
|
it "should use AWS SDK to copy file", (done)->
|
2019-06-18 08:25:14 -04:00
|
|
|
@s3Client.copyObject.callsArgWith(1, @error)
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.copyFile @bucketName, @sourceKey, @destKey, (err)=>
|
2014-02-14 11:39:05 -05:00
|
|
|
err.should.equal @error
|
2019-06-18 08:25:14 -04:00
|
|
|
@s3Client.copyObject.calledWith({Bucket: @bucketName, Key: @destKey, CopySource: @bucketName + '/' + @key}).should.equal true
|
2014-02-14 11:39:05 -05:00
|
|
|
done()
|
|
|
|
|
2019-01-09 05:31:59 -05:00
|
|
|
it "should return a NotFoundError object if the original file does not exist", (done)->
|
|
|
|
NoSuchKeyError = {code: "NoSuchKey"}
|
2019-06-18 08:25:14 -04:00
|
|
|
@s3Client.copyObject.callsArgWith(1, NoSuchKeyError)
|
2019-01-09 05:31:59 -05:00
|
|
|
@S3PersistorManager.copyFile @bucketName, @sourceKey, @destKey, (err)=>
|
|
|
|
expect(err instanceof @Errors.NotFoundError).to.equal true
|
2014-02-14 11:39:05 -05:00
|
|
|
done()
|
|
|
|
|
|
|
|
describe "deleteDirectory", ->
|
|
|
|
|
|
|
|
it "should list the contents passing them onto multi delete", (done)->
|
2014-03-04 10:01:13 -05:00
|
|
|
data =
|
2014-02-14 11:39:05 -05:00
|
|
|
Contents: [{Key:"1234"}, {Key: "456"}]
|
2019-06-18 08:25:14 -04:00
|
|
|
@knoxClient.list.callsArgWith(1, null, data)
|
|
|
|
@knoxClient.deleteMultiple.callsArgWith(1)
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.deleteDirectory @bucketName, @key, (err)=>
|
2019-06-18 08:25:14 -04:00
|
|
|
@knoxClient.deleteMultiple.calledWith(["1234","456"]).should.equal true
|
2014-02-14 11:39:05 -05:00
|
|
|
done()
|
|
|
|
|
|
|
|
describe "deleteFile", ->
|
|
|
|
|
|
|
|
it "should use correct options", (done)->
|
2019-06-13 16:57:49 -04:00
|
|
|
@request.callsArgWith(1)
|
2014-02-14 11:39:05 -05:00
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.deleteFile @bucketName, @key, (err)=>
|
2014-02-14 11:39:05 -05:00
|
|
|
opts = @request.args[0][0]
|
2014-03-04 10:01:13 -05:00
|
|
|
assert.deepEqual(opts.aws, {key:@settings.filestore.s3.key, secret:@settings.filestore.s3.secret, bucket:@bucketName})
|
2014-02-14 11:39:05 -05:00
|
|
|
opts.method.should.equal "delete"
|
|
|
|
opts.timeout.should.equal (30*1000)
|
|
|
|
opts.uri.should.equal "https://#{@bucketName}.s3.amazonaws.com/#{@key}"
|
|
|
|
done()
|
|
|
|
|
|
|
|
it "should return the error", (done)->
|
2019-06-13 16:57:49 -04:00
|
|
|
@request.callsArgWith(1, @error)
|
2014-02-14 11:39:05 -05:00
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.deleteFile @bucketName, @key, (err)=>
|
2014-02-14 11:39:05 -05:00
|
|
|
err.should.equal @error
|
|
|
|
done()
|
|
|
|
|
|
|
|
describe "checkIfFileExists", ->
|
|
|
|
|
|
|
|
it "should use correct options", (done)->
|
2019-06-13 16:57:49 -04:00
|
|
|
@request.callsArgWith(1, null, statusCode:200)
|
2014-02-14 11:39:05 -05:00
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.checkIfFileExists @bucketName, @key, (err)=>
|
2014-02-14 11:39:05 -05:00
|
|
|
opts = @request.args[0][0]
|
2014-03-04 10:01:13 -05:00
|
|
|
assert.deepEqual(opts.aws, {key:@settings.filestore.s3.key, secret:@settings.filestore.s3.secret, bucket:@bucketName})
|
2014-02-14 11:39:05 -05:00
|
|
|
opts.method.should.equal "head"
|
|
|
|
opts.timeout.should.equal (30*1000)
|
|
|
|
opts.uri.should.equal "https://#{@bucketName}.s3.amazonaws.com/#{@key}"
|
|
|
|
done()
|
|
|
|
|
|
|
|
it "should return true for a 200", (done)->
|
2019-06-13 16:57:49 -04:00
|
|
|
@request.callsArgWith(1, null, statusCode:200)
|
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.checkIfFileExists @bucketName, @key, (err, exists)=>
|
2014-02-14 11:39:05 -05:00
|
|
|
exists.should.equal true
|
|
|
|
done()
|
|
|
|
|
|
|
|
it "should return false for a non 200", (done)->
|
2019-06-13 16:57:49 -04:00
|
|
|
@request.callsArgWith(1, null, statusCode:404)
|
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.checkIfFileExists @bucketName, @key, (err, exists)=>
|
2014-02-14 11:39:05 -05:00
|
|
|
exists.should.equal false
|
|
|
|
done()
|
|
|
|
|
|
|
|
it "should return the error", (done)->
|
2019-06-13 16:57:49 -04:00
|
|
|
@request.callsArgWith(1, @error, {})
|
2014-02-14 11:39:05 -05:00
|
|
|
|
2014-02-25 11:38:13 -05:00
|
|
|
@S3PersistorManager.checkIfFileExists @bucketName, @key, (err)=>
|
2014-02-14 11:39:05 -05:00
|
|
|
err.should.equal @error
|
2014-02-25 11:38:13 -05:00
|
|
|
done()
|
2016-03-13 15:22:14 -04:00
|
|
|
|
2016-03-13 19:45:48 -04:00
|
|
|
describe "directorySize", ->
|
2016-03-13 15:22:14 -04:00
|
|
|
|
2016-03-13 19:45:48 -04:00
|
|
|
it "should sum directory files size", (done) ->
|
|
|
|
data =
|
|
|
|
Contents: [ {Size: 1024}, {Size: 2048} ]
|
2019-06-18 08:25:14 -04:00
|
|
|
@knoxClient.list.callsArgWith(1, null, data)
|
2016-03-13 19:45:48 -04:00
|
|
|
@S3PersistorManager.directorySize @bucketName, @key, (err, totalSize)=>
|
|
|
|
totalSize.should.equal 3072
|
2019-06-13 16:57:49 -04:00
|
|
|
done()
|