-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/s3 integration #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
ef2fd3a
feat: add file service infrastructure with S3/MinIO support
eeebbaandersson 4d78d8c
feat: integrate S3/MinIO file uploads into visa application workflow
eeebbaandersson eac7ac1
fix: add missing s3 config values to application-test.properties
eeebbaandersson f6fcdeb
fix: syn test-properties with values in S3 FileService
eeebbaandersson 6a38346
fix: update application-test.properties
eeebbaandersson 4ee302f
fix: update application-test.properties
eeebbaandersson 6eb7f91
fix: more updates to fix pipeline
eeebbaandersson d5d367d
fix: spotless fix
eeebbaandersson 6bf3edf
fix: suggested fixes from CodeRabbit
eeebbaandersson 321214b
fix: more suggested fixes from CodeRabbit
eeebbaandersson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
src/main/java/org/example/visacasemanagementsystem/config/S3Config.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package org.example.visacasemanagementsystem.config; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| 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.S3Configuration; | ||
| import software.amazon.awssdk.services.s3.presigner.S3Presigner; | ||
|
|
||
| import java.net.URI; | ||
|
|
||
| @Configuration | ||
| public class S3Config { | ||
|
|
||
| @Value("${minio.endpoint}") | ||
| private String endpoint; | ||
|
|
||
| @Value("${minio.accessKey}") | ||
| private String accessKey; | ||
|
|
||
| @Value("${minio.secretKey}") | ||
| private String secretKey; | ||
|
|
||
| @Value("${minio.region}") | ||
| private String region; | ||
|
|
||
|
|
||
| @Bean | ||
| public S3Client s3Client () { | ||
| return S3Client.builder() | ||
| .endpointOverride(URI.create(endpoint)) | ||
| .region(Region.of(region)) | ||
| .credentialsProvider(StaticCredentialsProvider.create( | ||
| AwsBasicCredentials.create(accessKey, secretKey))) | ||
| .serviceConfiguration(S3Configuration.builder() | ||
| .pathStyleAccessEnabled(true) | ||
| // .checksumValidationEnabled(false) | ||
| .build()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public S3Presigner s3Presigner () { | ||
| return S3Presigner.builder() | ||
| .endpointOverride(URI.create(endpoint)) | ||
| .region(Region.of(region)) | ||
| .credentialsProvider(StaticCredentialsProvider.create( | ||
| AwsBasicCredentials.create(accessKey, secretKey))) | ||
| .serviceConfiguration(S3Configuration.builder() | ||
| .pathStyleAccessEnabled(true) | ||
| .build()) | ||
| .build(); | ||
|
|
||
| } | ||
| } |
153 changes: 153 additions & 0 deletions
153
src/main/java/org/example/visacasemanagementsystem/file/FileService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| package org.example.visacasemanagementsystem.file; | ||
|
|
||
| import jakarta.annotation.PostConstruct; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
| import software.amazon.awssdk.core.sync.RequestBody; | ||
| import software.amazon.awssdk.services.s3.S3Client; | ||
| import software.amazon.awssdk.services.s3.model.*; | ||
| import software.amazon.awssdk.services.s3.presigner.S3Presigner; | ||
| import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; | ||
|
|
||
| import java.io.IOException; | ||
| import java.time.Duration; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.UUID; | ||
|
|
||
| @Service | ||
| @Slf4j | ||
| public class FileService { | ||
|
|
||
| private final S3Client s3Client; | ||
| private final S3Presigner s3Presigner; | ||
|
|
||
| private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of( | ||
| "application/pdf", | ||
| "image/jpeg", | ||
| "image/png"); | ||
|
|
||
| private static final long MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB | ||
|
|
||
|
|
||
| @Value("${minio.bucketName}") | ||
| private String bucketName; | ||
|
|
||
| @Value("${minio.corsAllowedOrigins:http://localhost:8080}") | ||
| private String corsAllowedOrigins; | ||
|
|
||
| public FileService(S3Client s3Client, S3Presigner s3Presigner) { | ||
| this.s3Client = s3Client; | ||
| this.s3Presigner = s3Presigner; | ||
| } | ||
|
|
||
| @PostConstruct | ||
| public void initializeBucket() { | ||
|
|
||
| if (bucketName == null || bucketName.equals("test-bucket")) { | ||
| log.info("Skipping bucket initialization (test profile or missing config)"); | ||
| return; | ||
| } | ||
| try { | ||
| try { | ||
| s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build()); | ||
| log.info("Bucket '{}' already exists", bucketName); | ||
| } catch (S3Exception e) { | ||
| if (e.statusCode() == 404) { | ||
| s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()); | ||
| log.info("Bucket '{}' created successfully", bucketName); | ||
| } else { throw e; } | ||
| } | ||
| } catch (Exception e) { | ||
| log.error("Failed to verify/create bucket '{}': {}", bucketName, e.getMessage(), e); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
|
|
||
| List<String> origins = Arrays.stream(corsAllowedOrigins.split(",")) | ||
| .map(String::trim) | ||
| .filter(s -> !s.isBlank()) | ||
| .toList(); | ||
|
|
||
| if (origins.isEmpty()) { | ||
| log.warn("minio.corsAllowedOrigins is empty; skipping CORS configuration for bucket '{}'", bucketName); | ||
| return; | ||
| } | ||
|
|
||
| CORSRule corsRule = CORSRule.builder() | ||
| .allowedOrigins(origins) | ||
| .allowedMethods("GET","HEAD") | ||
| .allowedHeaders("*") | ||
| .build(); | ||
|
|
||
| s3Client.putBucketCors(PutBucketCorsRequest.builder() | ||
| .bucket(bucketName) | ||
| .corsConfiguration(CORSConfiguration.builder().corsRules(corsRule).build()) | ||
| .build()); | ||
| log.info("CORS configuration applied to bucket '{}'", bucketName); | ||
| } catch (S3Exception e) { | ||
| log.warn("Failed to configure CORS to bucket '{}': {}", bucketName, e.getMessage(), e); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| public String uploadFile(MultipartFile file) throws IOException { | ||
| // Validate file size | ||
| if (file.getSize() > MAX_FILE_SIZE_BYTES) { | ||
| throw new IOException("File exceeds maximum allowed size of 10 MB"); | ||
| } | ||
|
|
||
| // Validate content type | ||
| String contentType = file.getContentType(); | ||
| if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) { | ||
| throw new IOException("Unsupported document type: " + contentType | ||
| + ". Allowed: PDF, JPEG, PNG"); | ||
| } | ||
|
|
||
| // Sanitize filename | ||
| String original = file.getOriginalFilename(); | ||
| String safeName = (original == null ? "file" | ||
| : original.replaceAll("[^A-Za-z0-9._-]", "_")); | ||
|
|
||
| String fileName = UUID.randomUUID() + "_" + safeName; | ||
|
|
||
| PutObjectRequest putObjectRequest = PutObjectRequest.builder() | ||
| .bucket(bucketName) | ||
| .key(fileName) | ||
| .contentType(contentType) | ||
| .build(); | ||
|
|
||
| s3Client.putObject(putObjectRequest, | ||
| RequestBody.fromInputStream(file.getInputStream(), file.getSize())); | ||
|
|
||
| return fileName; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| public String getPresignedDownloadUrl(String fileName) { | ||
| GetObjectRequest getObjectRequest = GetObjectRequest.builder() | ||
| .bucket(bucketName) | ||
| .key(fileName) | ||
| .responseContentDisposition("attachment; filename=\"" + fileName + "\"") | ||
| .build(); | ||
|
|
||
| GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() | ||
| .signatureDuration(Duration.ofMinutes(10)) | ||
| .getObjectRequest(getObjectRequest) | ||
| .build(); | ||
|
|
||
| return s3Presigner.presignGetObject(presignRequest).url().toString(); | ||
| } | ||
|
|
||
| public void deleteFile(String s3Key) { | ||
| DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() | ||
| .bucket(bucketName) | ||
| .key(s3Key) | ||
| .build(); | ||
| s3Client.deleteObject(deleteObjectRequest); | ||
| } | ||
|
|
||
|
|
||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.