""" ファイルアップロード管理 """ from fastapi import APIRouter, UploadFile, File, HTTPException from pathlib import Path import shutil import uuid from datetime import datetime router = APIRouter(prefix="/files", tags=["File Upload"]) # アップロード先のベースディレクトリ UPLOAD_BASE_DIR = Path("uploads") UPLOAD_BASE_DIR.mkdir(exist_ok=True) # 許可されるファイルタイプ ALLOWED_EXTENSIONS = { 'image': {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}, 'document': {'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt'}, } def get_file_extension(filename: str) -> str: """ファイル拡張子を取得""" return Path(filename).suffix.lower() def is_allowed_file(filename: str, file_type: str = None) -> bool: """許可されたファイルタイプか確認""" ext = get_file_extension(filename) if file_type == 'image': return ext in ALLOWED_EXTENSIONS['image'] elif file_type == 'document': return ext in ALLOWED_EXTENSIONS['document'] else: # すべての許可された拡張子 all_extensions = set() for exts in ALLOWED_EXTENSIONS.values(): all_extensions.update(exts) return ext in all_extensions def generate_unique_filename(original_filename: str, prefix: str = "") -> str: """一意のファイル名を生成""" ext = get_file_extension(original_filename) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") unique_id = str(uuid.uuid4())[:8] if prefix: return f"{prefix}_{timestamp}_{unique_id}{ext}" return f"{timestamp}_{unique_id}{ext}" @router.post("/upload/employee/{employee_code}") async def upload_employee_file( employee_code: str, file: UploadFile = File(...), file_type: str = "image" ): """ 従業員関連ファイルをアップロード Args: employee_code: 従業員コード file: アップロードするファイル file_type: ファイルタイプ (image, document) Returns: アップロードされたファイルのパス """ # ファイルタイプチェック if not is_allowed_file(file.filename, file_type): raise HTTPException( status_code=400, detail=f"許可されていないファイル形式です。{file_type}タイプの許可される拡張子: {ALLOWED_EXTENSIONS.get(file_type, 'なし')}" ) # 保存先ディレクトリ作成 employee_dir = UPLOAD_BASE_DIR / "employees" / employee_code employee_dir.mkdir(parents=True, exist_ok=True) # 一意のファイル名生成 unique_filename = generate_unique_filename(file.filename, employee_code) file_path = employee_dir / unique_filename # ファイル保存 try: with file_path.open("wb") as buffer: shutil.copyfileobj(file.file, buffer) except Exception as e: raise HTTPException(status_code=500, detail=f"ファイル保存に失敗しました: {str(e)}") # 相対パスを返す(データベースに保存する値) relative_path = f"/uploads/employees/{employee_code}/{unique_filename}" return { "success": True, "file_path": relative_path, "filename": unique_filename, "original_filename": file.filename, "size": file_path.stat().st_size } @router.post("/upload/general") async def upload_general_file( file: UploadFile = File(...), category: str = "general" ): """ 一般ファイルをアップロード Args: file: アップロードするファイル category: カテゴリー (general, reports, temp など) Returns: アップロードされたファイルのパス """ # ファイルタイプチェック if not is_allowed_file(file.filename): raise HTTPException( status_code=400, detail=f"許可されていないファイル形式です" ) # 保存先ディレクトリ作成 category_dir = UPLOAD_BASE_DIR / category category_dir.mkdir(parents=True, exist_ok=True) # 一意のファイル名生成 unique_filename = generate_unique_filename(file.filename) file_path = category_dir / unique_filename # ファイル保存 try: with file_path.open("wb") as buffer: shutil.copyfileobj(file.file, buffer) except Exception as e: raise HTTPException(status_code=500, detail=f"ファイル保存に失敗しました: {str(e)}") # 相対パスを返す relative_path = f"/uploads/{category}/{unique_filename}" return { "success": True, "file_path": relative_path, "filename": unique_filename, "original_filename": file.filename, "size": file_path.stat().st_size } @router.delete("/delete") async def delete_file(file_path: str): """ ファイルを削除 Args: file_path: 削除するファイルのパス (/uploads/... 形式) """ # セキュリティ: パストラバーサル攻撃を防ぐ if ".." in file_path or file_path.startswith("/"): file_path = file_path.lstrip("/") full_path = Path(file_path) # uploadsディレクトリ内のファイルのみ削除可能 if not str(full_path).startswith("uploads"): raise HTTPException(status_code=400, detail="無効なファイルパスです") if not full_path.exists(): raise HTTPException(status_code=404, detail="ファイルが見つかりません") try: full_path.unlink() return {"success": True, "message": "ファイルを削除しました"} except Exception as e: raise HTTPException(status_code=500, detail=f"ファイル削除に失敗しました: {str(e)}")