How to display image from AWS s3 using spring boot and react?

How to display images from amazon s3 bucket in react? Images are not being displayed using amazon s3,spring boot 2.5.3,react.

I have tested the end points in postman, it works.

React: Using Axios to connect to backend and MyDropZone

Using axios to make a post request, I can confirm that images are being saved in s3. Saved image

This is the reponse after making a post request, for the first 2 user.

[
   {
      "userId":"565c6cbe-7833-482d-b0d4-2edcd7fc6163",
      "userName":"John",
      "imageUrl":"pexels-photo-3147528.jpeg"
   },
   {
      "userId":"3c776990-38e8-4de4-b7c6-7875c0ebb20f",
      "userName":"Anthony",
      "imageUrl":"pexels-photo-3147528.jpeg"
   },
   {
      "userId":"bcac9cf2-5508-4996-953e-b18afe866581",
      "userName":"Peter",
      "imageUrl":null
   }
]

React:

import './App.css';
import axios from 'axios';
import { useState, useEffect,useCallback } from 'react';
import {useDropzone} from 'react-dropzone'



//
const UserProfiles = () => {

  const [userProfiles,setUserProfiles]=useState([])

  const fetchUserProfiles=() => {
    axios.get('http://localhost:5000/api/v1/users').then((response) => {
      console.log(response.data)
      setUserProfiles(response.data)
    })
  }

  useEffect(() => {
    fetchUserProfiles();
  }, [])

  return userProfiles.map((profile,index) => {
    return (
      <div key={index}>
        <MyDropZone userId={profile.userId}></MyDropZone>
        <h3>{profile.userId}</h3>
        {
          profile.userId ? (<img src={`http://localhost:5000/api/v1/users/${profile.userId}/image/download`} /> ) : <h5>No profile Image Uploaded</h5>
        }
      </div>
    );
  })
}

function MyDropZone({userId}) {
  const onDrop = useCallback(acceptedFiles => {
    // Do something with the files
    console.log(acceptedFiles[0])
    const file=acceptedFiles[0]
    //Form-data
    const formData = new FormData()
    formData.append('file', file)
    
    //Make a post req
    axios.post(`http://localhost:5000/api/v1/users/${userId}/image/upload`, formData, {
      headers: {
        'Content-Type':'multipart/form-data'
      }
    }).then((response) => {
      console.log(response)
      console.log("Uploaded")
    }).catch((error) => {
      console.log(error)
    })
  }, [])
  const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop})

  return (
    <div {...getRootProps()}>
      <input {...getInputProps()} />
      {
        isDragActive ?
          <p>Drop the files here ...</p> :
          <p>Drag 'n' drop some files here, or click to select files</p>
      }
    </div>
  )
}



function App() {
  return (
    <div className="App">
      <UserProfiles ></UserProfiles>
    </div>
  );
}

export default App;

The image is not being loaded in the UI.

{
 profile.userId ? (<img src={`http://localhost:5000/api/v1/users/${profile.userId}/image/download`} /> ) : <h5>No profile Image Uploaded</h5>
}

This is how it looks in the inspect element.

When I go to this http://localhost:5000/api/v1/users/565c6cbe-7833-482d-b0d4-2edcd7fc6163/image/download URL via browser. It has a response with

 ÿØÿàJFIFHHÿâICC_PROFILElcmsmntrRGB XYZ Ü)9acspAPPLöÖÓ-lcms descü^cprtwtpthbkpt|rXYZgXYZ¤bXYZ¸rTRCÌ@gTRCÌ@b

Update Added Backend code.

controller

@GetMapping(path = "{userId}/image/download")
public byte[] downloadUserProfileImage(@PathVariable("userId") UUID userId) {
    return userProfileService.downloadUserProfileImage(userId);
}

Service:

private UserProfile getUserProfileOrThrow(UUID userId) {
    UserProfile userProfile = userProfileRepository.getUserProfiles()
            .stream()
            .filter(profile -> profile.getUserId().equals(userId)).findFirst().orElseThrow(() -> new IllegalStateException("User does not exist" + userId)
            );
    return userProfile;
}
public byte[] downloadUserProfileImage(UUID userId) {
    UserProfile userProfile=getUserProfileOrThrow(userId);
    String path = String.format("%s/%s",
            BucketName.PROFILE_IMAGE.getBucketName(),
            userProfile.getUserId());

    return userProfile.getImageUrl()
            .map(key -> fileStore.download(path, key))
            .orElse(new byte[0]);
}

FileStore:

@Service
public class FileStore {
    private final AmazonS3 s3;

@Autowired
public FileStore(AmazonS3 s3) {
    this.s3 = s3;
}
public void save(String path,
                 String fileName,
                 Optional<Map<String, String>> optionalMetadata,
                 InputStream inputStream) {
    ObjectMetadata metadata = new ObjectMetadata();

    optionalMetadata.ifPresent(map -> {
        if (!map.isEmpty()) {
            map.forEach(metadata::addUserMetadata);
        }
    });

    try {
        s3.putObject(path, fileName, inputStream, metadata);
    } catch (AmazonServiceException e) {
        throw new IllegalStateException("Failed to store file to s3", e);
    }
}

public byte[] download(String path, String key) {
    try {
        S3Object object = s3.getObject(path, key);
        return IOUtils.toByteArray(object.getObjectContent());
    } catch (AmazonServiceException | IOException e) {
        throw new IllegalStateException("Failed to download file to s3", e);
    }
}
}

Amazon s3 config: @Configuration public class AmazonConfig {

@Bean
public AmazonS3 s3(){
    AWSCredentials  awsCredentials=new BasicAWSCredentials("my-credentials","my-secret-key");

    return AmazonS3ClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
            .withRegion(Regions.AP_SOUTH_1)
            .build();
}

}

UserProfile:

public class UserProfile {
    private final UUID userId;
    private final String userName;
    private String imageUrl;

    //This might be null
    public Optional<String> getImageUrl() {
        return Optional.ofNullable(imageUrl);
    }

    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }
//getters & setters
    }

Answer

When I was facing the same issue, I had to return the object.getObjectContent() image in a Base64 format.

Afterwards, when displaying the data in the front-end, you can try and to this:

<img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA
    AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
        9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />

You can try this Base64 decoder to see if your Base64 data is correct or not.

That implies that you make the GET call beforehand, save the result and then display the base64 string in the img src

UPDATE:

Depending on your approach, in order to download the images for each user profile, inside the .map, you can have a function that downloads the picture for each profile.

  const fetchUserProfileImage = async (userProfileId) => {
    return axios.get(`http://localhost:5000/api/v1/users/${profile.userId}/image/download`)

  }

  return userProfiles.map(async (profile,index) => {
    const userProfileImageBase64 = await fetchUserProfileImage(profile.userId)
    return (
      <div key={index}>
        <MyDropZone userId={profile.userId}></MyDropZone>
        <h3>{profile.userId}</h3>
        {
          profile.userId ? (<img src={`data:image/png;base64, ${userProfileImageBase64}`}/> ) : <h5>No profile Image Uploaded</h5>
        }
      </div>
    );
  })

Or if you don’t like to wait inside the .map, you can try to download all of the images before rendering the body and mapping them to the already existing user in the userProfiles state.

Or, the best approach imo is to add another profileImageSrc field to the User class and save it there whenever you upload an image in the backend. Then you don’t have to make extra calls and just consume the data received when fetching the userProfiles