174 lines
5.7 KiB
Python
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)}")
|