Refactor: split internal/imgcache into focused packages #39

Open
opened 2026-03-18 04:34:17 +01:00 by clawbot · 4 comments
Collaborator

Per PR #37 discussion, internal/imgcache/ currently bundles many distinct concerns into a single package. This issue tracks splitting it into focused, independently testable packages.

Already Done

  • imageprocessorinternal/imageprocessor/ — image format conversion and resizing (extracted in PR #37)
  • allowlistinternal/allowlist/ — host allow list (extracted in PR #41, renamed from whitelist per sneak's feedback)
  • magicinternal/magic/ — magic byte detection (extracted in PR #42)
  • fetcherinternal/httpfetcher/ — SSRF-safe fetcher (extracted in PR #43)
  • signatureinternal/signature/ — HMAC signing and verification (extracted in PR #46)

Remaining

  • urlparser (urlparser.go) → internal/urlparser/
    • ParsedURL, ParseImagePath, ParseImageURL, path traversal validation, size/format parsing
    • Uses ImageRequest, Size, ImageFormat, FitMode — would need to import shared types, or define standalone result types the caller maps onto the imgcache vocabulary
    • Move urlparser_test.go with it; keep coverage of the path-traversal rejection cases intact

This is the last extraction in scope for this issue. Once it lands, this issue closes.

Tightly Coupled (keep together, out of scope)

These share significant state and data flow with the service orchestrator. Splitting them further would require significant interface surgery and may not be worth it:

  • cache (cache.go) — cache lookup, variant storage, negative cache, stats. Tightly coupled with service.go and storage.go.
  • storage (storage.go) — content-addressed ContentStorage and MetadataStorage. Created and managed by Cache.
  • service (service.go) — the core orchestrator that wires cache, fetcher, and processor together.
  • types/interfaces (imgcache.go) — ImageRequest, ImageResponse, ImageFormat, FitMode, Size, etc. These are the shared vocabulary used across all subpackages.

Definition of Done

  • internal/urlparser/ exists and contains the URL/path parsing logic and its tests; internal/imgcache/urlparser.go and urlparser_test.go are gone.
  • No import cycle: urlparser does not import imgcache.
  • All call sites updated; no behavior change to parsing or to path-traversal rejection.
  • Constructor naming per sneak's feedback on PR #41: no stuttering (New(), not NewURLParser()).
  • make check green and docker build . passes.

Approach

Each extraction should:

  1. Create the new package under internal/
  2. Move types and functions, defining standalone types where needed to avoid circular imports
  3. Update all import sites
  4. Ensure docker build . passes (fmt, lint, test, build)
  5. One PR per package extraction to keep reviews manageable
Per [PR #37 discussion](https://git.eeqj.de/sneak/pixa/pulls/37#issuecomment-1173), `internal/imgcache/` currently bundles many distinct concerns into a single package. This issue tracks splitting it into focused, independently testable packages. ## Already Done - [x] **imageprocessor** → `internal/imageprocessor/` — image format conversion and resizing (extracted in [PR #37](https://git.eeqj.de/sneak/pixa/pulls/37)) - [x] **allowlist** → `internal/allowlist/` — host allow list (extracted in [PR #41](https://git.eeqj.de/sneak/pixa/pulls/41), renamed from whitelist per sneak's feedback) - [x] **magic** → `internal/magic/` — magic byte detection (extracted in [PR #42](https://git.eeqj.de/sneak/pixa/pulls/42)) - [x] **fetcher** → `internal/httpfetcher/` — SSRF-safe fetcher (extracted in [PR #43](https://git.eeqj.de/sneak/pixa/pulls/43)) - [x] **signature** → `internal/signature/` — HMAC signing and verification (extracted in [PR #46](https://git.eeqj.de/sneak/pixa/pulls/46)) ## Remaining - [ ] **urlparser** (`urlparser.go`) → `internal/urlparser/` - `ParsedURL`, `ParseImagePath`, `ParseImageURL`, path traversal validation, size/format parsing - Uses `ImageRequest`, `Size`, `ImageFormat`, `FitMode` — would need to import shared types, or define standalone result types the caller maps onto the imgcache vocabulary - Move `urlparser_test.go` with it; keep coverage of the path-traversal rejection cases intact This is the last extraction in scope for this issue. Once it lands, this issue closes. ## Tightly Coupled (keep together, out of scope) These share significant state and data flow with the service orchestrator. Splitting them further would require significant interface surgery and may not be worth it: - **cache** (`cache.go`) — cache lookup, variant storage, negative cache, stats. Tightly coupled with `service.go` and `storage.go`. - **storage** (`storage.go`) — content-addressed `ContentStorage` and `MetadataStorage`. Created and managed by `Cache`. - **service** (`service.go`) — the core orchestrator that wires cache, fetcher, and processor together. - **types/interfaces** (`imgcache.go`) — `ImageRequest`, `ImageResponse`, `ImageFormat`, `FitMode`, `Size`, etc. These are the shared vocabulary used across all subpackages. ## Definition of Done - `internal/urlparser/` exists and contains the URL/path parsing logic and its tests; `internal/imgcache/urlparser.go` and `urlparser_test.go` are gone. - No import cycle: `urlparser` does not import `imgcache`. - All call sites updated; no behavior change to parsing or to path-traversal rejection. - Constructor naming per sneak's feedback on PR #41: no stuttering (`New()`, not `NewURLParser()`). - `make check` green and `docker build .` passes. ## Approach Each extraction should: 1. Create the new package under `internal/` 2. Move types and functions, defining standalone types where needed to avoid circular imports 3. Update all import sites 4. Ensure `docker build .` passes (fmt, lint, test, build) 5. One PR per package extraction to keep reviews manageable
clawbot was assigned by sneak 2026-03-25 02:17:50 +01:00
clawbot removed their assignment 2026-07-25 12:10:11 +02:00
Author
Collaborator

Progress update on the internal/imgcache split.

Done and merged into main:

  • imageprocessor -> internal/imageprocessor/ (PR #37)
  • allowlist -> internal/allowlist/ (PR #41)
  • magic -> internal/magic/ (PR #42)
  • httpfetcher -> internal/httpfetcher/ (PR #43, merged 2026-07-25)

This pass:

  • Opened PR #46 (refactor/extract-signature) extracting signature -> internal/signature/, based on current main. It is independent of the httpfetcher move, so no dependency on other in-flight branches.
  • Moved Signer (New, Sign, Verify, GenerateSignedURL), ParseParams, and the error sentinels. To keep the import edge one-way, the package defines a standalone signature.Request; imgcache projects its ImageRequest onto it via a small helper (same approach magic used for ImageFormat).
  • No-stutter renames: NewSigner -> New, ParseSignatureParams -> ParseParams, ErrSignature{Required,Invalid,Expired} -> Err{Required,Invalid,Expired}. HMAC input bytes are unchanged, so existing signatures still verify.
  • docker build . passes (fmt-check, lint, test, build). Labelled needs-review, awaiting merge into protected main.

Remaining "easily separable" extraction:

  • urlparser (urlparser.go) -> internal/urlparser/. Note it is consumed outside imgcache (by internal/handlers/image.go via ParseImagePath/ParsedURL) and couples to ImageRequest, Size, ImageFormat, FitMode, so it needs standalone shared types on the extracted side. This is the natural next PR once #46 lands.

The "tightly coupled" group (cache, storage, service, and the shared types in imgcache.go) stays together as planned.

Progress update on the `internal/imgcache` split. Done and merged into `main`: - `imageprocessor` -> `internal/imageprocessor/` (PR #37) - `allowlist` -> `internal/allowlist/` (PR #41) - `magic` -> `internal/magic/` (PR #42) - `httpfetcher` -> `internal/httpfetcher/` (PR #43, merged 2026-07-25) This pass: - Opened PR #46 (`refactor/extract-signature`) extracting `signature` -> `internal/signature/`, based on current `main`. It is independent of the `httpfetcher` move, so no dependency on other in-flight branches. - Moved `Signer` (`New`, `Sign`, `Verify`, `GenerateSignedURL`), `ParseParams`, and the error sentinels. To keep the import edge one-way, the package defines a standalone `signature.Request`; `imgcache` projects its `ImageRequest` onto it via a small helper (same approach `magic` used for `ImageFormat`). - No-stutter renames: `NewSigner` -> `New`, `ParseSignatureParams` -> `ParseParams`, `ErrSignature{Required,Invalid,Expired}` -> `Err{Required,Invalid,Expired}`. HMAC input bytes are unchanged, so existing signatures still verify. - `docker build .` passes (fmt-check, lint, test, build). Labelled `needs-review`, awaiting merge into protected `main`. Remaining "easily separable" extraction: - `urlparser` (`urlparser.go`) -> `internal/urlparser/`. Note it is consumed outside `imgcache` (by `internal/handlers/image.go` via `ParseImagePath`/`ParsedURL`) and couples to `ImageRequest`, `Size`, `ImageFormat`, `FitMode`, so it needs standalone shared types on the extracted side. This is the natural next PR once #46 lands. The "tightly coupled" group (`cache`, `storage`, `service`, and the shared types in `imgcache.go`) stays together as planned.
Author
Collaborator

Direction for reworking PR #46 (currently labelled needs-rework): the two suggestions the previous review marked "non-blocking" are blocking, per sneak's comment on the PR. Both must land on refactor/extract-signature before re-review:

  1. Golden known-answer test in internal/signature: a test with a fixed signing key and a fully-specified Request, asserting the exact base64url signature string, plus a companion assertion for the exact generated signed URL. This pins the on-the-wire byte format (host:path:query:width:height:format:expiration) so any future change to the signed bytes fails loudly instead of silently invalidating issued URLs.

  2. Finish the allowlist terminology migration inside internal/imgcache: rename WithNoWhitelist() and every other whitelist-named identifier, test function name, and comment in the package to allowlist equivalents, consistent with internal/allowlist. User-facing YAML config keys stay unchanged in this PR; if any whitelist-named config surface remains, note it on the PR for a follow-up.

Process: work on the existing refactor/extract-signature branch, one logical change per commit, make check green before each commit, push when done, then summarize the rework in a comment on PR #46. All further discussion of the rework itself belongs on the PR.

Direction for reworking PR #46 (currently labelled `needs-rework`): the two suggestions the previous review marked "non-blocking" are blocking, per sneak's comment on the PR. Both must land on `refactor/extract-signature` before re-review: 1. **Golden known-answer test** in `internal/signature`: a test with a fixed signing key and a fully-specified `Request`, asserting the exact base64url signature string, plus a companion assertion for the exact generated signed URL. This pins the on-the-wire byte format (`host:path:query:width:height:format:expiration`) so any future change to the signed bytes fails loudly instead of silently invalidating issued URLs. 2. **Finish the allowlist terminology migration inside `internal/imgcache`**: rename `WithNoWhitelist()` and every other whitelist-named identifier, test function name, and comment in the package to allowlist equivalents, consistent with `internal/allowlist`. User-facing YAML config keys stay unchanged in this PR; if any whitelist-named config surface remains, note it on the PR for a follow-up. Process: work on the existing `refactor/extract-signature` branch, one logical change per commit, `make check` green before each commit, push when done, then summarize the rework in a comment on PR #46. All further discussion of the rework itself belongs on the PR.
Author
Collaborator

Additional rework direction for PR #46, per sneak's comment there (2026-08-07): the whitelist→allowlist rename must cover the user-facing config surface in the same pass — pre-1.0, there is no installed base and no deprecation window. Concretely, on refactor/extract-signature:

  • YAML key whitelist_hostsallowlist_hosts (no back-compat alias for the old key).
  • Config.WhitelistHostsConfig.AllowlistHosts in internal/config/config.go, plus every reference (including any existing test call sites — this mechanical rename is explicitly authorized by the one-pass mandate; test assertions themselves must not be weakened).
  • config.example.yml and README.md updated to the new key. Note README.md currently documents the key as source_host_whitelist, which does not match the actual whitelist_hosts key — fix the README to the real, renamed key while there.
  • Whole-repo grep for remaining whitelist/Whitelist afterward; the only acceptable survivors are historical entries in TODO.md/commit history.
  • Same process as before: one logical commit, make check green (script/cibuild for the authoritative lint), push, then summarize on PR #46.
Additional rework direction for PR #46, per sneak's comment there (2026-08-07): the whitelist→allowlist rename must cover the user-facing config surface in the same pass — pre-1.0, there is no installed base and no deprecation window. Concretely, on `refactor/extract-signature`: - YAML key `whitelist_hosts` → `allowlist_hosts` (no back-compat alias for the old key). - `Config.WhitelistHosts` → `Config.AllowlistHosts` in `internal/config/config.go`, plus every reference (including any existing test call sites — this mechanical rename is explicitly authorized by the one-pass mandate; test assertions themselves must not be weakened). - `config.example.yml` and `README.md` updated to the new key. Note `README.md` currently documents the key as `source_host_whitelist`, which does not match the actual `whitelist_hosts` key — fix the README to the real, renamed key while there. - Whole-repo grep for remaining `whitelist`/`Whitelist` afterward; the only acceptable survivors are historical entries in `TODO.md`/commit history. - Same process as before: one logical commit, `make check` green (`script/cibuild` for the authoritative lint), push, then summarize on PR #46.
Author
Collaborator

Status checkpoint (session handoff — a fresh session will pick up from the tracker):

  • PR #46 (signature extraction + full whitelist→allowlist rename including the allowlist_hosts config key) is merged into main at 6573b9d. Done extractions: imageprocessor, allowlist, magic, httpfetcher, signature.
  • Remaining separable piece for this issue: urlparser (urlparser.go) → internal/urlparser/. Note from the earlier analysis: it is consumed outside imgcache (by internal/handlers/image.go via ParseImagePath/ParsedURL) and couples to ImageRequest, Size, ImageFormat, FitMode, so it needs standalone types on the extracted side, mirroring how signature.Request and magic's ImageFormat were handled. That is the natural next PR here; the tightly coupled remainder (cache, storage, service, shared types) stays together, after which this issue can close.
  • Open question for sneak from the #46 review, still unanswered: the sole remaining whitelist string in the repo is the illustrative naming example in CLAUDE.md line 51 (SourceHostWhitelist as a good-naming example). Say the word and it becomes SourceHostAllowlist in a trivial follow-up.
Status checkpoint (session handoff — a fresh session will pick up from the tracker): - PR #46 (`signature` extraction + full whitelist→allowlist rename including the `allowlist_hosts` config key) is **merged** into `main` at `6573b9d`. Done extractions: `imageprocessor`, `allowlist`, `magic`, `httpfetcher`, `signature`. - Remaining separable piece for this issue: **`urlparser`** (`urlparser.go`) → `internal/urlparser/`. Note from the earlier analysis: it is consumed outside `imgcache` (by `internal/handlers/image.go` via `ParseImagePath`/`ParsedURL`) and couples to `ImageRequest`, `Size`, `ImageFormat`, `FitMode`, so it needs standalone types on the extracted side, mirroring how `signature.Request` and `magic`'s `ImageFormat` were handled. That is the natural next PR here; the tightly coupled remainder (`cache`, `storage`, `service`, shared types) stays together, after which this issue can close. - Open question for sneak from the #46 review, still unanswered: the sole remaining `whitelist` string in the repo is the illustrative naming example in `CLAUDE.md` line 51 (`SourceHostWhitelist` as a good-naming example). Say the word and it becomes `SourceHostAllowlist` in a trivial follow-up.
clawbot added this to the 1.0.0 milestone 2026-08-09 03:37:44 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#39