Initial commit - cleaned repository

This commit is contained in:
jungwoo choi
2025-09-28 20:41:57 +09:00
commit e3c28f796a
188 changed files with 28102 additions and 0 deletions

View File

@ -0,0 +1,197 @@
from fastapi import APIRouter, Query, HTTPException, Body
from fastapi.responses import Response
from typing import Optional, Dict
import mimetypes
from pathlib import Path
import hashlib
from ..core.config import settings
# MinIO 사용 여부에 따라 적절한 캐시 모듈 선택
if settings.use_minio:
from ..core.minio_cache import cache
else:
from ..core.cache import cache
router = APIRouter()
@router.get("/image")
async def get_image(
url: str = Query(..., description="원본 이미지 URL"),
size: Optional[str] = Query(None, description="이미지 크기 (thumb, card, list, detail, hero)")
):
"""
이미지 프록시 엔드포인트
- 외부 URL의 이미지를 가져와서 캐싱
- 선택적으로 리사이징 및 최적화
- WebP 포맷으로 자동 변환 (설정에 따라)
"""
try:
# 캐시 확인
cached_data = await cache.get(url, size)
if cached_data:
# 캐시된 이미지 반환
# SVG 체크
if url.lower().endswith('.svg') or cache._is_svg(cached_data):
content_type = 'image/svg+xml'
# GIF 체크 (GIF는 WebP로 변환하지 않음)
elif url.lower().endswith('.gif'):
content_type = 'image/gif'
# WebP 변환이 활성화된 경우 항상 WebP로 제공 (GIF 제외)
elif settings.convert_to_webp and size:
content_type = 'image/webp'
else:
content_type = mimetypes.guess_type(url)[0] or 'image/jpeg'
return Response(
content=cached_data,
media_type=content_type,
headers={
"Cache-Control": f"public, max-age={86400 * 7}", # 7일 브라우저 캐시
"X-Cache": "HIT",
"X-Image-Format": content_type.split('/')[-1].upper()
}
)
# 캐시 미스 - 이미지 다운로드
image_data = await cache.download_image(url)
# URL에서 MIME 타입 추측
guessed_type = mimetypes.guess_type(url)[0]
# SVG 확장자 체크 (mimetypes가 SVG를 제대로 인식하지 못할 수 있음)
if url.lower().endswith('.svg') or cache._is_svg(image_data):
content_type = 'image/svg+xml'
# GIF 체크
elif url.lower().endswith('.gif') or (guessed_type and 'gif' in guessed_type.lower()):
content_type = 'image/gif'
else:
content_type = guessed_type or 'image/jpeg'
# 리사이징 및 최적화 (SVG와 GIF는 특별 처리)
if size and content_type != 'image/svg+xml':
# GIF는 특별 처리
if content_type == 'image/gif':
image_data, content_type = cache._process_gif(image_data, settings.thumbnail_sizes[size])
else:
image_data, content_type = cache.resize_and_optimize_image(image_data, size)
# 캐시에 저장
await cache.set(url, image_data, size)
# 백그라운드에서 다른 크기들도 생성하도록 트리거
await cache.trigger_background_generation(url)
# 이미지 반환
return Response(
content=image_data,
media_type=content_type,
headers={
"Cache-Control": f"public, max-age={86400 * 7}",
"X-Cache": "MISS",
"X-Image-Format": content_type.split('/')[-1].upper()
}
)
except HTTPException:
raise
except Exception as e:
import traceback
print(f"Error processing image from {url}: {str(e)}")
traceback.print_exc()
# 403 에러를 명확히 처리
if "403" in str(e):
raise HTTPException(
status_code=403,
detail=f"이미지 접근 거부됨: {url}"
)
raise HTTPException(
status_code=500,
detail=f"이미지 처리 실패: {str(e)}"
)
@router.get("/stats")
async def get_stats():
"""캐시 통계 정보"""
cache_size = await cache.get_cache_size()
# 디렉토리 구조 통계 추가 (MinIO 또는 파일시스템)
dir_stats = await cache.get_directory_stats()
return {
"cache_size_gb": round(cache_size, 2),
"max_cache_size_gb": settings.max_cache_size_gb,
"cache_usage_percent": round((cache_size / settings.max_cache_size_gb) * 100, 2),
"directory_stats": dir_stats
}
@router.post("/cleanup")
async def cleanup_cache():
"""오래된 캐시 정리"""
await cache.cleanup_old_cache()
return {"message": "캐시 정리 완료"}
@router.post("/cache/delete")
async def delete_cache(request: Dict = Body(...)):
"""특정 URL의 캐시 삭제"""
url = request.get("url")
if not url:
raise HTTPException(status_code=400, detail="URL이 필요합니다")
try:
# URL의 모든 크기 버전 삭제
sizes = ["thumb", "card", "list", "detail", "hero", None] # None은 원본
deleted_count = 0
for size in sizes:
# 캐시 경로 계산
url_hash = hashlib.md5(url.encode()).hexdigest()
# 3단계 디렉토리 구조
level1 = url_hash[:2]
level2 = url_hash[2:4]
level3 = url_hash[4:6]
# 크기별 파일명
if size:
patterns = [
f"{url_hash}_{size}.webp",
f"{url_hash}_{size}.jpg",
f"{url_hash}_{size}.jpeg",
f"{url_hash}_{size}.png",
f"{url_hash}_{size}.gif"
]
else:
patterns = [
f"{url_hash}",
f"{url_hash}.jpg",
f"{url_hash}.jpeg",
f"{url_hash}.png",
f"{url_hash}.gif",
f"{url_hash}.webp"
]
# 각 패턴에 대해 파일 삭제 시도
for filename in patterns:
cache_path = settings.cache_dir / level1 / level2 / level3 / filename
if cache_path.exists():
cache_path.unlink()
deleted_count += 1
print(f"✅ 캐시 파일 삭제: {cache_path}")
return {
"status": "success",
"message": f"{deleted_count}개의 캐시 파일이 삭제되었습니다",
"url": url
}
except Exception as e:
print(f"❌ 캐시 삭제 오류: {e}")
raise HTTPException(
status_code=500,
detail=f"캐시 삭제 실패: {str(e)}"
)