diff --git a/.gitignore b/.gitignore
index d22c792f43..0b6bd24070 100644
--- a/.gitignore
+++ b/.gitignore
@@ -124,4 +124,7 @@ devDb*.db
*.xjb
#neo4j
-persistence-modules/neo4j/data/**
\ No newline at end of file
+persistence-modules/neo4j/data/**
+/deep-shallow-copy/.mvn/wrapper
+/deep-shallow-copy/mvnw
+/deep-shallow-copy/mvnw.cmd
diff --git a/aws-modules/aws-s3-update-object/pom.xml b/aws-modules/aws-s3-update-object/pom.xml
new file mode 100644
index 0000000000..b44cdb8c6a
--- /dev/null
+++ b/aws-modules/aws-s3-update-object/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.2
+
+
+ com.baeldung
+ aws-s3-update-object
+ 0.0.1-SNAPSHOT
+ aws-s3-update-object
+ Project demonstrating overwriting of S3 objects
+
+ 17
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.amazonaws
+ aws-java-sdk
+ 1.12.523
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/aws-modules/aws-s3-update-object/src/main/java/com/baeldung/awss3updateobject/AwsS3UpdateObjectApplication.java b/aws-modules/aws-s3-update-object/src/main/java/com/baeldung/awss3updateobject/AwsS3UpdateObjectApplication.java
new file mode 100644
index 0000000000..24866c287b
--- /dev/null
+++ b/aws-modules/aws-s3-update-object/src/main/java/com/baeldung/awss3updateobject/AwsS3UpdateObjectApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.awss3updateobject;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class AwsS3UpdateObjectApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AwsS3UpdateObjectApplication.class, args);
+ }
+
+}
diff --git a/aws-modules/aws-s3-update-object/src/main/java/com/baeldung/awss3updateobject/controller/FileController.java b/aws-modules/aws-s3-update-object/src/main/java/com/baeldung/awss3updateobject/controller/FileController.java
new file mode 100644
index 0000000000..e87358ef56
--- /dev/null
+++ b/aws-modules/aws-s3-update-object/src/main/java/com/baeldung/awss3updateobject/controller/FileController.java
@@ -0,0 +1,24 @@
+package com.baeldung.awss3updateobject.controller;
+
+import com.baeldung.awss3updateobject.service.FileService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+@RestController
+@RequestMapping("api/v1/file")
+public class FileController {
+
+ @Autowired
+ FileService fileService;
+
+ @PostMapping("/upload")
+ public String uploadFile(@RequestParam("file") MultipartFile multipartFile) throws Exception {
+ return this.fileService.uploadFile(multipartFile);
+ }
+
+ @PostMapping("/update")
+ public String updateFile(@RequestParam("file") MultipartFile multipartFile, @RequestParam("filePath") String exitingFilePath) throws Exception {
+ return this.fileService.updateFile(multipartFile, exitingFilePath);
+ }
+}
diff --git a/aws-modules/aws-s3-update-object/src/main/java/com/baeldung/awss3updateobject/service/FileService.java b/aws-modules/aws-s3-update-object/src/main/java/com/baeldung/awss3updateobject/service/FileService.java
new file mode 100644
index 0000000000..8f3458d060
--- /dev/null
+++ b/aws-modules/aws-s3-update-object/src/main/java/com/baeldung/awss3updateobject/service/FileService.java
@@ -0,0 +1,80 @@
+package com.baeldung.awss3updateobject.service;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import com.amazonaws.services.s3.model.*;
+import jakarta.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+@Service
+public class FileService {
+
+ private static final Logger logger = LoggerFactory.getLogger(FileService.class);
+
+ public AmazonS3 amazonS3;
+
+ @Value("${aws.s3bucket}")
+ public String awsS3Bucket;
+
+ @PostConstruct
+ private void init(){
+ AWSCredentials credentials = new BasicAWSCredentials(
+ "AWS AccessKey",
+ "AWS secretKey"
+ );
+ this.amazonS3 = AmazonS3ClientBuilder.standard()
+ .withRegion(Regions.fromName("us-east-1"))
+ .withCredentials(new AWSStaticCredentialsProvider(credentials))
+ .build();
+ }
+
+ public String uploadFile(MultipartFile multipartFile) throws Exception {
+ String key = "/documents/" + multipartFile.getOriginalFilename();
+ return this.uploadDocument(this.awsS3Bucket, key, multipartFile);
+ }
+
+ public String updateFile(MultipartFile multipartFile, String key) throws Exception {
+ return this.uploadDocument(this.awsS3Bucket, key, multipartFile);
+ }
+
+ private String uploadDocument(String s3bucket, String key, MultipartFile multipartFile) throws Exception {
+ try {
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentType(multipartFile.getContentType());
+ Map attributes = new HashMap<>();
+ attributes.put("document-content-size", String.valueOf(multipartFile.getSize()));
+ metadata.setUserMetadata(attributes);
+ InputStream documentStream = multipartFile.getInputStream();
+ PutObjectResult putObjectResult = this.amazonS3.putObject(new PutObjectRequest(s3bucket, key, documentStream, metadata));
+
+ S3Object s3Object = this.amazonS3.getObject(s3bucket, key);
+ logger.info("Last Modified: " + s3Object.getObjectMetadata().getLastModified());
+ return key;
+ } catch (AmazonS3Exception ex) {
+ if (ex.getErrorCode().equalsIgnoreCase("NoSuchBucket")) {
+ String msg = String.format("No bucket found with name %s", s3bucket);
+ throw new Exception(msg);
+ } else if (ex.getErrorCode().equalsIgnoreCase("AccessDenied")) {
+ String msg = String.format("Access denied to S3 bucket %s", s3bucket);
+ throw new Exception(msg);
+ }
+ throw ex;
+ } catch (IOException ex) {
+ String msg = String.format("Error saving file %s to AWS S3 bucket %s", key, s3bucket);
+ throw new Exception(msg);
+ }
+ }
+}
diff --git a/aws-modules/aws-s3-update-object/src/main/resources/application.properties b/aws-modules/aws-s3-update-object/src/main/resources/application.properties
new file mode 100644
index 0000000000..c840d970a8
--- /dev/null
+++ b/aws-modules/aws-s3-update-object/src/main/resources/application.properties
@@ -0,0 +1 @@
+aws.s3bucket=baeldung-documents;
diff --git a/aws-modules/aws-s3-update-object/src/test/java/com/baeldung/awss3updateobject/controller/FileControllerUnitTest.java b/aws-modules/aws-s3-update-object/src/test/java/com/baeldung/awss3updateobject/controller/FileControllerUnitTest.java
new file mode 100644
index 0000000000..ec2385f62b
--- /dev/null
+++ b/aws-modules/aws-s3-update-object/src/test/java/com/baeldung/awss3updateobject/controller/FileControllerUnitTest.java
@@ -0,0 +1,61 @@
+package com.baeldung.awss3updateobject.controller;
+
+import com.baeldung.awss3updateobject.service.FileService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.multipart.MultipartFile;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+
+public class FileControllerUnitTest {
+
+ private MockMvc mockMvc;
+
+ @Mock
+ private FileService fileService;
+
+ @InjectMocks
+ private FileController fileController;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ this.mockMvc = MockMvcBuilders.standaloneSetup(fileController).build();
+ }
+
+ @Test
+ public void givenValidMultipartFile_whenUploadedViaEndpoint_thenCorrectPathIsReturned() throws Exception {
+ MockMultipartFile multipartFile = new MockMultipartFile("multipartFile", "test.txt",
+ "text/plain", "test data".getBytes());
+
+ when(fileService.uploadFile(any(MultipartFile.class))).thenReturn("/documents/test.txt");
+
+ mockMvc.perform(multipart("/file/upload").file(multipartFile))
+ .andExpect(status().isOk())
+ .andExpect(content().string("/documents/test.txt"));
+ }
+
+ @Test
+ public void givenValidMultipartFileAndExistingPath_whenUpdatedViaEndpoint_thenSamePathIsReturned() throws Exception {
+ MockMultipartFile multipartFile = new MockMultipartFile("multipartFile", "test.txt",
+ "text/plain", "test update data".getBytes());
+ String existingFilePath = "/documents/existingFile.txt";
+
+ when(fileService.updateFile(any(MultipartFile.class), eq(existingFilePath))).thenReturn(existingFilePath);
+
+ mockMvc.perform(multipart("/file/update")
+ .file(multipartFile)
+ .param("exitingFilePath", existingFilePath))
+ .andExpect(status().isOk())
+ .andExpect(content().string(existingFilePath));
+ }
+}
\ No newline at end of file
diff --git a/aws-modules/aws-s3-update-object/src/test/java/com/baeldung/awss3updateobject/service/FileServiceUnitTest.java b/aws-modules/aws-s3-update-object/src/test/java/com/baeldung/awss3updateobject/service/FileServiceUnitTest.java
new file mode 100644
index 0000000000..3ccd41820e
--- /dev/null
+++ b/aws-modules/aws-s3-update-object/src/test/java/com/baeldung/awss3updateobject/service/FileServiceUnitTest.java
@@ -0,0 +1,97 @@
+package com.baeldung.awss3updateobject.service;
+
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.model.AmazonS3Exception;
+import com.amazonaws.services.s3.model.PutObjectRequest;
+import com.amazonaws.services.s3.model.S3Object;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+public class FileServiceUnitTest {
+
+ @Mock
+ private AmazonS3 amazonS3;
+
+ @Mock
+ private MultipartFile multipartFile;
+
+ @InjectMocks
+ private FileService fileService;
+
+ @BeforeEach
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ fileService = new FileService();
+ fileService.awsS3Bucket = "test-bucket";
+ fileService.amazonS3 = amazonS3;
+ }
+
+ @Test
+ public void givenValidFile_whenUploaded_thenKeyMatchesDocumentPath() throws Exception {
+ when(multipartFile.getName()).thenReturn("testFile");
+ when(multipartFile.getContentType()).thenReturn("application/pdf");
+ when(multipartFile.getSize()).thenReturn(1024L);
+ when(multipartFile.getInputStream()).thenReturn(mock(InputStream.class));
+
+ S3Object s3Object = new S3Object();
+ when(amazonS3.putObject(any())).thenReturn(null);
+ when(amazonS3.getObject(anyString(), anyString())).thenReturn(s3Object);
+
+ String key = fileService.uploadFile(multipartFile);
+
+ assertEquals("/documents/testFile", key);
+ }
+
+ @Test
+ public void givenValidFile_whenUploadFailsDueToNoBucket_thenExceptionIsThrown() throws Exception {
+ when(multipartFile.getName()).thenReturn("testFile");
+ when(multipartFile.getContentType()).thenReturn("application/pdf");
+ when(multipartFile.getSize()).thenReturn(1024L);
+ when(multipartFile.getInputStream()).thenReturn(mock(InputStream.class));
+
+ AmazonS3Exception exception = new AmazonS3Exception("Test exception");
+ exception.setErrorCode("NoSuchBucket");
+ when(amazonS3.putObject(any(PutObjectRequest.class))).thenThrow(exception);
+
+ assertThrows(Exception.class, () -> fileService.uploadFile(multipartFile));
+ }
+
+ @Test
+ public void givenExistingFile_whenUpdated_thenSameKeyIsReturned() throws Exception {
+ when(multipartFile.getName()).thenReturn("testFile");
+ when(multipartFile.getContentType()).thenReturn("application/pdf");
+ when(multipartFile.getSize()).thenReturn(1024L);
+ when(multipartFile.getInputStream()).thenReturn(mock(InputStream.class));
+
+ S3Object s3Object = new S3Object();
+ when(amazonS3.putObject(any(PutObjectRequest.class))).thenReturn(null);
+ when(amazonS3.getObject(anyString(), anyString())).thenReturn(s3Object);
+
+ String key = "/documents/existingFile";
+ String resultKey = fileService.updateFile(multipartFile, key);
+
+ assertEquals(key, resultKey);
+ }
+
+ @Test
+ public void givenFileWithIOException_whenUpdated_thenExceptionIsThrown() throws Exception {
+ when(multipartFile.getName()).thenReturn("testFile");
+ when(multipartFile.getContentType()).thenReturn("application/pdf");
+ when(multipartFile.getSize()).thenReturn(1024L);
+ when(multipartFile.getInputStream()).thenThrow(new IOException("Test IO Exception"));
+
+ assertThrows(Exception.class, () -> fileService.updateFile(multipartFile, "/documents/existingFile"));
+ }
+}
\ No newline at end of file