Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions src/main/java/backendlab/team4you/s3/S3Controller.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import backendlab.team4you.exceptions.FileStorageConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;

// Handles HTTP requests and returns data
@RestController
// All endpoints start with /api/files
@RequestMapping("/api/files")
public class S3Controller {

private static final Logger log = LoggerFactory.getLogger(S3Controller.class);

private final S3Service s3Service;

public S3Controller(S3Service s3Service) {
Expand All @@ -30,28 +33,41 @@ public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile fil
if (key == null || key.isBlank()) {
return ResponseEntity.badRequest().body("Invalid filename");
}
s3Service.uploadFile(key, file.getBytes(), file.getContentType());
return ResponseEntity.ok("File uploaded: " + key);
try {
s3Service.uploadFile(key, file.getBytes(), file.getContentType());
return ResponseEntity.ok("File uploaded: " + key);
} catch (FileStorageConfigurationException e) {
log.error("S3 upload failed for key={}", key, e);
return ResponseEntity.internalServerError().body("Could not upload file: " + key);
}
}

// GET /api/files/download/{key} — download a file
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/download/{key}")
public ResponseEntity<byte[]> downloadFile(@PathVariable String key) throws IOException {
public ResponseEntity<?> downloadFile(@PathVariable String key) throws IOException {
try (InputStream stream = s3Service.downloadFile(key)) {
byte[] bytes = stream.readAllBytes();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + key + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
} catch (FileStorageConfigurationException e) {
log.error("S3 download failed for key={}", key, e);
return ResponseEntity.internalServerError().body("Could not download file: " + key);
}
}

// DELETE /api/files/delete/{key} — delete a file
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/delete/{key}")
public ResponseEntity<String> deleteFile(@PathVariable String key) {
s3Service.deleteFile(key);
return ResponseEntity.ok("File deleted: " + key);
try {
s3Service.deleteFile(key);
return ResponseEntity.ok("File deleted: " + key);
} catch (FileStorageConfigurationException e) {
log.error("S3 delete failed for key={}", key, e);
return ResponseEntity.internalServerError().body("Could not delete file: " + key);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
54 changes: 34 additions & 20 deletions src/main/java/backendlab/team4you/s3/S3Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import software.amazon.awssdk.services.s3.model.S3Exception;
import backendlab.team4you.exceptions.FileStorageConfigurationException;



import java.io.InputStream;
Expand All @@ -29,34 +31,46 @@ public S3Service(S3Client s3Client) {

// Upload a file to S3
public void uploadFile(String key, byte[] data, String contentType) {
s3Client.putObject(
PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(contentType)
.build(),
RequestBody.fromBytes(data)
);
try {
s3Client.putObject(
PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(contentType)
.build(),
RequestBody.fromBytes(data)
);
} catch (S3Exception e) {
throw new FileStorageConfigurationException("Kunde inte ladda upp filen: " + key, e);
}
}

// Download a file from S3
public InputStream downloadFile(String key) {
return s3Client.getObject(
GetObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build()
);
try {
return s3Client.getObject(
GetObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build()
);
} catch (S3Exception e) {
throw new FileStorageConfigurationException("Kunde inte ladda ner filen: " + key, e);
}
}

// Delete a file from S3
public void deleteFile(String key) {
s3Client.deleteObject(
DeleteObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build()
);
try {
s3Client.deleteObject(
DeleteObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build()
);
} catch (S3Exception e) {
throw new FileStorageConfigurationException("Kunde inte radera filen: " + key, e);
}
}

public void uploadFileIfAbsent(String key, byte[] data, String contentType) {
Expand Down
36 changes: 36 additions & 0 deletions src/test/java/backendlab/team4you/s3/S3ServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import software.amazon.awssdk.services.s3.model.*;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.*;


import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
Expand Down Expand Up @@ -54,6 +56,7 @@ void deleteFile_shouldCallDeleteObject() {
// Assert — verify that deleteObject was called once
verify(s3Client, times(1)).deleteObject(any(DeleteObjectRequest.class));
}

@Test
void downloadFile_shouldCallGetObject() {
// Arrange
Expand All @@ -67,4 +70,37 @@ void downloadFile_shouldCallGetObject() {
// Assert — verify that getObject was called once
verify(s3Client, times(1)).getObject(any(GetObjectRequest.class));
}

@Test
void uploadFile_shouldThrowFileStorageConfigurationException_whenS3Fails() {
// Arrange
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
.thenThrow(S3Exception.builder().message("S3 error").build());

// Act & Assert
assertThrows(backendlab.team4you.exceptions.FileStorageConfigurationException.class,
() -> s3Service.uploadFile("test.txt", "Hello".getBytes(), "text/plain"));
}

@Test
void downloadFile_shouldThrowFileStorageConfigurationException_whenS3Fails() {
// Arrange
when(s3Client.getObject(any(GetObjectRequest.class)))
.thenThrow(S3Exception.builder().message("S3 error").build());

// Act & Assert
assertThrows(backendlab.team4you.exceptions.FileStorageConfigurationException.class,
() -> s3Service.downloadFile("test.txt"));
}

@Test
void deleteFile_shouldThrowFileStorageConfigurationException_whenS3Fails() {
// Arrange
when(s3Client.deleteObject(any(DeleteObjectRequest.class)))
.thenThrow(S3Exception.builder().message("S3 error").build());

// Act & Assert
assertThrows(backendlab.team4you.exceptions.FileStorageConfigurationException.class,
() -> s3Service.deleteFile("test.txt"));
}
}