|
|
@@ -11,26 +11,62 @@ from typing import Any, Dict
|
|
|
from app.core.config import get_settings
|
|
|
|
|
|
|
|
|
+class StorageError(Exception):
|
|
|
+ """存储参数/写入错误(如路径穿越)。"""
|
|
|
+
|
|
|
+ def __init__(self, message: str):
|
|
|
+ self.message = message
|
|
|
+ super().__init__(message)
|
|
|
+
|
|
|
+
|
|
|
def _unique_name(filename: str) -> str:
|
|
|
ext = os.path.splitext(filename)[1] or ""
|
|
|
return f"{uuid.uuid4().hex}{ext}"
|
|
|
|
|
|
|
|
|
+def _normalize_folder(folder: str) -> str:
|
|
|
+ """规范化 folder:拒绝空段以外的 '..' / 绝对路径痕迹。"""
|
|
|
+ raw = (folder or "").strip().replace("\\", "/")
|
|
|
+ if not raw:
|
|
|
+ return ""
|
|
|
+ # 拒绝绝对路径或盘符式前缀
|
|
|
+ if raw.startswith("/") or (len(raw) >= 2 and raw[1] == ":"):
|
|
|
+ raise StorageError("非法的文件夹路径")
|
|
|
+ parts: list[str] = []
|
|
|
+ for part in raw.split("/"):
|
|
|
+ if part in ("", "."):
|
|
|
+ continue
|
|
|
+ if part == "..":
|
|
|
+ raise StorageError("非法的文件夹路径")
|
|
|
+ parts.append(part)
|
|
|
+ return "/".join(parts)
|
|
|
+
|
|
|
+
|
|
|
def _object_key(folder: str, filename: str) -> str:
|
|
|
unique = _unique_name(filename)
|
|
|
date_part = datetime.now(timezone.utc).strftime("%Y%m/%d")
|
|
|
- folder = (folder or "").strip().strip("/")
|
|
|
+ folder = _normalize_folder(folder)
|
|
|
if folder:
|
|
|
return f"{folder}/{date_part}/{unique}"
|
|
|
return f"{date_part}/{unique}"
|
|
|
|
|
|
|
|
|
+def _ensure_within_base(base_dir: Path, dest: Path) -> None:
|
|
|
+ base_resolved = base_dir.resolve()
|
|
|
+ dest_resolved = dest.resolve()
|
|
|
+ try:
|
|
|
+ dest_resolved.relative_to(base_resolved)
|
|
|
+ except ValueError as exc:
|
|
|
+ raise StorageError("非法的文件夹路径") from exc
|
|
|
+
|
|
|
+
|
|
|
class LocalStorage:
|
|
|
def upload_file(self, content: bytes, filename: str, folder: str = "") -> Dict[str, str]:
|
|
|
settings = get_settings()
|
|
|
base_dir = Path(settings.LOCAL_STORAGE_DIR)
|
|
|
object_key = _object_key(folder, filename)
|
|
|
dest = base_dir / object_key
|
|
|
+ _ensure_within_base(base_dir, dest)
|
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
|
dest.write_bytes(content)
|
|
|
url = f"/uploads/{object_key}"
|