-
-
Notifications
You must be signed in to change notification settings - Fork 88
refactor: Migrate S3 Client from AWS SDK v2 to v3 #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
98cf010
6f49645
889ca3e
a9a0d46
ed0fd9e
dfe0fb2
06f8483
771b533
f493800
e5dcf37
0897b90
3ff1c77
92d4c10
bf36a95
da2c3e1
2eef4a1
05b2035
0d3e766
24b9a33
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
logs | ||
*.log | ||
npm-debug.log* | ||
.env | ||
|
||
# Runtime data | ||
pids | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,10 @@ | |
// | ||
// Stores Parse files in AWS S3. | ||
|
||
const AWS = require('aws-sdk'); | ||
const { S3Client, CreateBucketCommand, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3'); | ||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); | ||
const deasync = require('deasync'); | ||
|
||
const optionsFromArguments = require('./lib/optionsFromArguments'); | ||
|
||
const awsCredentialsDeprecationNotice = function awsCredentialsDeprecationNotice() { | ||
|
@@ -36,6 +39,15 @@ function buildDirectAccessUrl(baseUrl, baseUrlFileKey, presignedUrl, config, fil | |
return directAccessUrl; | ||
} | ||
|
||
function responseToBuffer(response) { | ||
return new Promise((resolve, reject) => { | ||
const chunks = []; | ||
response.Body.on('data', (chunk) => chunks.push(chunk)); | ||
response.Body.on('end', () => resolve(Buffer.concat(chunks))); | ||
response.Body.on('error', reject); | ||
}); | ||
} | ||
|
||
class S3Adapter { | ||
// Creates an S3 session. | ||
// Providing AWS access, secret keys and bucket are mandatory | ||
|
@@ -65,6 +77,21 @@ class S3Adapter { | |
globalCacheControl: this._globalCacheControl, | ||
}; | ||
|
||
// const s3Options = { | ||
// region: this._region, | ||
// // Add other configuration options if needed | ||
// }; | ||
|
||
if (options.accessKey && options.secretKey) { | ||
awsCredentialsDeprecationNotice(); | ||
s3Options.credentials = { | ||
accessKeyId: options.accessKey, | ||
secretAccessKey: options.secretKey, | ||
}; | ||
} else if (options.credentials) | ||
s3Options.credentials = options.credentials; | ||
|
||
|
||
if (options.accessKey && options.secretKey) { | ||
awsCredentialsDeprecationNotice(); | ||
s3Options.accessKeyId = options.accessKey; | ||
|
@@ -73,29 +100,28 @@ class S3Adapter { | |
|
||
Object.assign(s3Options, options.s3overrides); | ||
|
||
this._s3Client = new AWS.S3(s3Options); | ||
this._s3Client = new S3Client(s3Options); | ||
this._hasBucket = false; | ||
} | ||
|
||
createBucket() { | ||
let promise; | ||
if (this._hasBucket) { | ||
promise = Promise.resolve(); | ||
} else { | ||
promise = new Promise((resolve) => { | ||
this._s3Client.createBucket(() => { | ||
this._hasBucket = true; | ||
resolve(); | ||
}); | ||
}); | ||
async createBucket() { | ||
if (this._hasBucket) return; | ||
|
||
try { | ||
await this._s3Client.send(new CreateBucketCommand({ Bucket: this._bucket })); | ||
this._hasBucket = true; | ||
} catch (error) { | ||
if (error.name === 'BucketAlreadyOwnedByYou') | ||
this._hasBucket = true; | ||
else throw error; | ||
} | ||
return promise; | ||
} | ||
|
||
// For a given config object, filename, and data, store a file in S3 | ||
// Returns a promise containing the S3 object creation response | ||
createFile(filename, data, contentType, options = {}) { | ||
async createFile(filename, data, contentType, options = {}) { | ||
const params = { | ||
Bucket: this._bucket, | ||
Key: this._bucketPrefix + filename, | ||
Body: data, | ||
}; | ||
|
@@ -128,46 +154,65 @@ class S3Adapter { | |
const serializedTags = serialize(options.tags); | ||
params.Tagging = serializedTags; | ||
} | ||
return this.createBucket().then(() => new Promise((resolve, reject) => { | ||
this._s3Client.upload(params, (err, response) => { | ||
if (err !== null) { | ||
return reject(err); | ||
} | ||
return resolve(response); | ||
}); | ||
})); | ||
await this.createBucket(); | ||
const command = new PutObjectCommand(params); | ||
const response = await this._s3Client.send(command); | ||
return response; | ||
} | ||
|
||
deleteFile(filename) { | ||
return this.createBucket().then(() => new Promise((resolve, reject) => { | ||
const params = { | ||
Key: this._bucketPrefix + filename, | ||
}; | ||
this._s3Client.deleteObject(params, (err, data) => { | ||
if (err !== null) { | ||
return reject(err); | ||
} | ||
return resolve(data); | ||
}); | ||
})); | ||
async deleteFile(filename) { | ||
const params = { | ||
Bucket: this._bucket, | ||
Key: this._bucketPrefix + filename, | ||
}; | ||
await this.createBucket() | ||
const command = new DeleteObjectCommand(params); | ||
const response = await this._s3Client.send(command) | ||
return response; | ||
} | ||
|
||
// 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 }; | ||
return this.createBucket().then(() => new Promise((resolve, reject) => { | ||
this._s3Client.getObject(params, (err, data) => { | ||
if (err !== null) { | ||
return reject(err); | ||
} | ||
// Something happened here... | ||
if (data && !data.Body) { | ||
return reject(data); | ||
} | ||
return resolve(data.Body); | ||
async getFileData(filename) { | ||
const params = { | ||
Bucket: this._bucket, | ||
Key: this._bucketPrefix + filename, | ||
}; | ||
await this.createBucket() | ||
const command = new GetObjectCommand(params); | ||
const response = await this._s3Client.send(command); | ||
if (response && !response.Body) throw new Error(response); | ||
|
||
const buffer = await responseToBuffer(response); | ||
return buffer; | ||
} | ||
|
||
// Exposed only for testing purposes | ||
getSignedUrlSync(client, command, options) { | ||
let isDone = false; | ||
let signedUrl = ''; | ||
let error = null; | ||
|
||
getSignedUrl(client, command, options) | ||
.then((url) => { | ||
signedUrl = url; | ||
isDone = true; | ||
}) | ||
.catch((err) => { | ||
error = err; | ||
isDone = true; | ||
}); | ||
})); | ||
|
||
// Block the event loop until the promise resolves | ||
while (!isDone) { | ||
deasync.sleep(100); // Sleep for 100 milliseconds | ||
|
||
} | ||
|
||
if (error) { | ||
throw error; | ||
} | ||
|
||
return signedUrl; | ||
} | ||
|
||
// Generates and returns the location of a file stored in S3 for the given request and filename | ||
|
@@ -184,12 +229,11 @@ class S3Adapter { | |
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); | ||
const options = this._presignedUrlExpires ? { expiresIn: this._presignedUrlExpires } : {}; | ||
|
||
const command = new GetObjectCommand(params); | ||
presignedUrl = this.getSignedUrlSync(this._s3Client, command, options); | ||
|
||
if (!this._baseUrl) { | ||
return presignedUrl; | ||
} | ||
|
@@ -203,30 +247,31 @@ class S3Adapter { | |
return buildDirectAccessUrl(this._baseUrl, baseUrlFileKey, presignedUrl, config, filename); | ||
} | ||
|
||
handleFileStream(filename, req, res) { | ||
async handleFileStream(filename, req, res) { | ||
const params = { | ||
Bucket: this._bucket, | ||
Key: this._bucketPrefix + filename, | ||
Range: req.get('Range'), | ||
}; | ||
return this.createBucket().then(() => new Promise((resolve, reject) => { | ||
this._s3Client.getObject(params, (error, data) => { | ||
if (error !== null) { | ||
return reject(error); | ||
} | ||
if (data && !data.Body) { | ||
return reject(data); | ||
} | ||
res.writeHead(206, { | ||
'Accept-Ranges': data.AcceptRanges, | ||
'Content-Length': data.ContentLength, | ||
'Content-Range': data.ContentRange, | ||
'Content-Type': data.ContentType, | ||
}); | ||
res.write(data.Body); | ||
res.end(); | ||
return resolve(data.Body); | ||
}); | ||
})); | ||
|
||
await this.createBucket(); | ||
const command = new GetObjectCommand(params); | ||
const data = await this._s3Client.send(command); | ||
if (data && !data.Body) throw new Error(data); | ||
|
||
res.writeHead(206, { | ||
'Accept-Ranges': data.AcceptRanges, | ||
'Content-Length': data.ContentLength, | ||
'Content-Range': data.ContentRange, | ||
'Content-Type': data.ContentType, | ||
}); | ||
data.Body.on('data', (chunk) => res.write(chunk)); | ||
data.Body.on('end', () => res.end()); | ||
data.Body.on('error', (e) => { | ||
res.status(404); | ||
res.send(e.message); | ||
}); | ||
return responseToBuffer(data); | ||
} | ||
} | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.