DocHub/backend/app/local_file_client.py
Tianyang 542361c63a fix: natural-sort file tree, indent nested folders, fix editor layout
Also add a reindex endpoint and button to rebuild the Meilisearch
index from files already on disk, without needing to re-upload them.
2026-08-01 17:32:32 +08:00

87 lines
3.0 KiB
Python

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
def walk_files(self) -> list[str]:
"""递归遍历所有文件,返回相对于 base_dir 的 key 列表(不含目录)"""
result = []
for root, _dirs, filenames in os.walk(self._base_dir):
for filename in filenames:
full_path = Path(root) / filename
rel_path = full_path.relative_to(self._base_dir)
result.append(str(rel_path).replace(os.sep, "/"))
return result