修正
This commit is contained in:
173
backend/app/routers/file_upload.py
Normal file
173
backend/app/routers/file_upload.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
ファイルアップロード管理
|
||||
"""
|
||||
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)}")
|
||||
Reference in New Issue
Block a user