import os import uuid import hashlib from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query from sqlalchemy.orm import Session from sqlalchemy import func from typing import List, Optional from ..database import get_db from ..models import MediaAsset from ..schemas import MediaAssetCreate, MediaAssetUpdate, MediaAssetResponse from .auth import get_current_user, org_filter router = APIRouter(prefix="/api/assets", tags=["assets"]) PROJECT_ROOT = Path(__file__).resolve().parents[4] if os.getenv('PROJECT_ROOT'): PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT')) UPLOAD_DIR = PROJECT_ROOT / "content" / "images" ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"} @router.get("", response_model=List[MediaAssetResponse]) def list_assets( file_type: Optional[str] = None, tag: Optional[str] = None, topic_id: Optional[str] = None, search: Optional[str] = None, limit: int = Query(50, le=200), offset: int = Query(0, ge=0), db: Session = Depends(get_db), current_user=Depends(get_current_user) ): query = db.query(MediaAsset) of = org_filter(current_user, MediaAsset) if of is not True: query = query.filter(of) if file_type: query = query.filter(MediaAsset.file_type == file_type) if tag: query = query.filter(MediaAsset.tags.contains([tag])) if topic_id: query = query.filter(MediaAsset.topic_ids.contains([topic_id])) if search: query = query.filter( (MediaAsset.filename.contains(search)) | (MediaAsset.alt_text.contains(search)) ) return query.order_by(MediaAsset.created_at.desc()).offset(offset).limit(limit).all() @router.get("/tags") def list_tags( db: Session = Depends(get_db), current_user=Depends(get_current_user) ): of = org_filter(current_user, MediaAsset) base = db.query(MediaAsset) if of is not True: base = base.filter(of) assets = base.with_entities(MediaAsset.tags).all() all_tags = set() for a in assets: if a[0]: all_tags.update(a[0]) return sorted(all_tags) @router.get("/counts") def get_counts( db: Session = Depends(get_db), current_user=Depends(get_current_user) ): of = org_filter(current_user, MediaAsset) base = db.query(MediaAsset) if of is not True: base = base.filter(of) total = base.count() by_type = {} rows = base.with_entities(MediaAsset.file_type, func.count(MediaAsset.id)).group_by(MediaAsset.file_type).all() for ftype, cnt in rows: by_type[ftype] = cnt return {"total": total, "by_type": by_type} @router.post("/upload", response_model=MediaAssetResponse) async def upload_asset( file: UploadFile = File(...), tags: Optional[str] = Query(None, description="逗号分隔的标签"), alt_text: Optional[str] = Query(None), topic_ids: Optional[str] = Query(None, description="逗号分隔的选题ID"), db: Session = Depends(get_db), current_user=Depends(get_current_user) ): if file.content_type not in ALLOWED_IMAGE_TYPES: raise HTTPException(status_code=400, detail=f"不支持的文件类型: {file.content_type}") UPLOAD_DIR.mkdir(parents=True, exist_ok=True) suffix = Path(file.filename).suffix or "" unique_name = f"{uuid.uuid4().hex[:12]}{suffix}" file_path = UPLOAD_DIR / unique_name content = await file.read() file_size = len(content) with open(file_path, "wb") as f: f.write(content) file_type = file.content_type.split("/")[0] if file_type not in ("image", "video", "application"): if suffix in (".pdf", ".doc", ".docx", ".ppt", ".pptx"): file_type = "document" elif suffix in (".mp4", ".mov", ".avi"): file_type = "video" else: file_type = "image" parsed_tags = [t.strip() for t in tags.split(",")] if tags else [] parsed_topic_ids = [t.strip() for t in topic_ids.split(",")] if topic_ids else [] asset = MediaAsset( filename=file.filename, file_path=str(file_path), file_type=file_type, mime_type=file.content_type, size=file_size, alt_text=alt_text, tags=parsed_tags, topic_ids=parsed_topic_ids, uploaded_by=current_user.username, org_id=current_user.org_id or "default", ) db.add(asset) db.commit() db.refresh(asset) return asset @router.put("/{asset_id}", response_model=MediaAssetResponse) def update_asset( asset_id: int, data: MediaAssetUpdate, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): asset = db.query(MediaAsset).filter(MediaAsset.id == asset_id).first() if not asset: raise HTTPException(status_code=404, detail="素材不存在") if current_user.role != "admin" and asset.org_id != current_user.org_id: raise HTTPException(status_code=404, detail="素材不存在") for k, v in data.model_dump(exclude_unset=True).items(): setattr(asset, k, v) db.commit() db.refresh(asset) return asset @router.delete("/{asset_id}") def delete_asset( asset_id: int, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): asset = db.query(MediaAsset).filter(MediaAsset.id == asset_id).first() if not asset: raise HTTPException(status_code=404, detail="素材不存在") if current_user.role != "admin" and asset.org_id != current_user.org_id: raise HTTPException(status_code=404, detail="素材不存在") if os.path.exists(asset.file_path): try: os.remove(asset.file_path) except OSError: pass db.delete(asset) db.commit() return {"ok": True} @router.post("/{asset_id}/use") def increment_usage( asset_id: int, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): asset = db.query(MediaAsset).filter(MediaAsset.id == asset_id).first() if not asset: raise HTTPException(status_code=404, detail="素材不存在") if current_user.role != "admin" and asset.org_id != current_user.org_id: raise HTTPException(status_code=404, detail="素材不存在") asset.usage_count = (asset.usage_count or 0) + 1 db.commit() return {"ok": True, "usage_count": asset.usage_count}