Table of Contents

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

objectStorage IObjectStorage

The object storage abstraction for S3 operations.

s3Options IOptions<S3Options>

S3 configuration options including bucket names and region.

logger ILogger<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

objectKey string

S3 object key of the resume to delete.

cancellationToken CancellationToken

Cancellation token to cancel the operation.

Returns

Task<bool>

true if the resume was successfully deleted; false if 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

objectKey string

S3 object key (e.g., "profile-photos/{userId}/{uuid}.jpg").

expiresIn TimeSpan?

Time until the URL expires. Defaults to 15 minutes if not specified.

cancellationToken CancellationToken

Cancellation 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

userId Guid

The unique identifier of the user uploading the photo.

fileExtension string

File extension (e.g., "jpg", "jpeg", "png"). Case-insensitive, leading dot is optional.

contentType string

MIME type of the file (e.g., "image/jpeg", "image/png"). Must match the file extension.

cancellationToken CancellationToken

Cancellation 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

objectKey string

S3 object key of the resume (e.g., "resumes/{applicationId}/{uniqueId}-{filename}.pdf").

expiresIn TimeSpan?

Time until the URL expires. Defaults to 5 minutes if not specified (shorter than profile photos for security).

cancellationToken CancellationToken

Cancellation token to cancel the operation.

Returns

Task<string>

A presigned GET URL for accessing the resume file.

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

objectKey string

The 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

roleId Guid

The unique identifier of the volunteer role (used for logging only).

applicationId Guid

The unique identifier of the volunteer application.

fileStream Stream

The file stream containing the resume data.

fileName string

The original filename of the resume (used for validation and sanitization).

contentType string

MIME type of the file (e.g., "application/pdf", "application/msword").

cancellationToken CancellationToken

Cancellation 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.