paths.py 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. """DMS存储目录创建和安全路径解析。"""
  2. from __future__ import annotations
  3. from pathlib import Path, PurePosixPath
  4. STORAGE_SUBDIRECTORIES = (
  5. "original",
  6. "preview",
  7. "extracted",
  8. "temporary",
  9. "quarantine",
  10. "recycle",
  11. "voucher",
  12. )
  13. class UnsafeStoragePathError(ValueError):
  14. """相对路径试图逃逸DMS存储根目录。"""
  15. def ensure_storage_directories(storage_root: str | Path) -> Path:
  16. root = Path(storage_root).expanduser().resolve()
  17. root.mkdir(parents=True, exist_ok=True)
  18. for directory in STORAGE_SUBDIRECTORIES:
  19. (root / directory).mkdir(parents=True, exist_ok=True)
  20. return root
  21. def resolve_storage_path(
  22. relative_path: str,
  23. storage_root: str | Path,
  24. ) -> Path:
  25. """安全解析数据库相对路径并保证结果仍位于存储根目录。"""
  26. if not relative_path or "\x00" in relative_path:
  27. raise UnsafeStoragePathError("存储相对路径不能为空或包含空字符")
  28. normalized = PurePosixPath(relative_path.replace("\\", "/"))
  29. has_drive_prefix = bool(normalized.parts and ":" in normalized.parts[0])
  30. if normalized.is_absolute() or has_drive_prefix or ".." in normalized.parts:
  31. raise UnsafeStoragePathError("存储路径必须是根目录内的安全相对路径")
  32. root = Path(storage_root).expanduser().resolve()
  33. candidate = root.joinpath(*normalized.parts).resolve()
  34. if not candidate.is_relative_to(root):
  35. raise UnsafeStoragePathError("存储路径超出DMS存储根目录")
  36. return candidate
  37. def preview_cache_path(file_hash: str, storage_root: str | Path) -> Path:
  38. """基于文件SHA-256返回预览PDF缓存路径(支持前两位分层)。"""
  39. if not file_hash or len(file_hash) < 2:
  40. raise UnsafeStoragePathError("文件哈希无效")
  41. root = Path(storage_root).expanduser().resolve()
  42. prefix = file_hash[:2].lower()
  43. candidate = (root / "preview" / prefix / f"{file_hash}.pdf").resolve()
  44. if not candidate.is_relative_to(root / "preview"):
  45. raise UnsafeStoragePathError("预览缓存路径超出预览目录")
  46. return candidate
  47. def preview_directory_for_hash(file_hash: str, storage_root: str | Path) -> Path:
  48. """返回预览缓存文件所在目录,确保位于 preview/ 下。"""
  49. path = preview_cache_path(file_hash, storage_root)
  50. return path.parent
  51. def is_path_inside_preview(path: Path, storage_root: str | Path) -> bool:
  52. """检查路径是否位于预览缓存目录内。"""
  53. root = Path(storage_root).expanduser().resolve()
  54. resolved = path.resolve()
  55. return resolved.is_relative_to(root / "preview")