Class S3Service
- Namespace
- Builvero.Infrastructure.Services
- Assembly
- Builvero.Infrastructure.dll
Service for S3 operations related to profile photo uploads and resumes. Uses IObjectStorage abstraction internally for testability and feature-flagging.
public class S3Service : IS3Service
- Inheritance
-
S3Service
- Implements
- Inherited Members
Constructors
S3Service(IObjectStorage, IOptions<S3Options>, ILogger<S3Service>)
Initializes a new instance of the S3Service class.
public S3Service(IObjectStorage objectStorage, IOptions<S3Options> s3Options, ILogger<S3Service> logger)
Parameters
objectStorageIObjectStorageThe object storage abstraction for S3 operations.
s3OptionsIOptions<S3Options>S3 configuration options including bucket names and region.
loggerILogger<S3Service>Logger for recording S3 operations and errors.
Methods
DeleteResumeAsync(string, CancellationToken)
Deletes a resume file from S3.
public Task<bool> DeleteResumeAsync(string objectKey, CancellationToken cancellationToken = default)
Parameters
objectKeystringS3 object key of the resume to delete.
cancellationTokenCancellationTokenCancellation token to cancel the operation.
Returns
- Task<bool>
trueif the resume was successfully deleted;falseif the object key is null or whitespace.
Remarks
If the object key is null or whitespace, the method returns false without making an S3 API call.
Note: S3 delete operations are idempotent. Deleting a non-existent object does not throw an error.
Exceptions
- Exception
Thrown when S3 API returns an error during deletion.
GeneratePresignedGetUrlAsync(string, TimeSpan?, CancellationToken)
Generates a presigned GET URL for retrieving a file from S3.
public Task<string> GeneratePresignedGetUrlAsync(string objectKey, TimeSpan? expiresIn = null, CancellationToken cancellationToken = default)
Parameters
objectKeystringS3 object key (e.g., "profile-photos/{userId}/{uuid}.jpg").
expiresInTimeSpan?Time until the URL expires. Defaults to 15 minutes if not specified.
cancellationTokenCancellationTokenCancellation token to cancel the operation.
Returns
- Task<string>
A presigned GET URL for accessing the file, or an empty string if the object key is null or whitespace.
Remarks
This method ALWAYS generates a presigned URL, regardless of the UsePublicUrls configuration setting.
This ensures that private S3 buckets are never exposed with plain URLs.
A guardrail is implemented to verify that the returned URL contains AWS signature parameters (X-Amz-*). If the URL does not appear to be presigned, an error is logged and an empty string is returned to prevent plain S3 URL leakage. This is a defensive measure to catch configuration or implementation errors.
The presigned URL includes query parameters for authentication and expiration:
- X-Amz-Algorithm: Signature algorithm (AWS4-HMAC-SHA256)
- X-Amz-Credential: AWS credentials
- X-Amz-Date: Request timestamp
- X-Amz-Expires: Expiration time in seconds
- X-Amz-Signature: Request signature
- X-Amz-SignedHeaders: Headers included in signature
GenerateProfilePhotoUploadUrlAsync(Guid, string, string, CancellationToken)
Generates a presigned URL for uploading a profile photo to S3.
public Task<(string UploadUrl, string ObjectKey)> GenerateProfilePhotoUploadUrlAsync(Guid userId, string fileExtension, string contentType, CancellationToken cancellationToken = default)
Parameters
userIdGuidThe unique identifier of the user uploading the photo.
fileExtensionstringFile extension (e.g., "jpg", "jpeg", "png"). Case-insensitive, leading dot is optional.
contentTypestringMIME type of the file (e.g., "image/jpeg", "image/png"). Must match the file extension.
cancellationTokenCancellationTokenCancellation token to cancel the operation.
Returns
- Task<(string UploadUrl, string ObjectKey)>
A tuple containing the presigned upload URL and the S3 object key where the file will be stored.
Remarks
The generated object key follows the format: profile-photos/{userId}/{uuid}.{extension}
This ensures uniqueness while maintaining organization by user.
Allowed file extensions: jpg, jpeg, png Allowed content types: image/jpeg, image/jpg, image/png The presigned URL expires after 5 minutes.
Exceptions
- ArgumentException
Thrown when file extension or content type is not in the allowed list.
GenerateResumeDownloadUrlAsync(string, TimeSpan?, CancellationToken)
Generates a presigned GET URL for downloading a resume from S3.
public Task<string> GenerateResumeDownloadUrlAsync(string objectKey, TimeSpan? expiresIn = null, CancellationToken cancellationToken = default)
Parameters
objectKeystringS3 object key of the resume (e.g., "resumes/{applicationId}/{uniqueId}-{filename}.pdf").
expiresInTimeSpan?Time until the URL expires. Defaults to 5 minutes if not specified (shorter than profile photos for security).
cancellationTokenCancellationTokenCancellation token to cancel the operation.
Returns
Remarks
This method ALWAYS generates a presigned URL, regardless of the UsePublicUrls configuration setting.
This ensures that private S3 buckets are never exposed with plain URLs.
A guardrail is implemented to verify that the returned URL contains AWS signature parameters (X-Amz-*). If the URL does not appear to be presigned, an error is logged and an exception is thrown to prevent plain S3 URL leakage. This is a defensive measure to catch configuration or implementation errors.
The default expiration is 5 minutes (shorter than profile photos) for additional security, as resumes may contain sensitive personal information.
Exceptions
- ArgumentException
Thrown when the object key is null or empty.
- InvalidOperationException
Thrown when the generated URL does not appear to be presigned (guardrail check fails).
- Exception
Thrown when S3 API returns an error during URL generation.
GetProfilePhotoUrl(string)
[Obsolete] This method is disabled to prevent plain S3 URL leakage.
[Obsolete("Use GeneratePresignedGetUrlAsync instead for private buckets. This method is disabled to prevent plain S3 URL leakage.")]
public string GetProfilePhotoUrl(string objectKey)
Parameters
objectKeystringThe object key (ignored, method always throws).
Returns
- string
Never returns; always throws InvalidOperationException.
Remarks
This method is obsolete and disabled. Use GeneratePresignedGetUrlAsync(string, TimeSpan?, CancellationToken) instead to ensure presigned URLs are used for secure access to private S3 buckets.
Exceptions
- InvalidOperationException
Always thrown to prevent use of this obsolete method.
UploadResumeAsync(Guid, Guid, Stream, string, string, CancellationToken)
Uploads a resume file to S3 for a volunteer application.
public Task<string> UploadResumeAsync(Guid roleId, Guid applicationId, Stream fileStream, string fileName, string contentType, CancellationToken cancellationToken = default)
Parameters
roleIdGuidThe unique identifier of the volunteer role (used for logging only).
applicationIdGuidThe unique identifier of the volunteer application.
fileStreamStreamThe file stream containing the resume data.
fileNamestringThe original filename of the resume (used for validation and sanitization).
contentTypestringMIME type of the file (e.g., "application/pdf", "application/msword").
cancellationTokenCancellationTokenCancellation token to cancel the operation.
Returns
- Task<string>
The S3 object key where the resume was stored (format:
resumes/{applicationId}/{uniqueId}-{safeFileName}).
Remarks
Allowed file extensions: pdf, doc, docx Allowed content types: application/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document
The filename is sanitized to remove invalid characters and limit length to 100 characters.
The object key format is: resumes/{applicationId}/{uniqueId}-{safeFileName}
Exceptions
- ArgumentException
Thrown when:
- File extension is not in the allowed list (pdf, doc, docx)
- Content type is not in the allowed list
- Exception
Thrown when S3 upload fails.