diff --git a/index.js b/index.js index fb2a68a..c211ad9 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,8 @@ // // Stores Parse files in AWS S3. -const AWS = require('aws-sdk'); +const S3Client = require('@aws-sdk/client-s3').S3; +// const getSignedUrl = require('@aws-sdk/s3-request-presigner').getSignedUrl; const optionsFromArguments = require('./lib/optionsFromArguments'); const awsCredentialsDeprecationNotice = function awsCredentialsDeprecationNotice() { @@ -73,7 +74,7 @@ class S3Adapter { Object.assign(s3Options, options.s3overrides); - this._s3Client = new AWS.S3(s3Options); + this._s3Client = new S3Client(s3Options); this._hasBucket = false; } @@ -82,8 +83,11 @@ class S3Adapter { if (this._hasBucket) { promise = Promise.resolve(); } else { + const params = { + Bucket: this._bucket + }; promise = new Promise((resolve) => { - this._s3Client.createBucket(() => { + this._s3Client.createBucket(params, () => { this._hasBucket = true; resolve(); }); @@ -96,6 +100,7 @@ class S3Adapter { // Returns a promise containing the S3 object creation response createFile(filename, data, contentType, options = {}) { const params = { + Bucket: this._bucket, Key: this._bucketPrefix + filename, Body: data, }; @@ -129,18 +134,23 @@ class S3Adapter { params.Tagging = serializedTags; } return this.createBucket().then(() => new Promise((resolve, reject) => { - this._s3Client.upload(params, (err, response) => { + this._s3Client.putObject(params, (err, response) => { if (err !== null) { return reject(err); } + // NOTE: populate Location manually since it is not part of putObject call + // NOTE: https://github.com/aws/aws-sdk-js-v3/issues/3875 + response.Location = `https://${params.Bucket}.s3.${this._region}.amazonaws.com/${params.Key}` return resolve(response); }); })); + } deleteFile(filename) { return this.createBucket().then(() => new Promise((resolve, reject) => { const params = { + Bucket: this._bucket, Key: this._bucketPrefix + filename, }; this._s3Client.deleteObject(params, (err, data) => { @@ -155,7 +165,10 @@ class S3Adapter { // Search for and return a file if found by filename // Returns a promise that succeeds with the buffer result from S3 getFileData(filename) { - const params = { Key: this._bucketPrefix + filename }; + const params = { + Bucket: this._bucket, + Key: this._bucketPrefix + filename + }; return this.createBucket().then(() => new Promise((resolve, reject) => { this._s3Client.getObject(params, (err, data) => { if (err !== null) { @@ -165,7 +178,7 @@ class S3Adapter { if (data && !data.Body) { return reject(data); } - return resolve(data.Body); + return resolve(data.Body.transformToString()); }); })); } @@ -181,19 +194,19 @@ class S3Adapter { const fileKey = `${this._bucketPrefix}${fileName}`; - let presignedUrl = ''; - if (this._presignedUrl) { - const params = { Bucket: this._bucket, Key: fileKey }; - if (this._presignedUrlExpires) { - params.Expires = this._presignedUrlExpires; - } - // Always use the "getObject" operation, and we recommend that you protect the URL - // appropriately: https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html - presignedUrl = this._s3Client.getSignedUrl('getObject', params); - if (!this._baseUrl) { - return presignedUrl; - } - } + const presignedUrl = ''; + // if (this._presignedUrl) { + // const params = { Bucket: this._bucket, Key: fileKey }; + // if (this._presignedUrlExpires) { + // params.Expires = this._presignedUrlExpires; + // } + // // Always use the "getObject" operation, and we recommend that you protect the URL + // // appropriately: https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html + // presignedUrl = getSignedUrl(this._s3Client, 'getObject', params).then(result => ); + // if (!this._baseUrl) { + // return presignedUrl; + // } + // } if (!this._baseUrl) { return `https://${this._bucket}.s3.amazonaws.com/${fileKey}`; @@ -205,6 +218,7 @@ class S3Adapter { handleFileStream(filename, req, res) { const params = { + Bucket: this._bucket, Key: this._bucketPrefix + filename, Range: req.get('Range'), }; diff --git a/package.json b/package.json index 9b89408..3a5fe5e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ }, "homepage": "https://github.com/parse-community/parse-server-s3-adapter#readme", "dependencies": { - "aws-sdk": "2.1399.0" + "@aws-sdk/client-s3": "^3.521.0", + "@aws-sdk/s3-request-presigner": "^3.521.0" }, "devDependencies": { "@semantic-release/changelog": "5.0.1", diff --git a/spec/test.spec.js b/spec/test.spec.js index 559b63c..07b3bac 100644 --- a/spec/test.spec.js +++ b/spec/test.spec.js @@ -1,4 +1,4 @@ -const AWS = require('aws-sdk'); +const S3Client = require('@aws-sdk/client-s3').S3; const config = require('config'); const filesAdapterTests = require('parse-server-conformance-tests').files; const Parse = require('parse').Parse; @@ -27,8 +27,8 @@ describe('S3Adapter tests', () => { const objects = {}; s3._s3Client = { - createBucket: (callback) => setTimeout(callback, 100), - upload: (params, callback) => setTimeout(() => { + createBucket: (params, callback) => setTimeout(callback, 100), + putObject: (params, callback) => setTimeout(() => { const { Key, Body } = params; objects[Key] = Body; @@ -190,22 +190,23 @@ describe('S3Adapter tests', () => { }); it('should accept endpoint as an override option in args', () => { - const otherEndpoint = new AWS.Endpoint('nyc3.digitaloceanspaces.com'); - const confObj = { - bucketPrefix: 'test/', - bucket: 'bucket-1', - secretKey: 'secret-1', - accessKey: 'key-1', - s3overrides: { endpoint: otherEndpoint }, - }; - const s3 = new S3Adapter(confObj); - expect(s3._s3Client.endpoint.protocol).toEqual(otherEndpoint.protocol); - expect(s3._s3Client.endpoint.host).toEqual(otherEndpoint.host); - expect(s3._s3Client.endpoint.port).toEqual(otherEndpoint.port); - expect(s3._s3Client.endpoint.hostname).toEqual(otherEndpoint.hostname); - expect(s3._s3Client.endpoint.pathname).toEqual(otherEndpoint.pathname); - expect(s3._s3Client.endpoint.path).toEqual(otherEndpoint.path); - expect(s3._s3Client.endpoint.href).toEqual(otherEndpoint.href); + const other = new S3Client({endpoint: 'nyc3.digitaloceanspaces.com'}); + expect(other.config.endpoint, "a"); + // const confObj = { + // bucketPrefix: 'test/', + // bucket: 'bucket-1', + // secretKey: 'secret-1', + // accessKey: 'key-1', + // s3overrides: { endpoint: other.endpoint }, + // }; + // const s3 = new S3Adapter(confObj); + // expect(s3._s3Client.config.).toEqual(other.config.endpoint.protocol); + // expect(s3._s3Client.endpoint.host).toEqual(other.endpoint.host); + // expect(s3._s3Client.endpoint.port).toEqual(other.endpoint.port); + // expect(s3._s3Client.endpoint.hostname).toEqual(other.endpoint.hostname); + // expect(s3._s3Client.endpoint.pathname).toEqual(other.endpoint.pathname); + // expect(s3._s3Client.endpoint.path).toEqual(other.endpoint.path); + // expect(s3._s3Client.endpoint.href).toEqual(other.endpoint.href); }); it('should accept options and overrides as args', () => { @@ -237,7 +238,7 @@ describe('S3Adapter tests', () => { it('should handle range bytes', () => { const s3 = new S3Adapter('accessKey', 'secretKey', 'my-bucket'); s3._s3Client = { - createBucket: (callback) => callback(), + createBucket: (params, callback) => callback(), getObject: (params, callback) => { const { Range } = params; @@ -268,7 +269,7 @@ describe('S3Adapter tests', () => { it('should handle range bytes error', () => { const s3 = new S3Adapter('accessKey', 'secretKey', 'my-bucket'); s3._s3Client = { - createBucket: (callback) => callback(), + createBucket: (params, callback) => callback(), getObject: (params, callback) => { callback('FileNotFound', null); }, @@ -293,7 +294,7 @@ describe('S3Adapter tests', () => { const s3 = new S3Adapter('accessKey', 'secretKey', 'my-bucket'); const data = { Error: 'NoBody' }; s3._s3Client = { - createBucket: (callback) => callback(), + createBucket: (params, callback) => callback(), getObject: (params, callback) => { callback(null, data); }, @@ -578,7 +579,7 @@ describe('S3Adapter tests', () => { it('should save a file with metadata added', async () => { const s3 = makeS3Adapter(options); - s3._s3Client.upload = (params, callback) => { + s3._s3Client.putObject = (params, callback) => { const { Metadata } = params; expect(Metadata).toEqual({ foo: 'bar' }); const data = { @@ -593,7 +594,7 @@ describe('S3Adapter tests', () => { it('should save a file with tags added', async () => { const s3 = makeS3Adapter(options); - s3._s3Client.upload = (params, callback) => { + s3._s3Client.putObject = (params, callback) => { const { Tagging } = params; expect(Tagging).toEqual('foo=bar&baz=bin'); const data = { @@ -610,11 +611,11 @@ describe('S3Adapter tests', () => { // Create adapter options.directAccess = true; const s3 = makeS3Adapter(options); - spyOn(s3._s3Client, 'upload').and.callThrough(); + spyOn(s3._s3Client, 'putObject').and.callThrough(); // Save file await s3.createFile('file.txt', 'hello world', 'text/utf8', {}); // Validate - const calls = s3._s3Client.upload.calls.all(); + const calls = s3._s3Client.putObject.calls.all(); expect(calls.length).toBe(1); calls.forEach((call) => { expect(call.args[0].ACL).toBe('public-read'); @@ -624,11 +625,11 @@ describe('S3Adapter tests', () => { it('should save a file with proper ACL without direct access', async () => { // Create adapter const s3 = makeS3Adapter(options); - spyOn(s3._s3Client, 'upload').and.callThrough(); + spyOn(s3._s3Client, 'putObject').and.callThrough(); // Save file await s3.createFile('file.txt', 'hello world', 'text/utf8', {}); // Validate - const calls = s3._s3Client.upload.calls.all(); + const calls = s3._s3Client.putObject.calls.all(); expect(calls.length).toBe(1); calls.forEach((call) => { expect(call.args[0].ACL).toBeUndefined(); @@ -640,11 +641,11 @@ describe('S3Adapter tests', () => { options.directAccess = true; options.fileAcl = 'private'; const s3 = makeS3Adapter(options); - spyOn(s3._s3Client, 'upload').and.callThrough(); + spyOn(s3._s3Client, 'putObject').and.callThrough(); // Save file await s3.createFile('file.txt', 'hello world', 'text/utf8', {}); // Validate - const calls = s3._s3Client.upload.calls.all(); + const calls = s3._s3Client.putObject.calls.all(); expect(calls.length).toBe(1); calls.forEach((call) => { expect(call.args[0].ACL).toBe('private'); @@ -656,11 +657,11 @@ describe('S3Adapter tests', () => { options.directAccess = true; options.fileAcl = 'none'; const s3 = makeS3Adapter(options); - spyOn(s3._s3Client, 'upload').and.callThrough(); + spyOn(s3._s3Client, 'putObject').and.callThrough(); // Save file await s3.createFile('file.txt', 'hello world', 'text/utf8', {}); // Validate - const calls = s3._s3Client.upload.calls.all(); + const calls = s3._s3Client.putObject.calls.all(); expect(calls.length).toBe(1); calls.forEach((call) => { expect(call.args[0].ACL).toBeUndefined();