Files
2026-07-13 13:08:41 +08:00

177 lines
6.3 KiB
Python

import os
from typing import Optional, Union
from fastapi import HTTPException
from utils.get_env import get_temp_directory_env
import uuid
class TempFileService:
def __init__(self):
self.base_dir = get_temp_directory_env() or "/tmp/presenton"
self.cleanup_base_dir()
os.makedirs(self.base_dir, exist_ok=True)
def _base_dir_realpath(self) -> str:
return os.path.realpath(self.base_dir)
def _is_within_base_dir(self, path: str) -> bool:
base_dir = self._base_dir_realpath()
return path == base_dir or path.startswith(f"{base_dir}{os.sep}")
def _assert_within_base_dir(self, path: str, detail: str):
if not self._is_within_base_dir(path):
raise HTTPException(status_code=400, detail=detail)
def sanitize_upload_filename(self, file_name: Optional[str]) -> str:
normalized_name = (file_name or "").replace("\\", "/")
safe_name = os.path.basename(normalized_name).strip()
if not safe_name or safe_name in {".", ".."}:
raise HTTPException(status_code=400, detail="Invalid filename")
return safe_name
def resolve_temp_path(self, file_path: str, must_exist: bool = False) -> str:
if not isinstance(file_path, str) or not file_path.strip():
raise HTTPException(status_code=400, detail="Invalid file path")
try:
resolved_path = os.path.realpath(os.path.abspath(file_path))
except OSError as exc:
raise HTTPException(status_code=404, detail="File not found") from exc
base_dir = self._base_dir_realpath()
if not (
resolved_path == base_dir
or resolved_path.startswith(f"{base_dir}{os.sep}")
):
raise HTTPException(
status_code=400,
detail="File path must stay within the temp directory",
)
if must_exist and not os.path.exists(resolved_path):
raise HTTPException(status_code=404, detail="File not found")
return resolved_path
def resolve_existing_temp_paths(self, file_paths: Optional[list[str]]) -> list[str]:
if not file_paths:
return []
return [
self.resolve_temp_path(file_path, must_exist=True) for file_path in file_paths
]
def create_dir_in_dir(self, base_dir: str, dir_name: Optional[str] = None) -> str:
temp_dir_name = (
self.sanitize_upload_filename(dir_name) if dir_name else str(uuid.uuid4())
)
temp_dir = os.path.join(base_dir, temp_dir_name)
temp_dir = os.path.realpath(temp_dir)
self._assert_within_base_dir(
temp_dir, "Directory path must stay within the temp directory"
)
os.makedirs(temp_dir, exist_ok=True)
return temp_dir
def create_temp_dir(self, dir_name: Optional[str] = None) -> str:
return self.create_dir_in_dir(self.base_dir, dir_name)
def create_temp_file_path(
self, file_path: str, dir_path: Optional[str] = None
) -> str:
if dir_path is None:
dir_path = self.base_dir
safe_name = self.sanitize_upload_filename(file_path)
resolved_dir = os.path.realpath(dir_path)
self._assert_within_base_dir(
resolved_dir, "Directory path must stay within the temp directory"
)
full_path = os.path.join(resolved_dir, safe_name)
full_path = os.path.realpath(full_path)
self._assert_within_base_dir(
full_path, "File path must stay within the temp directory"
)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
return full_path
def create_temp_file(
self,
file_path: str,
content: Union[bytes, str],
dir_path: Optional[str] = None,
) -> str:
file_path = self.create_temp_file_path(file_path, dir_path)
mode = "wb" if isinstance(content, bytes) else "w"
with open(file_path, mode) as f:
f.write(content)
return file_path
def read_temp_file(self, file_path: str, binary: bool = True) -> Union[bytes, str]:
file_path = self.resolve_temp_path(file_path, must_exist=True)
base_dir = self._base_dir_realpath()
if not (file_path == base_dir or file_path.startswith(f"{base_dir}{os.sep}")):
raise HTTPException(
status_code=400,
detail="File path must stay within the temp directory",
)
mode = "rb" if binary else "r"
with open(file_path, mode) as f:
return f.read()
async def update_temp_file_from_upload(self, file_path: str, upload_file) -> None:
if not isinstance(file_path, str) or not file_path.strip():
raise HTTPException(status_code=400, detail="Invalid file path")
base_dir = self._base_dir_realpath()
normalized_path = os.path.realpath(os.path.abspath(file_path))
if not normalized_path.startswith(base_dir):
raise HTTPException(
status_code=400,
detail="File path must stay within the temp directory",
)
self._assert_within_base_dir(
normalized_path, "File path must stay within the temp directory"
)
with open(normalized_path, "wb") as f:
f.write(await upload_file.read())
def cleanup_temp_file(self, file_path: str):
try:
file_path = self.resolve_temp_path(file_path, must_exist=True)
except HTTPException as exc:
if exc.status_code == 404:
return
raise
os.remove(file_path)
def _delete_dir_files(self, dir_path: str):
dir_path = self.resolve_temp_path(dir_path, must_exist=True)
for root, dirs, files in os.walk(dir_path, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
def cleanup_temp_dir(self, dir_path: str):
try:
dir_path = self.resolve_temp_path(dir_path, must_exist=True)
except HTTPException as exc:
if exc.status_code == 404:
return
raise
self._delete_dir_files(dir_path)
os.rmdir(dir_path)
def cleanup_base_dir(self):
self.cleanup_temp_dir(self.base_dir)
TEMP_FILE_SERVICE = TempFileService()