Generate S3 URL in "virtual-hosted style" format - amazon-web-services

I'm using Javascript SDK on AWS Lambda to generate signed URL. But getSignedUrl is only returning path style URL. I have tried setting s3ForcePathStyle as false.
const params = {
Bucket: 'bucket_name',
Key: 'key_name'
};
const options = {
signatureVersion: 'v4',
useAccelerateEndpoint: false,
// endpoint: new AWS.Endpoint('https://bucket_name.s3.amazonaws.com'),
s3ForcePathStyle: false
};
const client = new AWS.S3(options);
exports.handler = async (event) => {
const signedURL = await (new Promise((resolve, reject) => {
client.getSignedUrl('putObject', params, (err, data) => {
if (err) {
reject(err);
}
else {
resolve(data);
}
});
}));
return signedURL;
};
If I uncomment endpoint: new AWS.Endpoint('https://bucket_name.s3.amazonaws.com') I'm getting an absurd URL which combines both:
https://bucket_name.s3.amazonaws.com/bucket_name/key_name......
tl;dr
https://s3.ap-south-1.amazonaws.com/bucket_name/key_name..... # Code returns path-style
https://bucket_name.s3.amazonaws.com/key_name..... # I want virtual-hosted-style

Related

Migrate GetObject (v2) to GetObjectCommand (v3) - aws-sdk

I'm trying to migrate an Express endpoint from v2 to v3 of the aws-sdk for JavaScript. The endpoint is a file downloader for AWS S3.
In version 2, I passed the result of GetObject back to the browser in a readable stream. In version 3 that same technique fails with the error:
TypeError: data.Body.createReadStream is not a function
How do I work with the data that returned from the new GetObjectCommand? Is it a blob? I'm struggling to find anything useful in the v3 SDK docs.
Here are the two versions of the endpoint:
import AWS from 'aws-sdk'
import dotenv from 'dotenv'
import { GetObjectCommand, S3Client } from '#aws-sdk/client-s3'
dotenv.config()
// VERSION 3 DOWNLOADER - FAILS
const getFileFromS3v3 = async (req, res) => {
const client = new S3Client({ region: 'us-west-2' })
const params = {
Bucket: process.env.AWS_BUCKET,
Key: 'Tired.pdf',
}
const command = new GetObjectCommand(params)
try {
const data = await client.send(command)
console.log(data)
data.Body.createReadStream().pipe(res)
} catch (error) {
console.log(error)
}
}
// VERSION 2 DOWNLOADER - WORKS
const getFileFromS3 = async (req, res) => {
const filename = req.query.filename
var s3 = new AWS.S3()
var s3Params = {
Bucket: process.env.AWS_BUCKET,
Key: 'Tired.pdf',
}
// if the file header exists, stream the file to the response
s3.headObject(s3Params, (err) => {
if (err && err.code === 'NotFound') {
console.log('File not found: ' + filename)
} else {
s3.getObject(s3Params).createReadStream().pipe(res)
}
})
}
export { getFileFromS3, getFileFromS3v3 }
This version 3 code works. Thanks to a major assist, the trick was to pipe data.Body and not use any of the fileStream methods.
import { GetObjectCommand, S3Client } from '#aws-sdk/client-s3'
import dotenv from 'dotenv'
dotenv.config()
const getFileFromS3 = async (req, res) => {
const key = req.query.filename
const client = new S3Client({ region: process.env.AWS_REGION })
const params = {
Bucket: process.env.AWS_BUCKET,
Key: key,
}
const command = new GetObjectCommand(params)
try {
const data = await client.send(command)
data.Body.pipe(res)
} catch (error) {
console.log(error)
}
}
export { getFileFromS3 }
When called from this frontend function the code above returns the file from S3 to the browser.
const downloadFile = async (filename) => {
const options = {
url: `/api/documents/?filename=${filename}`,
method: 'get',
responseType: 'blob',
}
try {
const res = await axios(options)
fileDownload(res.data, filename)
} catch (error) {
console.log(error)
}
}

NestJS: Image Upload & Serve API

I tried to create an API for uploading & retrieving images with NestJS. Images should be stored on S3.
What I currently have:
Controller
#Post()
#UseInterceptors(FileFieldsInterceptor([
{name: 'photos', maxCount: 10},
]))
async uploadPhoto(#UploadedFiles() files): Promise<void> {
await this.s3Service.savePhotos(files.photos)
}
#Get('/:id')
#Header('content-type', 'image/jpeg')
async getPhoto(#Param() params,
#Res() res) {
const photoId = PhotoId.of(params.id)
const photoObject = await this.s3Service.getPhoto(photoId)
res.send(photoObject)
}
S3Service
async savePhotos(photos: FileUploadEntity[]): Promise<any> {
return Promise.all(photos.map(photo => {
const filePath = `${moment().format('YYYYMMDD-hhmmss')}${Math.floor(Math.random() * (1000))}.jpg`
const params = {
Body: photo.buffer,
Bucket: Constants.BUCKET_NAME,
Key: filePath,
}
return new Promise((resolve) => {
this.client.putObject(params, (err: any, data: any) => {
if (err) {
logger.error(`Photo upload failed [err=${err}]`)
ExceptionHelper.throw(ErrorCodes.SERVER_ERROR_UNCAUGHT_EXCEPTION)
}
logger.info(`Photo upload succeeded [filePath=${filePath}]`)
return resolve()
})
})
}))
}
async getPhoto(photoId: PhotoId): Promise<AWS.S3.Body> {
const object: S3.GetObjectOutput = await this.getObject(S3FileKey.of(`${Constants.S3_PHOTO_PATH}/${photoId.value}`))
.catch(() => ExceptionHelper.throw(ErrorCodes.RESOURCE_NOT_FOUND_PHOTO)) as S3.GetObjectOutput
logger.info(JSON.stringify(object.Body))
return object.Body
}
async getObject(s3FilePath: S3FileKey): Promise<S3.GetObjectOutput> {
logger.info(`Retrieving object from S3 s3FilePath=${s3FilePath.value}]`)
return this.client.getObject({
Bucket: Constants.BUCKET_NAME,
Key: s3FilePath.value
}).promise()
.catch(err => {
logger.error(`Could not retrieve object from S3 [err=${err}]`)
ExceptionHelper.throw(ErrorCodes.SERVER_ERROR_UNCAUGHT_EXCEPTION)
}) as S3.GetObjectOutput
}
The photo object actually ends up in S3, but when I download it I can't open it.
Same for the GET => can't be displayed.
What general mistake(s) I'm making here?
Not sure what values are you returning to your consumer and which values they use to get the Image again; Could you post how the actual response looks like, what is the request and verify, if the FQDN & Path match?
It seems you forgot about ACL as well, i.e. the resources you upload this way are not public-read by default.
BTW you could use aws SDK there:
import { Injectable } from '#nestjs/common'
import * as AWS from 'aws-sdk'
import { InjectConfig } from 'nestjs-config'
import { AwsConfig } from '../../config/aws.config'
import UploadedFile from '../interfaces/uploaded-file'
export const UPLOAD_WITH_ACL = 'public-read'
#Injectable()
export class ImageUploadService {
s3: AWS.S3
bucketName
cdnUrl
constructor(#InjectConfig() private readonly config) {
const awsConfig = (this.config.get('aws') || { bucket: '', secretKey: '', accessKey: '', cdnUrl: '' }) as AwsConfig // read from envs
this.bucketName = awsConfig.bucket
this.cdnUrl = awsConfig.cdnUrl
AWS.config.update({
accessKeyId: awsConfig.accessKey,
secretAccessKey: awsConfig.secretKey,
})
this.s3 = new AWS.S3()
}
upload(file: UploadedFile): Promise<string> {
return new Promise((resolve, reject) => {
const params: AWS.S3.Types.PutObjectRequest = {
Bucket: this.bucketName,
Key: `${Date.now().toString()}_${file.originalname}`,
Body: file.buffer,
ACL: UPLOAD_WITH_ACL,
}
this.s3.upload(params, (err, data: AWS.S3.ManagedUpload.SendData) => {
if (err) {
return reject(err)
}
resolve(`${this.cdnUrl}/${data.Key}`)
})
})
}
}
For anyone having the same troubles, I finally figured it out:
I enabled binary support on API Gateway (<your-gateway> Settings -> Binary Media Types -> */*) and then returned all responses from lambda base64 encoded. API Gateway will do the decode automatically before returning the response to the client.
With serverless express you can can enable the auto base64 encoding easily at the server creation:
const BINARY_MIME_TYPES = [
'application/javascript',
'application/json',
'application/octet-stream',
'application/xml',
'font/eot',
'font/opentype',
'font/otf',
'image/jpeg',
'image/png',
'image/svg+xml',
'text/comma-separated-values',
'text/css',
'text/html',
'text/javascript',
'text/plain',
'text/text',
'text/xml',
]
async function bootstrap() {
const expressServer = express()
const nestApp = await NestFactory.create(AppModule, new ExpressAdapter(expressServer))
await nestApp.init()
return serverlessExpress.createServer(expressServer, null, BINARY_MIME_TYPES)
}
In the Controller, you're now able to just return the S3 response body:
#Get('/:id')
async getPhoto(#Param() params,
#Res() res) {
const photoId = PhotoId.of(params.id)
const photoObject: S3.GetObjectOutput = await this.s3Service.getPhoto(photoId)
res
.set('Content-Type', 'image/jpeg')
.send(photoObject.Body)
}
Hope this helps somebody!

AWS Lambda - unable to convert SDK call to promise

I have a Lambda which looks like so:
module.exports.handler = (event, context, callback) => {
AWS.config.setPromisesDependency(null);
const uploadPromise = s3.upload(params).promise();
uploadPromise.then((data) => {
const response = {
...
};
return response;
}).catch((error) => {
console.log(error);
});
};
Calling it from Postman results in server error in Postman. CloudWatch logs have no further info.
Doing:
s3.upload(params, (error, data) => {
if (error) {
console.error("error occurred storing to s3: ", error);
return;
}
const response = {
...
};
return response;
});
does not result in a server error.
I am trying to follow the information from AWS that can be found here:
https://aws.amazon.com/blogs/developer/support-for-promises-in-the-sdk/
Postman is able to upload to Lambda by doing the following with async/await and try/catch:
exports.handler = async function(event, context) {
const s3 = new AWS.S3();
const encodedImage = util.inspect(event.body);
const decodedImage = Buffer.from(encodedImage, "base64");
const filePath = "test.png";
const params = {
Body: decodedImage,
Bucket: "my bucket",
Key: filePath,
ACL: "public-read",
ContentType: "mime/png"
};
try {
const result = await s3.upload(params).promise();
const response = {
statusCode: 200,
headers: {
my_header: "my_value"
},
body: JSON.stringify(result),
isBase64Encoded: false
};
return response;
} catch (error) {
console.log('error')
}
};

Lambda function s3.getObject returns "Internal server error"

This code works just fine locally using nodejs. Images download from s3, write to file.
However, in Lambda (using nodejs 8.10) I'm getting "Internal Server Error" when testing the function with this in the Logs:
"Execution failed due to configuration error: Malformed Lambda proxy response"
I am using the lambda proxy response in the callback, but clearly some AWS SDK error with S3 is not getting caught.
I do have a role setup with S3 full access that the Lambda has access to.
What am I missing with my first Lambda function? Docs and tutorials I've followed correctly and it is not working.
const async = require('async')
const aws = require('aws-sdk')
const fs = require('fs')
const exec = require('child_process').exec
const bucket = 'mybucket'
const s3Src = 'bucket_prefix'
const s3Dst = 'new_prefix'
const local = `${__dirname}/local/`
aws.config.region = 'us-west-2'
const s3 = new aws.S3()
exports.handler = async (event, context, callback) => {
const outputImage = 'hello_world.png'
const rack = JSON.parse(event.body)
const images = my.images
async.waterfall([
function download(next) {
let downloaded = 0
let errors = false
let errorMessages = []
for (let i = 0; i < images.length; i++) {
let key = `${s3Src}/${images[i].prefix}/${images[i].image}`,
localImage = `${local}${images[i].image}`
getBucketObject(bucket, key, localImage).then(() => {
++downloaded
if (downloaded === images.length) { // js is non blocking, need to check if all images have been downloaded. If so, then go to next function
if (errors) {
next(errorMessages.join(' '))
} else {
next(null)
}
}
}).catch(error => {
errorMessages.push(`${error} - ${localImage}`)
++downloaded
errors = true
})
}
function getBucketObject(bucket, key, dest) {
return new Promise((resolve, reject) => {
let ws = fs.createWriteStream(dest)
ws.once('error', (err) => {
return reject(err)
})
ws.once('finish', () => {
return resolve(dest)
})
let s3Stream = s3.getObject({
Bucket: bucket,
Key: key
}).createReadStream()
s3Stream.pause() // Under load this will prevent first few bytes from being lost
s3Stream.on('error', (err) => {
return reject(err)
})
s3Stream.pipe(ws)
s3Stream.resume()
})
}
}
], err => {
if (err) {
let response = {
"statusCode": 400,
"headers": {
"my_header": "my_value"
},
"body": JSON.stringify(err),
"isBase64Encoded": false
}
callback(null, response)
} else {
let response = {
"statusCode": 200,
"headers": {
"my_header": "my_value"
},
"body": JSON.stringify(`<img src="${local}${outputImage}" />`),
"isBase64Encoded": false
}
callback(null, response)
}
}
)
}
Response should be always sent to callback function. Your code sends response only on error. That's why Lambda executor thinks your code fails.
BTW - should your functions in async.waterfall be separated with coma, as two tasks?
Locally, I've been running nodejs 10.10 and lambda currently is at 8.10. That is a big part I'm sure. In the end I had to remove the async. I had to move the getBucketObject function out of the waterfall. Once I made those adjustments it started working. And another issue was the downloaded images needed to go into "/tmp" directory.
const aws = require('aws-sdk')
const async = require('async')
const fs = require('fs')
const bucket = 'mybucket'
const s3Src = 'mys3src'
const local = '/tmp/'
aws.config.region = 'us-west-2'
const s3 = new aws.S3()
exports.handler = (event, context, callback) => {
const outputImage = 'hello_world.png'
async.waterfall([
function download(next) {
let downloaded = 0,
errorMessages = []
for (let i = 0; i < event['images'].length; i++) {
let key = `${s3Src}/${event['images'][i]['prefix']}/${event['images'][i]['image']}`,
localImage = `${local}${event['images'][i]['image']}`
getBucketObject(bucket, key, localImage).then(() => {
downloaded++
if (downloaded === event['images'].length) {
if (errorMessages.length > 0) {
next(errorMessages.join(' '))
} else {
console.log('All downloaded')
next(null)
}
}
}).catch(error => {
downloaded++
errorMessages.push(`${error} - ${localImage}`)
if (downloaded === event['images'].length) {
next(errorMessages.join(' '))
}
})
}
}
], err => {
if (err) {
console.error(err)
callback(null, {
"statusCode": 400,
"body": JSON.stringify(err),
"isBase64Encoded": false
})
} else {
console.log('event image created!')
callback(null, {
"statusCode": 200,
"body": JSON.stringify(`<img src="${local}${outputImage}" />`),
"isBase64Encoded": false
})
}
}
)
}
function getBucketObject(bucket, key, dest) {
return new Promise((resolve, reject) => {
let ws = fs.createWriteStream(dest)
ws.once('error', (err) => {
return reject(err)
})
ws.once('finish', () => {
return resolve(dest)
})
let s3Stream = s3.getObject({
Bucket: bucket,
Key: key
}).createReadStream()
s3Stream.pause() // Under load this will prevent first few bytes from being lost
s3Stream.on('error', (err) => {
return reject(err)
})
s3Stream.pipe(ws)
s3Stream.resume()
})
}

AWS S3 promise meteor

So, I'm using AWS = Npm.require('aws-sdk'); from Amazon.
Apparently promise are implemented. http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Request.html#promise-property
See my functions (Server):
s3 = new AWS.S3({ apiVersion: "2006-03-01" });
bucketExists = (bucket) => {
check(bucket, String);
const params = {
Bucket: bucket
};
return s3.putObject({Bucket: 'bucket', Key: 'key'}).promise();
};
Meteor.methods({
addPhoto(userId, photo) {
check(userId, String);
check(photo, Object);
bucketExists(userId)
.then(
function(value) {
console.log("Contents: " + value);
})
.catch(function(reason) {
console.error("Error or timeout", reason);
});
}
});
Exeption:
Exception while invoking method 'addPhoto' TypeError: Object [object Object] has no method 'promise'.
WTF
So, I try without call promise(). Like that:
bucketExists = (bucket) => {
check(bucket, String);
const params = {
Bucket: bucket
};
return new Promise(
(resolve, reject) => {
s3.waitFor('bucketExists', params, (err, res) => {
if (err) reject(err);
else resolve(res);
});
});
};
But nothing appear. I start with ES6. So please help.
Thanks