Skip to content

feat(api): printer photos - #114

Open
ChristopherHoffman wants to merge 7 commits into
mainfrom
feat/printer-images
Open

ChristopherHoffman wants to merge 7 commits into
mainfrom
feat/printer-images

Conversation

@ChristopherHoffman

@ChristopherHoffman ChristopherHoffman commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Adds printer photos to the API: a PrinterImage entity, upload/delete/reorder/set-default endpoints, an authenticated per-user thumbnail map, and the deletion cleanup that has to come with them.

Pairs with HoffmanEngineering/3d-print-log-ui#176.

What's here

  • PrinterImage entity, indexes and migration. A filtered unique index enforces exactly one default per printer in the database rather than by convention, so two concurrent "first" uploads cannot both win.
  • PrinterImageService — upload, delete, reorder, set-default, ported from the filament image path. Ownership is always pi.Printer.UserId == userId; PrinterImage.CreatedById records who uploaded, which is not the same thing.
  • Endpoints under /api/Printers/{id}/images, plus GET /api/Printers/thumbnails — the whole-user signed map every cross-cutting surface reads. It is authenticated-only, which is what structurally keeps printer photos off public print pages.
  • Printer list thumbnails. The list caches an unsigned blob path and signs per request. A signed URL cached for the entry lifetime would outlive its own signature. The paths live on a separate CachedPrinterSummaryPage rather than behind [JsonIgnore] on the response DTO, because HybridCache round-trips through System.Text.Json — a [JsonIgnore] member does not survive the cache, and keeping the path off the response type makes leaking it structurally impossible.
  • MediaStorageQuotaService becomes the single definition of storage used, adopted by FilamentImageService, FileAttachmentService and SubscriptionService, so the usage a user is shown is the usage they are held to.
  • Image caps unified at 5 free / 20 Pro across prints, filaments and printers, exposed as MaxImages.
  • Printer deletion and account deletion both remove image rows, File rows and blobs. The PrinterImage → File FKs are Restrict, so either would otherwise fail outright once a printer had photos.

The migration would have broken the deploy

The generated migration gave CreatedById/UpdatedById cascading FKs to Users by EF convention. Users → Printers → PrinterImages is already a cascade path, so that second one makes SQL Server reject the table with error 1785. The integration suite never sees it: it runs on SQLite via EnsureCreated() and never executes migrations at all.

Verified against a real SQL Server 2022 — the generated migration fails with 1785, the corrected one applies cleanly:

dotnet ef database update --connection "Server=localhost,1434;Database=PrintLogDb_MigrationCheck;..."

The model keeps Cascade and the migration overrides it to NoAction, matching what AddProjects and AddFilamentImage already had to do — this is the third time this trap has been hit. Account deletion removes those rows explicitly in UserDeletionService, so nothing depended on the cascade.

Testing

  • dotnet test — 1485 passing, 0 failing, 30 skipped (all pre-existing environment skips).
  • New coverage: PrinterImageServiceTests, PrinterImagesControllerTests, PrinterImageHydrationTests, PrinterImageConcurrencyTests, PrinterImageDeletionTests, MediaStorageQuotaTests.
  • Migration applied end-to-end against SQL Server 2022, as above.
  • Exercised by hand through the UI against a local API in E2ETesting: create a printer with two staged photos, star the second, confirm both upload and that the starred one becomes the list thumbnail.

…ounting

PrinterImageService ports the filament upload/delete/reorder/set-default paths, with
ownership on Printer.UserId rather than PrinterImage.CreatedById (which records the
uploader). PrinterService gains the subscription cap and the SAS signing helpers.

MediaStorageQuotaService becomes the single definition of storage used, adopted by
FilamentImageService, FileAttachmentService and SubscriptionService, so the usage a
user is shown is the usage they are held to.
Adds POST/GET/DELETE/reorder/set-as-default under /api/Printers/{id}/images, the
authenticated whole-user map at /api/Printers/thumbnails, and Images on the printer
detail response.

The printer list caches an UNSIGNED blob path and signs per request: a signed URL
cached for the entry lifetime would outlive its signature. The paths live on a
separate CachedPrinterSummaryPage rather than behind [JsonIgnore] on the response
DTO, because HybridCache round-trips values through System.Text.Json - a [JsonIgnore]
member does not survive the cache, and keeping the path off the response type makes
leaking it structurally impossible.

Printer deletion and account deletion both remove image rows, File rows and blobs in
that order; the PrinterImage -> File FKs are Restrict, so either would otherwise fail
outright once a printer had photos.
Users -> Printers -> PrinterImages is already a cascade path, so the Cascade the
model implies on CreatedById/UpdatedById gives SQL Server a second one and it
rejects the table with error 1785. Verified against a real SQL Server 2022: the
generated migration fails with 1785, this one applies cleanly.

The model keeps Cascade and the migration overrides it, matching AddProjects and
AddFilamentImage; account deletion removes these rows explicitly through
UserDeletionService rather than by cascade. Integration tests run on SQLite via
EnsureCreated and never execute the migration, so nothing catches this before deploy.
Integration tests build their schema with EnsureCreated() on SQLite, so a broken
migration passes the whole suite. Notes the two SQL Server rules EF's conventions
violate - multiple cascade paths (1785) and correlated aggregates (8124) - and the
two-minute check against the SQL Server already in docker-compose.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant