fix: Clean up variant files in delete_media() - v1.4.0

Following design approved by architect in docs/design/v1.4.0/

Changes to starpunk/media.py:
- Update delete_media() to fetch and delete variant files from disk
- Query media_variants table before deletion for file paths
- Use best-effort cleanup with try/except for each file
- Log individual file deletion failures as warnings
- Update final log to show total files deleted (original + variants)
- Update module docstring to reflect v1.4.0 capabilities:
  * 50MB max upload, 10MB max output
  * Image variants (thumb, small, medium, large)
  * Tiered resize strategy

This fixes the issue where variant files were left orphaned on disk
when media was deleted. The database CASCADE already deleted variant
records, but the physical files remained.

All tests pass: uv run pytest tests/test_media_upload.py -v (23/23)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-16 11:10:21 -07:00
parent 68d1a1d879
commit f04eab2112

View File

@@ -4,8 +4,10 @@ Media upload and management for StarPunk
Per ADR-057 and ADR-058:
- Social media attachment model (media at top of note)
- Pillow-based image optimization
- 10MB max file size, 4096x4096 max dimensions
- Auto-resize to 2048px for performance
- 50MB max upload, 10MB max output (v1.4.0)
- Image variants: thumb, small, medium, large (v1.4.0)
- Tiered resize strategy based on input size (v1.4.0)
- 4096x4096 max dimensions
- 4 images max per note
"""
@@ -594,9 +596,10 @@ def get_note_media(note_id: int) -> List[Dict]:
def delete_media(media_id: int) -> None:
"""
Delete media file and database record
Delete media file, variants, and database record
Per Q8: Cleanup orphaned files
Per v1.4.0: Also cleanup variant files
Args:
media_id: Media ID to delete
@@ -611,15 +614,38 @@ def delete_media(media_id: int) -> None:
return
media_path = row[0]
media_dir = Path(current_app.config.get('DATA_PATH', 'data')) / 'media'
# Delete database record (cascade will delete note_media entries)
# Get variant paths before deleting (v1.4.0)
variant_rows = db.execute(
"SELECT path FROM media_variants WHERE media_id = ?",
(media_id,)
).fetchall()
# Delete database record (cascade will delete media_variants and note_media entries)
db.execute("DELETE FROM media WHERE id = ?", (media_id,))
db.commit()
# Delete file from disk
media_dir = Path(current_app.config.get('DATA_PATH', 'data')) / 'media'
full_path = media_dir / media_path
# Delete files from disk (best-effort cleanup)
deleted_count = 0
# Delete original file
full_path = media_dir / media_path
try:
if full_path.exists():
full_path.unlink()
current_app.logger.info(f"Deleted media file: {media_path}")
deleted_count += 1
except OSError as e:
current_app.logger.warning(f"Failed to delete media file {media_path}: {e}")
# Delete variant files (v1.4.0)
for variant_row in variant_rows:
variant_path = media_dir / variant_row[0]
try:
if variant_path.exists():
variant_path.unlink()
deleted_count += 1
except OSError as e:
current_app.logger.warning(f"Failed to delete variant file {variant_row[0]}: {e}")
current_app.logger.info(f"Deleted media {media_id}: {deleted_count} file(s) removed from disk")