Skip to content

Pre-Signed URL

The Pre-Signed URL pattern allows clients to upload or download files directly to/from object storage (S3), without transiting through the application server. The server generates a cryptographically signed temporary URL with constraints (MIME type, max size, expiration).

In Granit, this pattern is at the core of Granit.BlobStorage with a Direct-to-Cloud architecture, a post-upload validation pipeline, and a GDPR-compliant crypto-shredding mechanism.

sequenceDiagram
    participant C as Client
    participant API as Granit API
    participant S3 as S3 in Europe
    participant V as Validation Pipeline
    participant DB as BlobDescriptorStore

    Note over C,DB: Phase 1 -- Initiation
    C->>API: InitiateUploadAsync("medical-docs", request)
    API->>DB: Create BlobDescriptor (Status = Pending)
    API->>S3: Generate PUT Pre-Signed URL
    API-->>C: PresignedUploadTicket (URL + expiry)

    Note over C,S3: Phase 2 -- Direct upload
    C->>S3: PUT {presignedUrl} [binary file]
    Note over C,S3: The application server is not involved

    Note over S3,DB: Phase 3 -- Validation
    S3-->>V: Notification (SNS/webhook)
    V->>V: MagicBytesValidator (Order=10)
    V->>V: MaxSizeValidator (Order=20)
    alt Validation passed
        V->>DB: Status = Valid
    else Validation failed
        V->>DB: Status = Rejected
    end

    Note over C,DB: Phase 4 -- Download
    C->>API: CreateDownloadUrlAsync("medical-docs", blobId)
    API->>DB: Check Status = Valid
    API->>S3: Generate GET Pre-Signed URL
    API-->>C: PresignedDownloadUrl (URL + expiry)
    C->>S3: GET {presignedUrl}
ComponentFileRole
IBlobStoragesrc/Granit.BlobStorage/IBlobStorage.csPublic API: InitiateUploadAsync, CreateDownloadUrlAsync, DeleteAsync
DefaultBlobStoragesrc/Granit.BlobStorage/Internal/DefaultBlobStorage.csFacade orchestrating all components
BlobDescriptorsrc/Granit.BlobStorage/BlobDescriptor.csEntity: status, metadata, audit trail
BlobStatussrc/Granit.BlobStorage/BlobStatus.csState machine: Pending > Uploading > Valid/Rejected > Deleted
PresignedUploadTicketsrc/Granit.BlobStorage/PresignedUploadTicket.csDTO: BlobId, URL, HttpMethod, Expiry, RequiredHeaders
PresignedDownloadUrlsrc/Granit.BlobStorage/PresignedDownloadUrl.csDTO: URL + expiry
ComponentFileRole
IBlobKeyStrategysrc/Granit.BlobStorage/IBlobKeyStrategy.csObject key generation and parsing
PrefixBlobKeyStrategysrc/Granit.BlobStorage.S3/Internal/PrefixBlobKeyStrategy.csFormat: {tenantId}/{container}/{yyyy}/{MM}/{blobId}
ValidatorFileOrderRole
MagicBytesValidatorsrc/Granit.BlobStorage/Validators/MagicBytesValidator.cs10Verifies actual MIME type (magic bytes)
MaxSizeValidatorsrc/Granit.BlobStorage/Validators/MaxSizeValidator.cs20Verifies declared vs actual size

DefaultBlobStorage.DeleteAsync():

  1. Physically deletes the S3 object (storageClient.DeleteObjectAsync)
  2. Marks the BlobDescriptor as Deleted (soft delete)
  3. Retains the metadata in DB for the ISO 27001 audit trail (3 years)
ProblemSolution
Large medical files (MRI, scans) saturate the serverDirect upload client to S3, server never sees the binary
Client-side MIME type validation is unreliableServer-side post-upload validation via magic bytes
GDPR right to erasure + ISO 27001 audit trail (contradictory)Crypto-shredding: binary destroyed, metadata retained in soft delete
Multi-tenant isolation in the S3 bucketKey prefixed with tenantId + TryExtractTenantId() check
Security: the client must not have S3 credentialsPre-signed URL with short expiration, MIME type constraints
// Upload a medical document
public sealed class UploadMedicalDocumentHandler
{
public static async Task<PresignedUploadTicket> Handle(
UploadDocumentCommand command,
IBlobStorage blobStorage,
CancellationToken cancellationToken)
{
PresignedUploadTicket ticket = await blobStorage.InitiateUploadAsync(
containerName: "medical-documents",
new BlobUploadRequest(
FileName: command.FileName,
ContentType: "application/pdf",
MaxAllowedBytes: 50_000_000), // 50 MB
ct);
// The client uses ticket.UploadUrl to upload directly to S3
return ticket;
}
}
// GDPR-compliant deletion (crypto-shredding)
await blobStorage.DeleteAsync(
containerName: "medical-documents",
blobId: documentId,
deletionReason: "GDPR Art. 17 -- patient request",
cancellationToken);
// -> S3 object physically deleted
// -> BlobDescriptor retained in DB (IsDeleted=true, DeletedBy, DeletedAt, DeletionReason)