Hi - I’m struggling to get images returned through...
# help
r
Hi - I’m struggling to get images returned through a serverless defined AWS configuration. Apologies for the long post but wanted to share the detail since there are various stages. I have a API Gateway resource that calls a lambda function to retrieve an image from an S3 bucket. The image is confirmed to be in the bucket and I can download it from the console it can opened and viewed fine. In serverless.ts I have:
Copy code
OMWGetThumbPhoto: {
  handler: '.build/main/handler/someHandler.handleGetThumb',
  layers: [
    sharpLayerArn
  ],
  events: [
    {
      http: {
        method: 'get',
        path: 'resourceConfig/{resourceId}/thumb',
        cors: true                  
      }
    }
  ]
},
In the code lambda function the image is retrieved like this:
Copy code
public static async getFileByName(bucket: string, name: string): Promise<AWS.S3.GetObjectOutput> {
  const s3 = S3Factory.getS3();
  const getObjectParams: GetObjectRequest = {
    Bucket: bucket,
    Key: name,
  };
  return s3.getObject(getObjectParams).promise();
}
The data for the response is built like this (none of the exceptions throw and image is determined to be of type image/png):
Copy code
public static async getPhotoByName(name: string, fullsize = true): Promise<PhotoAndMime> {
  const photoName = fullsize ? name : `${this.SMALL_PHOTO_PREFIX}${name}`;

  const photo = await FileStorage.getFileByName(this.getBucketId(), photoName);

  if (!photo || !photo.Body) throw new Error(`Failed to retrieve image file for photo '${photoName}'`);
  let photoStr = '';
  const base64Photo = photo.Body.toString('base64');
  const type = await FileType.fromBuffer(photo.Body as Buffer);
  if (!type) throw new Error(`Invalid or missing image type for for photo '${photoName}'`);
  <http://log.info|log.info>(`File Type determined to be: ${type.mime}`);
  photoStr = `data:${type.mime};base64,${base64Photo}`;

  return { photo: photoStr, mimeType: type.mime };
}
Then the response is returned like this
Copy code
async function handleGetCommon(event: APIGatewayProxyEvent, fullSize: boolean): Promise<APIGatewayProxyResult> {
  let response: APIGatewayProxyResult;
  try {
    const result = await ResourcePhotoService.getPhotoById(resourceId, datasetId, fullSize);

    if (result) {
      response = {
        statusCode: 200,
        headers: { 'Content-Type': result.mimeType },
        body: result.photo,
        isBase64Encoded: true,
      };
    } else {
      response = { statusCode: 204, body: '' };
    }
  } catch (err) {
    response = HandlerUtils.handleCaughtError(err, `Could not retrieve resource config photo`);
  }

  Utils.addCorsHeaders(response);
  return response;
}
The web application that calls the API Gateway uses axios like this:
Copy code
const { data } = await axios.get(url, {
  headers: {
    accept: 'image/jpg, image/png',
    Authorization: `Bearer ${token}`,
  },
  responseType: 'arraybuffer',
  params,
});
and tries to process the response the data uses this function to convert the response to an image data URL
Copy code
export function convertBinaryToImage(rawPhoto) {
  return new Promise((resolve) => {
    if (rawPhoto?.byteLength > 0) {
      FileType.fromBuffer(rawPhoto).then((type) => {
        const blob = new Blob([rawPhoto], { type: type.mime });
        const reader = new FileReader();
        reader.readAsDataURL(blob);
        reader.onload = () => {
          return resolve(reader.result); // data url
        };
      });
    }
  });
}
However it cannot determine the file type, something seems to be getting corrupted along the way. Has anyone solved the problem of retrieving a private image from an S3 bucket and return that via API Gateway to be displayed on a web page? I feel like I’m missing some serverless configuration somewhere. I’ve tried setting response.contentHandling CONVERT_TO_TEXT and CONVERT_TO_BINARY but that didn’t seem to make any difference.
It seems the answer to this is to convert the received arraybuffer differently in the browser by doing
Copy code
String.fromCharCode.apply(null, new Uint8Array(rawThumb))
Although, I don’t yet understand why and how this relates to how API Gateway transforms responses!
f
hmm.. there seems to be quite a few moving pieces.. it’d might be easier if there’s a working version of this and I can poke around the code
as a side note.. I try to fetch image/binary data from S3 directly in the frontend, instead of passing it through the Lambda
r
Thanks Frank. Presumably that's not possible with private images?
Or is that using the sdk client side, maybe with amplify?
This is some sample frontend code, it uses the aws-amplify library. But u don’t have to use the whole amplify framework.
If you are using Cognito Identity Pool for authorization, you assign this permission to the authorized users
Copy code
new iam.PolicyStatement({
        actions: ["s3:*"],
        effect: iam.Effect.ALLOW,
        resources: [
          bucketArn + "/private/${<http://cognito-identity.amazonaws.com:sub|cognito-identity.amazonaws.com:sub>}/*",
        ],
      })
r
I considered the storage object in amplify as we're using amplify for the auth to cognito. I think the problem when I looked was that it didn't support more than one bucket
f
so the authenticated users can only access their own “folder” in the s3 bucket
R u using cognito identity pool?
r
Yes
f
And u created an authenticated role for the identity pool right?
r
Yep
f
nice, then in the authenticated role, u can pretty much allow access to any number of buckets:
Copy code
action: s3:*
effect: allow
resource: [
  arn:s3:...bucket1/${<http://cognito-identity.amazonaws.com:sub|cognito-identity.amazonaws.com:sub>}/*,
  arn:s3:...bucket2/${<http://cognito-identity.amazonaws.com:sub|cognito-identity.amazonaws.com:sub>}/*,
  ...
]
this is some pseudo IAM policy
the
${<http://cognito-identity.amazonaws.com:sub|cognito-identity.amazonaws.com:sub>}
will be the authenticated user’s Cognito Identity Pool’s user id
so the user has access to his own folder in bucket1 and bucket 2 in this case
r
It's actually a bit easier here because the bucket will be accessible to all authenticated users. I'll read the article you shared. We were originally trying to use Storage from aws-amplify but as we needed more than one bucket to be accessed on the app, it didn't work as you could only configure a single bucket
f
I see.. the example above only relies on amplify for the login
after that you should be able to access any number of buckets (any resources in general) the user is granted to based on the auth role iam poilcy
r
Great, I'll read it shortly. Thank you!
Hi Frank. Just had a quick look, it is using the amplify Storage module
f
It’s using the module to do the request signing I think…
r
Yeah, and last time I checked you could only configure a single bucket 🙁
f
@Jay knows this better.. lemme get him to confirm
r
OK, cool
p
@Ross Coundon would it be an alternative to use the lambda to only give you a signed URL to your private image?
r
Potentially, but there will be lots of them so not sure that's scalable
p
not sure if I understood what wouldn't scale 🤔 I think the function to sign doesn't make any calls to remote services. To me, it looks simpler and faster than processing the image in lambda as in the code you've shared. Of course, if you can use Amplify to abstract all the madness it would be great.
r
@Paulo - maybe it’s my misunderstanding of how signing works, sounds like a neat solution in that case! Thank you
@Frank @Jay This article from last year shows an undocumented workaround for the lack of support in amplify for multiple buckets https://medium.com/dnx-labs/using-multiple-buckets-aws-amplify-66083d7a4301 I can’t rely on that for a production app
@Paulo - could I create a presigned URL for the bucket that would allow access to all the objects in that bucket?
p
dunno. never tried that but I think it works by object
r
Just thinking through the logic, managing the presigned URLs returned from a stateless lambda might be tricky - is there an issue with creating a new URL for each request? I.e. in the app, a few users might be logged in uploading and downloading images. I can’t really cache the URL because the image to download might change. Therefore, I think I’d need to create a new array of URLs every time someone makes a request
p
Not sure If I followed.. but those URLs are not supposed to be cached indeed.. they are temporary so I dont think it would be a problem to generate new ones every time you need
r
Ok, as you say, they’re client code generated so probs not a biggie to regenerate. 🤔
p
guess so
r
As I’ve got this working I might keep things the way they are however this has given me a great idea for simplifying another area of the app
So thanks for helping
p
cool. no probs 👍
j
Chiming in a little late here. And it's been a while since I looked at this. But way back, we used to use the AWS SDK directly to do uploads. You should be able to do it with this. Here's the old version of the chapter — https://branchv125--serverless-stack.netlify.app/chapters/upload-a-file-to-s3.html. And here's the version of the React code from back then — https://github.com/AnomalyInnovations/serverless-stack-demo-client/blob/v1.2.5/src/libs/awsLib.js.
r
Great, thanks @Jay. I'll have a look and compare to what I'm doing. It's funny, it seems to me like it should be a fairly common use case but maybe not