import os from pathlib import Path from datetime import datetime, timezone class LocalFileClient: """本地文件系统存储客户端""" def __init__(self, base_dir: str): self._base_dir = Path(base_dir).resolve() self._base_dir.mkdir(parents=True, exist_ok=True) def _resolve_path(self, key: str) -> Path: """将 key 转换为文件系统路径,确保不会逃出 base_dir""" path = (self._base_dir / key).resolve() if not path.is_relative_to(self._base_dir): raise ValueError(f"Path traversal attempt: {key}") return path def list_objects(self, prefix: str) -> list[dict]: """列出指定前缀下的所有对象(文件和目录)""" prefix_path = self._base_dir / prefix if prefix else self._base_dir if not prefix_path.exists(): return [] result = [] try: for entry in prefix_path.iterdir(): rel_path = entry.relative_to(self._base_dir) key = str(rel_path).replace(os.sep, "/") if entry.is_dir(): result.append({ "key": key + "/", "size": None, "is_dir": True, "modifiedAt": None, }) else: stat = entry.stat() result.append({ "key": key, "size": stat.st_size, "is_dir": False, "modifiedAt": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(), }) except (PermissionError, OSError): pass return result def get_object(self, key: str) -> bytes: path = self._resolve_path(key) if not path.exists() or not path.is_file(): raise FileNotFoundError(key) return path.read_bytes() def put_object(self, key: str, data: bytes, content_type: str) -> None: path = self._resolve_path(key) path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(data) def delete_object(self, key: str) -> None: path = self._resolve_path(key) if not path.exists(): raise FileNotFoundError(key) path.unlink() # 清理空目录(逐层向上) parent = path.parent while parent != self._base_dir: if not any(parent.iterdir()): parent.rmdir() parent = parent.parent else: break