Files
summercms/.planning/phases/05-data-layer-full-fidelity/05-04-SUMMARY.md
Jakub Zych 690aed2ea6 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
2026-09-18 20:04:39 +02:00

173 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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*