Skip to main content

binary-file-storage

Pattern for storing and serving binary files (images, etc.) across both File and Blob storage backends

Source facts

Repository
bradygaster/squad-places
Last source activity
March 6, 2026 at 18:22
Detected SKILL.md language
English
Stars
8
Forks
4

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
binary-file-storage
description
Pattern for storing and serving binary files (images, etc.) across both File and Blob storage backends
domain
storage, api-design
confidence
high
source
earned — image support feature implementation
## Context When adding binary file support (images, attachments) to Squad Places, both FileStorageService and BlobStorageService must be extended consistently. The pattern applies to any new binary content type. ## Patterns ### Storage Interface Add `Save{Type}Async(Guid ownerId, Guid id, byte[] data, string contentType)` and `Get{Type}Async(Guid ownerId, Guid id)` returning `(byte[] Data, string ContentType)?` to `IBlobStorageService`. Owner-scoped IDs enable natural data isolation. ### FileStorageService - Store binary at `{basePath}/{type}/{ownerId}/{id}{extension}` - Store content type in a `.meta` sidecar file at `{basePath}/{type}/{ownerId}/{id}.meta` - Map content types to extensions in a switch expression - Create the top-level directory in `InitializeAsync()`; owner subdirectories created on-demand via `Directory.CreateDirectory()` ### BlobStorageService - Use a dedicated container (e.g., `images`) - Blob path: `{ownerId}/{id}{extension}` — virtual folder structure - Set `BlobHttpHeaders.ContentType` on upload - For retrieval, search by prefix with `GetBlobsAsync(BlobTraits, BlobStates, prefix, CancellationToken)` - Create container in `InitializeAsync()` ### API Pattern - `POST /api/{type}` — accept base64 body with owner ID, validate owner exists, store, return relative URL - `GET /api/{type}/{ownerId}/{id}` — serve with `Results.File(data, contentType)` - Inline upload on parent entity (e.g., `ImageData` on `PublishArtifactRequest`) as convenience - **Only relative URLs** — reject absolute http/https URLs to prevent SSRF and ensure all content is hosted ### URL Security - Validate URLs with regex matching `/api/{type}/{guid}/{guid}` format - In HTML sanitizer, use `FilterUrl` event to strip non-relative `src` attributes rather than removing the tag entirely - This is more surgical than scheme-based allowlisting — permits relative paths without custom schemes ### Dockerfile Add the new data directory to the `mkdir -p` line. ## Examples See `src/SquadPlaces.Data/FileStorageService.cs` (SaveImageAsync/GetImageAsync) and `src/SquadPlaces.Web/Api/ApiEndpoints.cs` (image endpoints). ## Anti-Patterns - Don't store binary data as base64 in JSON — decode to bytes first - Don't use `GetBlobsAsync(prefix:)` named parameter — the .NET 10 API requires positional args - Don't forget the `.meta` sidecar in file storage — without it, you can't serve with correct Content-Type
View on GitHub