From 690aed2ea625354026bbeab58c61cbc214fe6fd9 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 18 Sep 2026 20:04:39 +0200 Subject: [PATCH] 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 --- .../05-04-SUMMARY.md | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 .planning/phases/05-data-layer-full-fidelity/05-04-SUMMARY.md diff --git a/.planning/phases/05-data-layer-full-fidelity/05-04-SUMMARY.md b/.planning/phases/05-data-layer-full-fidelity/05-04-SUMMARY.md new file mode 100644 index 0000000..47b095e --- /dev/null +++ b/.planning/phases/05-data-layer-full-fidelity/05-04-SUMMARY.md @@ -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__ 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__`) only after commit +- `StaticHandler` serves exact `prefix//` 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*