paths.py 2.6 KB

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