Hello world!
This is the second article about my experience at Makers building a social network clone. Link to the first article - https://dev.to/olnov/working-with-http-requests-in-spring-boot-5el0. The current one is dedicated to storing images for users' profiles.
Step #1. Planning.
As a part of our MVP, we decided to implement the following user story:
As a user,
I want to update my profile to include my photo,
So that other users can easily find me in the app
We also agreed on non-functional requirements.
- Image files are PNG or JPG only.
- File size should not exceed 1 MB.
Step #2. Analyze and Design.
We came up with different options for where to store images - remote server folder, database and object storage. Even though our application was not highly loaded, we decided to use S3 as the most appropriate solution for distributed systems. (P.S. and I had an existing iDrive e2 subscription from a previous project).
In the Database, there were only links to the public URL of iDrive storage.
Here is the DB diagram:
High-level app diagram:
Step #3. Build.
The backend part needed the following to be completed:
- Add AWS SDK to the project.
- Implement Image Service to work with external integration.
- Implement an Image Controller to put images into a bucket using the Image Service and save the corresponding record in the DB.
In pom.xml I added the following:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.28.18</version>
</dependency>
ImageService Class:
package com.makersacademy.acebook.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import java.io.InputStream;
import java.net.URI;
import java.util.UUID;
@Service
public class ImageService {
private final S3Client s3Client;
@Value("${idrive.e2.bucket-name}")
private String bucketName;
@Value("${idrive.e2.endpoint}")
private String endpoint;
@Value("${idrive.e2.public_endpoint}")
private String public_endpoint;
public ImageService(@Value("${idrive.e2.access-key}") String accessKey,
@Value("${idrive.e2.secret-key}") String secretKey,
@Value("${idrive.e2.endpoint}") String endpoint) {
AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
this.s3Client = S3Client.builder()
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.endpointOverride(URI.create(endpoint))
.region(Region.AWS_GLOBAL) // I dind't see exact instructions from iDrive, so I set this parameter to AWS_GLOBAL
.build();
}
public String uploadImage(InputStream imageStream, long contentLength, String contentType) {
String key = "media/" + UUID.randomUUID();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(contentType)
.contentLength(contentLength) // Specify the content length
.build();
s3Client.putObject(putObjectRequest, software.amazon.awssdk.core.sync.RequestBody.fromInputStream(imageStream, contentLength));
// Return the image URL
return public_endpoint + "/" + bucketName + "/" + key;
}
}
Idrive connection variables are stored in application.properties file:
#S3 storage settings
idrive.e2.access-key=${E2_ACCESS_KEY}
idrive.e2.secret-key=${E2_SECRET_KEY}
idrive.e2.endpoint=${E2_ENDPOINT}
idrive.e2.bucket-name=${E2_BUCKET_NAME}
idrive.e2.public_endpoint=${E2_PUBLIC_ENDPOINT}
Ok, now ImageController Class:
package com.makersacademy.acebook.controller;
import com.makersacademy.acebook.model.Post;
import com.makersacademy.acebook.repository.PostRepository;
import com.makersacademy.acebook.service.ImageService;
import com.makersacademy.acebook.model.User;
import com.makersacademy.acebook.repository.UserRepository;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.view.RedirectView;
import java.io.IOException;
@Controller
public class ImageController {
@Autowired
private ImageService imageService;
@Autowired
private UserRepository userRepository;
@Autowired
private PostRepository postRepository;
@PostMapping("/upload/{userId}")
public RedirectView uploadImage(@PathVariable Long userId, @RequestParam("file") MultipartFile file) {
try {
// Check if the user exists
User user = userRepository.findById(userId).orElseThrow(()->new RuntimeException("User not found"));
// Upload image to IDrive e2 and get image URL
String imageUrl = imageService.uploadImage(file.getInputStream(), file.getSize(), file.getContentType());
// Save the image URL in the user's profile
user.setUser_photo(imageUrl);
userRepository.save(user);
return new RedirectView("/media/"+userId);
} catch (IOException e) {
throw new RuntimeException("Error uploading file:"+e.getMessage());
}
}
@PostMapping("/upload/post")
public RedirectView uploadImageAsPost(@RequestParam("file") MultipartFile file, HttpSession session){
try {
Long userId = (Long) session.getAttribute("user_id");
if (userId == null) {
throw new RuntimeException("Not authorized");
}
String imageUrl = imageService.uploadImage(file.getInputStream(), file.getSize(), file.getContentType());
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
Post post = new Post(imageUrl,user,true);
postRepository.save(post);
return new RedirectView("/");
} catch (IOException e) {
throw new RuntimeException("Error uploading file:"+e.getMessage());
}
}
}
On the front end to upload the profile image I added the following:
<div class="mb-3">
<form id="image-form" th:action="@{/api/images/upload/{id}(id=${user.id})}" method="post"
enctype="multipart/form-data">
<"formFile" class="form-label btn btn-primary">Upload Image</label>
<input class="form-control" type="file" name="file" id="formFile" accept=".jpg,.jpeg,.png"
onchange="autoSubmitImage()" hidden/>
</form>
</div>
<script th:inline="javascript">
/*<![CDATA[*/
const autoSubmitImage = ()=> {
document.getElementById("image-form").submit();
}
/*]]>*/
</script>
And the result.
This is it.
Cheers!
Top comments (0)