Skip to content

File Storage

[!abstract] Summary Images are compressed client-side and uploaded directly to Cloudflare R2 via presigned URLs. No server-side processing.

Overview

Unicorn Trails uses Cloudflare R2 (S3-compatible object storage) for:

  • Marker photos (multiple per marker, up to configurable limit)
  • Profile pictures (one per user, replaced on upload)

File Categories

Category Scope Max Count
ProfilePicture Per user 1 (replaces existing)
MarkerImage Per marker Configurable (default 5)

Storage Paths

profiles/{userId}/{fileId}{extension}
markers/{markerId}/{fileId}{extension}

Upload Flow

sequenceDiagram
    participant C as Client
    participant A as API
    participant R2 as Cloudflare R2

    C->>C: Validate file (type, size)
    C->>C: Compress image (browser)
    C->>C: Validate output size
    C->>A: Request upload URL
    A->>A: Create File record (Pending)
    A-->>C: Presigned URL + FileId

    C->>R2: PUT (direct upload)
    R2-->>C: 200 OK

    C->>A: Confirm upload
    A->>R2: Verify file exists
    A->>R2: Get file size
    A->>A: Validate size ≤ limit
    A->>A: Delete old file (profile picture only)
    A->>A: Mark File as Completed
    A-->>C: 200 OK

Client-Side Processing

Input Validation

  • File type in allowed MIME types (configurable)
  • File size within pre-compression limit (configurable)

Compression Settings

Uses browser-image-compression — see Compression Settings for the current defaults. Web Worker with fallback to main thread.

Output Validation

  • Compressed size ≤ backend limit (configurable, default 10 MB)
  • Valid image MIME type

File Lifecycle

Status Description
Pending Upload URL generated, awaiting upload
Processing Reserved for future use
Completed File uploaded and confirmed
Failed Reserved for future use

The IsAvailable property returns true when status is Completed AND storage key exists.

Validation Summary

Stage Check Action on Fail
Frontend select MIME type allowed Skip file, show toast
Frontend select Size ≤ pre-compression limit Skip file, show toast
Frontend compress Output ≤ backend limit Show error toast
Backend get URL Image count < max per marker Return error
Backend confirm File exists in R2 Return error
Backend confirm Size ≤ backend limit Delete from R2, return error

Cleanup

The CleanupJob handles orphaned files:

  • Files with Pending status older than upload URL expiration + 5 min buffer
  • Deletes from R2 storage and removes database records

For deleted markers:

  • Marker images are deleted from R2 before marker is permanently removed
  • Only removes marker if all file deletions succeed