docs(05-04): complete attachments system_files blob and thumb plan

Tasks completed: 3/3
- Task 1: File, Thumb, bucket, system_files migration
- Task 2: delete lifecycle and static handler
- Task 3: Album/Collection MorphName

SUMMARY: .planning/phases/05-data-layer-full-fidelity/05-04-SUMMARY.md
This commit is contained in:
Jakub Zych
2026-09-18 20:04:39 +02:00
parent d8ecb41981
commit 690aed2ea6

View File

@@ -0,0 +1,172 @@
---
phase: 05-data-layer-full-fidelity
plan: 04
subsystem: database
tags: [attach, system_files, gocloud, blob, imaging, thumbnails, morph]
requires:
- phase: 05-data-layer-full-fidelity
provides: Winter-directory registries, Album/Collection models, lagoon.Migrate, HasAfterDelete
provides:
- framework-owned system_files table migrated before every plugin set
- lagoon/attach File model, gocloud.dev/blob bucket, Winter-exact Thumb() naming with lazy imaging
- DeleteForOwner two-phase contract (rows in-tx, blobs after commit)
- StaticHandler for public attachments (tested, not route-wired)
- Album/Collection MorphName PHP class strings
affects: [05-05-remaining-models, 05-06-tests, 06-http, 12-api-parity]
tech-stack:
added:
- gocloud.dev v0.46.0
- github.com/disintegration/imaging v1.6.2
patterns:
- lagoon.Migrate runs attach.Migrations before iterating plugins
- Thumb exists-check then resize; memblob in tests, fileblob in config
- DeleteForOwner collects blob keys in-tx; DeleteKeys runs after commit
key-files:
created:
- lagoon/attach/file.go
- lagoon/attach/file_test.go
- lagoon/attach/migrations.go
- lagoon/attach/bucket.go
- lagoon/attach/bucket_test.go
- lagoon/attach/thumb.go
- lagoon/attach/thumb_test.go
- lagoon/attach/static.go
- lagoon/attach/static_test.go
- lagoon/attach/lifecycle_test.go
- config/storage.yaml
modified:
- lagoon/migrations.go
- lagoon/migrations_test.go
- go.mod
- go.sum
- plugins/golem15/fonoteka/models/album.go
- plugins/golem15/fonoteka/models/collection.go
key-decisions:
- "Blob keys are partition+disk_name (no public/protected prefix); fileblob roots at storage/app/uploads"
- "DeleteForOwner afterCommit is an in-tx key collector; DeleteKeys removes originals and thumb_<id>_ prefixes after commit"
- "Album/Collection MorphName returns the PHP class string and does not import attach (models stay a leaf)"
patterns-established:
- "system_files is framework-owned; lagoon.Migrate always runs it first, even with an empty plugin list"
- "StaticHandler rebuilds keys from disk_name via PartitionDirectory; unvalidated path segments never reach NewReader"
requirements-completed: [DATA-08, DATA-09]
duration: 18min
completed: 2026-09-18
---
# Phase 5 Plan 04: Attachments Summary
**Framework-owned `system_files` with gocloud.dev/blob, Winter-exact `Thumb()` naming and lazy `disintegration/imaging` resize, post-commit force-delete blob removal, and Album/Collection morph names**
## Performance
- **Duration:** 18 min
- **Started:** 2026-09-18T17:46:04Z
- **Completed:** 2026-09-18T18:03:39Z
- **Tasks:** 3
- **Files modified:** 18
## Accomplishments
- `lagoon.Migrate(gdb, nil)` creates `system_files` (Winter column set plus metadata and a composite attachment lookup index) before any plugin set
- `ThumbFilename` / `PartitionDirectory` match Winter Storm (`thumb_42_200_200_0_0_crop.jpg`, `abc/123/xyz/`); `File.Thumb` resizes once via `disintegration/imaging` and reuses the blob on the second call
- `OpenBucket` fails loud on empty `storage.uploads.bucket_url`; `fileblob`/`memblob` via `gocloud.dev/blob`; `fonoteka.go/config/storage.yaml` mirrors `cms.php` uploads
- Soft-delete of an owner keeps `system_files` rows and blobs; force-delete removes rows inside the transaction and blobs (original + `thumb_<id>_`) only after commit
- `StaticHandler` serves exact `prefix/<partition>/<disk_name>` keys with `Content-Type` and 404s traversal/mismatch; not mounted on any route
- Album and Collection implement `attach.Owner` with `Golem15\Fonoteka\Models\Album` and `...\Collection`
## Task Commits
Each task was committed atomically:
1. **Task 1 RED: attach tests** - `2d84ae4` (test) in summercms.go
2. **Task 1 GREEN: File, Thumb, bucket, system_files migration** - `ba9c992` (feat) in summercms.go
3. **Task 1: storage.uploads config** - `81ca63b` (feat) in fonoteka.go
4. **Task 2: delete lifecycle and static handler** - `d8ecb41` (feat) in summercms.go
5. **Task 3: Album/Collection MorphName** - `a2c3816` (feat) in fonoteka.go
**Plan metadata:** pending (this file)
## Files Created/Modified
- `lagoon/attach/file.go`, `file_test.go` — `File` (`system_files`), `Owner`, `DeleteForOwner`/`DeleteKeys`
- `lagoon/attach/migrations.go` — `202609180001_create_system_files`
- `lagoon/attach/bucket.go`, `bucket_test.go` — `OpenBucket`/`Publish`/`PublicPathPrefix`
- `lagoon/attach/thumb.go`, `thumb_test.go` — Winter naming + lazy imaging
- `lagoon/attach/static.go`, `static_test.go` — public file handler (httptest only)
- `lagoon/attach/lifecycle_test.go` — soft-delete keep / force-delete two-phase
- `lagoon/migrations.go`, `migrations_test.go` — attach set runs first; `TestMigrateRunsSystemFilesFirst`
- `go.mod`, `go.sum` — `gocloud.dev v0.46.0`, `disintegration/imaging v1.6.2`
- `fonoteka.go/config/storage.yaml` — `uploads.bucket_url` / `public_path_prefix`
- `fonoteka.go/plugins/golem15/fonoteka/models/{album,collection}.go` — `MorphName()`
- `fonoteka.go/plugins/golem15/fonoteka/go.mod`, `go.sum` — transitives from `lagoon` importing `attach`
## Decisions Made
- Blob keys are `PartitionDirectory(disk_name)+disk_name` with no `public/`/`protected/` prefix; the fileblob root is `storage/app/uploads` (cutover can point the URL at `.../uploads/public`)
- `DeleteForOwner`'s `afterCommit` callback only records keys while still inside the GORM transaction; `DeleteKeys` is the post-commit blob remover (prefix keys ending in `_` are listed)
- Models satisfy `attach.Owner` by method set only and do not import `lagoon/attach`, keeping `models/` a leaf
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Lifecycle tests cannot live in package attach and import lagoon**
- **Found during:** Task 2 (TestFileLifecycle)
- **Issue:** `lagoon` already imports `lagoon/attach`; an internal-package test importing `lagoon` is a cycle
- **Fix:** `lifecycle_test.go` is `package attach_test` so it can call `lagoon.Migrate`/`lagoon.Use` against a dedicated testcontainers Postgres
- **Files modified:** `lagoon/attach/lifecycle_test.go`
- **Verification:** `go test ./lagoon/attach/... -run TestFileLifecycle`
- **Committed in:** `d8ecb41` (Task 2)
---
**Total deviations:** 1 auto-fixed (1 blocking)
**Impact on plan:** Required for a compiling lifecycle test that proves `Migrate(gdb, nil)` plus the two-phase delete contract. No scope creep.
## Issues Encountered
None beyond the auto-fix above.
## Authentication Gates
None.
## TDD Gate Compliance
- RED: `2d84ae4` `test(05-04): add failing tests for attach File, Thumb, and bucket`
- GREEN: `ba9c992` `feat(05-04): implement attach File, Thumb, blob bucket, system_files migration`
- REFACTOR: not needed
- Task 1 is `tdd="true"`; Tasks 2–3 are standard `type="auto"`
## Known Stubs
- `StaticHandler` is intentionally not registered on any plugin route (D-16 / this phase's HTTP-route boundary; `is_public` gate is Phase 6/12, T-05-16)
- HTTP upload endpoint stays Phase 12 (API-02)
## User Setup Required
None - no external service configuration required. Operators set `storage.uploads.bucket_url` (default `file://./storage/app/uploads` in `config/storage.yaml`).
## Next Phase Readiness
Ready for 05-05 (remaining models). Attachments are usable from Album/Collection via `MorphName()` and `system_files`. Plan 05-06 should smoke-test photos/image query helpers on the classes/ layer.
## Self-Check: PASSED
- Key files exist on disk (`lagoon/attach/{file,migrations,bucket,thumb,static}.go`, `fonoteka.go/config/storage.yaml`, MorphName on Album/Collection)
- Commits `2d84ae4`, `ba9c992`, `d8ecb41` (summercms.go) and `81ca63b`, `a2c3816` (fonoteka.go) exist
- `go vet ./lagoon/...` and `go test ./lagoon/attach/...` green in summercms.go (short and full)
- `go vet ./...` and `go build ./...` green in fonoteka.go
- `go.mod` pins `gocloud.dev v0.46.0` and `github.com/disintegration/imaging v1.6.2`
- `ThumbFilename(42, 200, 200, 0, 0, "crop", "jpg")` is `thumb_42_200_200_0_0_crop.jpg`; `PartitionDirectory("abc123xyz.jpg")` is `abc/123/xyz/`
---
*Phase: 05-data-layer-full-fidelity*
*Completed: 2026-09-18*