diff --git a/README.md b/README.md
index 17520d6..cb6160c 100644
--- a/README.md
+++ b/README.md
@@ -486,3 +486,27 @@ systemctl restart docker
- `ng build -c production`
- upload to S3
- visit `http://portal.shyshkin.net`
+
+#### 38 Save Profile Images to S3
+
+##### 38.2 Working with S3 (tutorial)
+
+1. Follow Tutorial
+ - [How to Upload Files to Amazon S3 in Spring Boot](https://www.section.io/engineering-education/spring-boot-amazon-s3/)
+2. Create S3 Bucket
+ - `portal-user-profile-images`
+3. Access and secret keys
+ - My Security Credentials
+ - will redirect to `https://console.aws.amazon.com/iam/home?region=eu-north-1#/security_credentials`
+ - Create Access Key
+ - Access key ID: `AKIA...2GBJ`
+ - Secret access key: `LUS...H+yuAW`
+4. Adding Amazon SDK dependency
+ - ``
+ - ` com.amazonaws`
+ - ` aws-java-sdk`
+ - ` 1.12.75`
+ - ``
+5. Create configuration
+6. Create ProfileImageService implementation
+
diff --git a/support-portal-backend/pom.xml b/support-portal-backend/pom.xml
index 761ad32..b79dac7 100644
--- a/support-portal-backend/pom.xml
+++ b/support-portal-backend/pom.xml
@@ -86,6 +86,12 @@
spring-boot-starter-validation
+
+ com.amazonaws
+ aws-java-sdk
+ 1.12.75
+
+
org.springframework.boot
spring-boot-starter-test
@@ -108,7 +114,6 @@
org.apache.httpcomponents
httpclient
4.5.13
- test
diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/config/AmazonConfig.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/config/AmazonConfig.java
new file mode 100644
index 0000000..6452bc7
--- /dev/null
+++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/config/AmazonConfig.java
@@ -0,0 +1,30 @@
+package net.shyshkin.study.fullstack.supportportal.backend.config;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+
+@Configuration
+@Profile("image-s3")
+public class AmazonConfig {
+
+ @Bean
+ public AmazonS3 s3(@Value("${app.amazon-s3.access-key}") String accessKey,
+ @Value("${app.amazon-s3.secret-key}") String secretKey,
+ @Value("${app.amazon-s3.region}") String region) {
+
+ AWSCredentials awsCredentials =
+ new BasicAWSCredentials(accessKey, secretKey);
+ return AmazonS3ClientBuilder
+ .standard()
+ .withRegion(region)
+ .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
+ .build();
+ }
+}
diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/S3ProfileImageService.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/S3ProfileImageService.java
new file mode 100644
index 0000000..b277db4
--- /dev/null
+++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/S3ProfileImageService.java
@@ -0,0 +1,83 @@
+package net.shyshkin.study.fullstack.supportportal.backend.service;
+
+import com.amazonaws.AmazonServiceException;
+import com.amazonaws.SdkClientException;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.model.ObjectListing;
+import com.amazonaws.services.s3.model.ObjectMetadata;
+import com.amazonaws.services.s3.model.S3Object;
+import com.amazonaws.services.s3.model.S3ObjectInputStream;
+import com.amazonaws.util.IOUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.ImageStorageException;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.UUID;
+
+@Slf4j
+@Service
+@Profile("image-s3")
+@RequiredArgsConstructor
+public class S3ProfileImageService implements ProfileImageService {
+
+ private final AmazonS3 amazonS3;
+
+ @Value("${app.amazon-s3.bucket-name}")
+ private String bucketName;
+
+ @Override
+ public byte[] retrieveProfileImage(UUID userId, String filename) {
+
+ String fileKey = createFileKey(userId, filename);
+
+ try {
+ S3Object object = amazonS3.getObject(bucketName, fileKey);
+ S3ObjectInputStream objectContent = object.getObjectContent();
+ return IOUtils.toByteArray(objectContent);
+ } catch (AmazonServiceException | IOException exception) {
+ throw new ImageStorageException("Failed to download the file from Amazon S3", exception);
+ }
+ }
+
+ @Override
+ public String persistProfileImage(UUID userId, MultipartFile profileImage, String filename) {
+
+// String fileName = String.format("%s", profileImage.getOriginalFilename());
+ String fileKey = createFileKey(userId, filename);
+
+ ObjectMetadata objectMetadata = new ObjectMetadata();
+ objectMetadata.addUserMetadata("Content-Type", profileImage.getContentType());
+ objectMetadata.addUserMetadata("Content-Length", String.valueOf(profileImage.getSize()));
+
+ try {
+ amazonS3.putObject(bucketName, fileKey, profileImage.getInputStream(), objectMetadata);
+ } catch (IOException | SdkClientException exception) {
+ throw new ImageStorageException("Failed to persist to Amazon S3", exception);
+ }
+
+ return null;
+ }
+
+ @Override
+ public void clearUserStorage(UUID userId) {
+
+ try {
+ String prefix = userId + "/";
+ ObjectListing objectListing = amazonS3.listObjects(bucketName, prefix);
+
+ objectListing.getObjectSummaries()
+ .forEach(file -> amazonS3.deleteObject(bucketName, file.getKey()));
+ } catch (SdkClientException exception) {
+ throw new ImageStorageException("Failed to delete objects from Amazon S3", exception);
+ }
+ }
+
+ private String createFileKey(UUID userId, String filename) {
+ return String.format("%s/%s", userId, filename);
+ }
+}
diff --git a/support-portal-backend/src/main/resources/application.yml b/support-portal-backend/src/main/resources/application.yml
index d126fc7..f5c3cc1 100644
--- a/support-portal-backend/src/main/resources/application.yml
+++ b/support-portal-backend/src/main/resources/application.yml
@@ -128,4 +128,17 @@ server.ssl:
key-store-password: secret # Keystore password
key-store-type: PKCS12 # Keystore format
+---
+spring:
+ config:
+ activate:
+ on-profile: image-s3
+app:
+ amazon-s3:
+ access-key: ${AMAZON_S3_ACCESS_KEY}
+ secret-key: ${AMAZON_S3_SECRET_KEY}
+ region: eu-north-1
+ bucket-name: portal-user-profile-images
+
+