Files
njts-accounting-core/backend/app/routers/file_upload.py
2026-01-21 10:58:19 +09:00

174 lines
5.7 KiB
Python

"""
ファイルアップロード管理
"""
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)}")