feat(media): Add comprehensive logging for media uploads - v1.4.1

Implements media upload logging per docs/design/v1.4.1/media-logging-design.md

Changes:
- Add logging to save_media() in starpunk/media.py:
  * INFO: Successful uploads with file details
  * WARNING: Validation/optimization/variant failures
  * ERROR: Unexpected system errors
- Remove duplicate logging in Micropub media endpoint
- Add 5 comprehensive logging tests in TestMediaLogging class
- Bump version to 1.4.1
- Update CHANGELOG.md

All media upload operations now logged for debugging and observability.
Validation errors, optimization failures, and variant generation issues
are tracked at appropriate log levels. Original functionality unchanged.

Test results: 28/28 media tests pass, 5 new logging tests pass

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-16 17:22:22 -07:00
parent fd92a1d1eb
commit 07f351fef7
9 changed files with 1114 additions and 67 deletions

View File

@@ -391,81 +391,123 @@ def save_media(file_data: bytes, filename: str) -> Dict:
"""
from starpunk.database import get_db
# Validate image (returns 3-tuple, signature unchanged)
mime_type, orig_width, orig_height = validate_image(file_data, filename)
# Compute file size for optimization strategy
# Capture file size for logging
file_size = len(file_data)
# Optimize image with size-aware strategy (now returns 4-tuple with bytes)
optimized_img, width, height, optimized_bytes = optimize_image(file_data, file_size)
try:
# Validate image (returns 3-tuple, signature unchanged)
try:
mime_type, orig_width, orig_height = validate_image(file_data, filename)
except ValueError as e:
current_app.logger.warning(
f'Media upload validation failed: filename="{filename}", '
f'size={file_size}b, error="{e}"'
)
raise
# Generate UUID-based filename (per Q5)
file_ext = Path(filename).suffix.lower()
if not file_ext:
# Determine extension from MIME type
for mime, exts in ALLOWED_MIME_TYPES.items():
if mime == mime_type:
file_ext = exts[0]
break
# Optimize image with size-aware strategy (now returns 4-tuple with bytes)
try:
optimized_img, width, height, optimized_bytes = optimize_image(file_data, file_size)
except ValueError as e:
current_app.logger.warning(
f'Media upload optimization failed: filename="{filename}", '
f'size={file_size}b, error="{e}"'
)
raise
stored_filename = f"{uuid.uuid4()}{file_ext}"
# Generate UUID-based filename (per Q5)
file_ext = Path(filename).suffix.lower()
if not file_ext:
# Determine extension from MIME type
for mime, exts in ALLOWED_MIME_TYPES.items():
if mime == mime_type:
file_ext = exts[0]
break
# Create date-based path (per Q2)
now = datetime.now()
year = now.strftime('%Y')
month = now.strftime('%m')
relative_path = f"{year}/{month}/{stored_filename}"
stored_filename = f"{uuid.uuid4()}{file_ext}"
# Get media directory from app config
media_dir = Path(current_app.config.get('DATA_PATH', 'data')) / 'media'
full_dir = media_dir / year / month
full_dir.mkdir(parents=True, exist_ok=True)
# Create date-based path (per Q2)
now = datetime.now()
year = now.strftime('%Y')
month = now.strftime('%m')
relative_path = f"{year}/{month}/{stored_filename}"
# Save optimized image (using bytes from optimize_image to avoid re-encoding)
full_path = full_dir / stored_filename
full_path.write_bytes(optimized_bytes)
# Get media directory from app config
media_dir = Path(current_app.config.get('DATA_PATH', 'data')) / 'media'
full_dir = media_dir / year / month
full_dir.mkdir(parents=True, exist_ok=True)
# Get actual file size (from optimized bytes)
actual_size = len(optimized_bytes)
# Save optimized image (using bytes from optimize_image to avoid re-encoding)
full_path = full_dir / stored_filename
full_path.write_bytes(optimized_bytes)
# Insert into database
db = get_db(current_app)
cursor = db.execute(
"""
INSERT INTO media (filename, stored_filename, path, mime_type, size, width, height)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(filename, stored_filename, relative_path, mime_type, actual_size, width, height)
)
db.commit()
media_id = cursor.lastrowid
# Get actual file size (from optimized bytes)
actual_size = len(optimized_bytes)
# Generate variants (synchronous) - v1.4.0 Phase 2
# Pass year, month, and optimized_bytes to avoid fragile path traversal and file I/O
base_filename = stored_filename.rsplit('.', 1)[0]
variants = generate_all_variants(
optimized_img,
full_dir,
base_filename,
file_ext,
media_id,
year,
month,
optimized_bytes
)
# Insert into database
db = get_db(current_app)
cursor = db.execute(
"""
INSERT INTO media (filename, stored_filename, path, mime_type, size, width, height)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(filename, stored_filename, relative_path, mime_type, actual_size, width, height)
)
db.commit()
media_id = cursor.lastrowid
return {
'id': media_id,
'filename': filename,
'stored_filename': stored_filename,
'path': relative_path,
'mime_type': mime_type,
'size': actual_size,
'width': width,
'height': height,
'variants': variants
}
# Generate variants (synchronous) - v1.4.0 Phase 2
# Pass year, month, and optimized_bytes to avoid fragile path traversal and file I/O
base_filename = stored_filename.rsplit('.', 1)[0]
variants = []
try:
variants = generate_all_variants(
optimized_img,
full_dir,
base_filename,
file_ext,
media_id,
year,
month,
optimized_bytes
)
except Exception as e:
current_app.logger.warning(
f'Media upload variant generation failed: filename="{filename}", '
f'media_id={media_id}, error="{e}"'
)
# Continue - original image is still usable
# Log success
was_optimized = len(optimized_bytes) < file_size
current_app.logger.info(
f'Media upload successful: filename="{filename}", '
f'stored="{stored_filename}", size={len(optimized_bytes)}b, '
f'optimized={was_optimized}, variants={len(variants)}'
)
return {
'id': media_id,
'filename': filename,
'stored_filename': stored_filename,
'path': relative_path,
'mime_type': mime_type,
'size': actual_size,
'width': width,
'height': height,
'variants': variants
}
except ValueError:
# Already logged at WARNING level in validation/optimization blocks
raise
except Exception as e:
current_app.logger.error(
f'Media upload failed unexpectedly: filename="{filename}", '
f'error_type="{type(e).__name__}", error="{e}"'
)
raise
def attach_media_to_note(note_id: int, media_ids: List[int], captions: List[str]) -> None: