fix(playlists): cover upload — decode(400) vs persist(500), unique temp, existence re-check - #842
Conversation
…emp, existence re-check Two pre-existing cover-upload issues CodeRabbit flagged on #841 (verbatim move, so correctly not fixed there): 1. One `except Exception as e` wrapped BOTH the PIL decode and the img.save/ tmp.replace, returned 400 for both, and echoed `e` — so a disk/permission failure was mislabeled a client error and could leak a filesystem path. Now: decode/validation -> 400 "Invalid image" (generic); save/replace failure -> logged 500 "could not save cover" (no detail). 2. A shared `{pid}.png.tmp` let two concurrent uploads clobber each other's temp file. Now a unique `tempfile.mkstemp` in the cover dir, atomic replace to publish. Plus an existence re-check just before publishing so an upload that raced a playlist delete can't leave an orphan cover. mkstemp itself is INSIDE the try (Codex catch): an unwritable dir / full disk raises there and is the same persistence failure as save/replace, so it hits the logged generic-500 path instead of escaping as an unhandled 500. Cleanup guards `tmp is not None` for the mkstemp-failed case. Did NOT add a full per-playlist critical section (CodeRabbit's "heavy lift"): FeedBack is single-user (Principle I), so a cover upload racing a delete on the same id can't happen — documented in the code rather than building a lock framework for a precluded race. tests/test_playlist_cover_errors.py pins all of it; negative-checked three ways: the old single-except-400 shape fails the 500 + no-leak-400 tests, and moving mkstemp back outside the try fails the temp-creation-500 test. Fix passes 5/5. Full suite 2405 passed; boot smoke: valid cover 200, bad image -> generic "Invalid image" 400, no .tmp litter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe playlist cover endpoint now returns generic validation and persistence errors, uses unique temporary files with atomic replacement, rechecks playlist existence, logs save failures, and removes temporary artifacts. New tests cover success, invalid images, save failures, temp-file failures, cleanup, and missing playlists. ChangesPlaylist cover upload
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant CoverEndpoint
participant PILImage
participant PlaylistDatabase
participant FileSystem
Client->>CoverEndpoint: Upload cover payload
CoverEndpoint->>PILImage: Decode and thumbnail image
CoverEndpoint->>PlaylistDatabase: Recheck playlist existence
CoverEndpoint->>FileSystem: Create temporary file and write PNG
CoverEndpoint->>FileSystem: Atomically replace playlist cover
CoverEndpoint-->>Client: Return success or generic error
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/routers/playlists.py (1)
212-245: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSolid decode/persist split. Temp-file creation is correctly inside the
try, the fd is closed beforereplace, existence is re-checked before publishing, and failures are logged and cleaned up. No functional concerns.Optional: Ruff flags the blind
except Exceptionat Line 218 (and the same pattern at Line 241). Both are intentional boundary catches, so consider silencing them with# noqa: BLE001to match the convention already used elsewhere in the repo and keep lint output clean.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/routers/playlists.py` around lines 212 - 245, Silence Ruff’s intentional broad-exception warnings in the image decode and persistence handlers by adding `# noqa: BLE001` to both `except Exception` clauses surrounding `Image.open`/conversion and the temporary-file save/replace flow. Keep the existing error responses, cleanup, and logging unchanged.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@lib/routers/playlists.py`:
- Around line 212-245: Silence Ruff’s intentional broad-exception warnings in
the image decode and persistence handlers by adding `# noqa: BLE001` to both
`except Exception` clauses surrounding `Image.open`/conversion and the
temporary-file save/replace flow. Keep the existing error responses, cleanup,
and logging unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d6a4c8d4-da73-4314-a794-fa08504974ca
📒 Files selected for processing (2)
lib/routers/playlists.pytests/test_playlist_cover_errors.py
Follow-up to #841, addressing the two cover-upload findings CodeRabbit raised there. Both are pre-existing (byte-identical to
origin/main), so they correctly weren't fixed in the verbatim move.except Exception as ewrapped both the PIL decode andimg.save/tmp.replace, returned400for both, and echoede— a disk/permission failure was mislabeled a client error and could leak a filesystem path. Now: decode/validation →400 "Invalid image"(generic); save/replace failure → logged500 "could not save cover"(no detail).{pid}.png.tmplet two concurrent uploads clobber each other's temp file → now a uniquetempfile.mkstempin the cover dir, atomic replace to publish, plus an existence re-check just before publishing so an upload racing a playlist delete can't leave an orphan cover.I did not add a full per-playlist critical section (CodeRabbit's "heavy lift"): FeedBack is single-user (Principle I), so a cover upload racing a delete on the same id can't happen. That rationale is documented in the code rather than building a locking framework for a race the deployment model precludes.
Codex caught a gap in my first cut:
tempfile.mkstempsat outside thetry, so an unwritable dir / full disk escaped the clean 500 path. Moved inside; cleanup guardstmp is not None.Test
tests/test_playlist_cover_errors.py, negative-checked three ways:except-400 shape → fails the500test and the no-leak400testmkstempback outside thetry→ fails the temp-creation-500testFix passes 5/5. Full suite 2405 passed; boot smoke: valid cover
200, bad image → genericInvalid image400, no.tmplitter.Closes the two review threads on #841.
🤖 Generated with Claude Code
Summary by CodeRabbit