Display an image from Fly storage

Hi, I’m developing a Shopify app using Remix.

I’ve implemented image display, and it’s working.
Here’s the folder structure for images saved on Tigris Storage.
My bucket name: myhdd

images
|___ item-1.jpg
|___ item-2.jpg
|___ item-3.jpg
|___ item-4.jpg
|___ item-5.jpg

My example code:

import { useState, useEffect, useLoaderData } from 'react';
import { Page, Grid } from '@shopify/polaris';
import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const ImageItem = ( { item } ) => {
	const [ imageUrl, setImageUrl ] = useState( null );

	const S3 = new S3Client({
		region: 'auto',
		endpoint: 'https://fly.storage.tigris.dev',
		credentials: {
			accessKeyId: 'my_accessKeyId',
			secretAccessKey: 'my_secretAccessKey'
		}
	});

	const getImageUrl = async () => {
		const ssd   = await S3.send( new ListObjectsV2Command({ Bucket: 'myhdd' }) );
		const image = ssd?.Contents?.find( obj => obj.Key === `images/${ item.handle }.jpg` );
		const url   = await getSignedUrl( S3, new GetObjectCommand({ Bucket: 'myhdd', Key: image.Key }), { expiresIn: 3600 } );

		setImageUrl( url );
	}

	useEffect(
		() => getImageUrl(),
		[ item.handle ]
	);

	return (
		<img width="100%" height="250" src={ imageUrl } style={{ objectFit: 'cover', objectPosition: 'center' }}/>
	)
}

const App = () => {
	const { myData } = useLoaderData(); // My data list.

	return (
		<Page title="Library">
			<Grid>
				{
					myData.map(
						item => (
							<Grid.Cell key={ item.handle } columnSpan={{xs: 6, sm: 3, md: 3, lg: 6, xl: 6}}>
								<ImageItem item={ item }></ImageItem>
							</Grid.Cell>
						)
					)
				}
			</Grid>
		</Page>
	);
};

export default App;

And output image like this:

<img width="100%" height="250" style="object-fit: cover; object-position: center center;" src="https://myhdd.fly.storage.tigris.dev/images/item-01.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=tid_eGXVwyxdwSmrWzAqrEiNriyKBWDugrHNsiNMYOmDjuLIuZAoCi%2F20241022%2Fauto%2Fs3%2Faws4_request&amp;X-Amz-Date=20241022T053225Z&amp;X-Amz-Expires=3600&amp;X-Amz-Signature=2bee4cedf69b159daac4e326d9f19fd18cb9a74cba18b752531af00673fd67eb&amp;X-Amz-SignedHeaders=host&amp;x-id=GetObject">

Is this setup correct? Is there a way to shorten the image path?

Assuming the setup is correct, how can I retrieve all images from the item-a folder?

images
|___ item-a
	|___ item-a-1.jpg
	|___ item-a-2.jpg
|___ item-2.jpg
|___ item-3.jpg
|___ item-4.jpg
|___ item-5.jpg

Thank in advanced!

If you’re using the bucket only to store images for your store, then make the bucket public and use the public url for the bucket, plus the image key (which could include the path ‘/’ if you used that for images). You can also attach a custom domain to your public bucket, and use that as well. See:

If you don’t want public bucket, then have a route/endpoint in your remix application (e.g. images/path-to-image) and process request to that route by fetching the object from Tigris, then return the Blob data as the response. You can include the response headers as well, if you want your app/client to cache in the browser (I’d recommend this, so that every request for image doesn’t hit your app).

3 Likes

Thank you, I’ll give it a try!

I don’t use Remix so I could be wrong. But looking from the React lens, this would expose your secrets on the client. You need to look at the rest of your app to see if you follow this pattern.

As @pmbanugo said, this is inefficient - imagine if you had 10,000’s or 100,000’s of images.

This is the right idea, but you need to put it behind an API, eg:

<img src={'/api/getUrl?key=somekey'} />

Where key is the key to your image in your bucket… or a generic filename and your API would be responsible for doing the key lookup. You can make your images private and implement authorization on it by including a hashed userId as part of the key. If the the userId part of the key is authorized, generate the presignedUrl w/ a 1second expiration and a long cache-control header on it.

2 Likes

From Questions / Help to JavaScript

Added storage, tigris

Thank you for your valuable comment.

I added the secret strings, but I’m unable to retrieve them. The database and Shopify secrets are working fine, so I might be doing something wrong.

Could you provide a link to the documentation?
Thank you.

You will still expose your secrets by having them in your client component, even if they are SSR’d. The hydration would copy the secrets to the client.

As for accessible public keys on the client: Environment Variables | Remix
Notice the “public keys” here. You’re putting private secrets on the client:

secretAccessKey: 'my_secretAccessKey'

Your /api/getUrl endpoint can either read the image and return it as a buffer (this will consume memory on your server) or you can return a redirect w/ the presignedUrl to the client (this will defer the bandwidth to the client)

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.