Initial commit: snapAna 截图智能整理工具
包含 FastAPI 后端、React 前端、队列/OCR/标签/待办等完整功能。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""跨平台路径工具:重点兼容 Windows UNC 网络路径(\\\\NAS\\share\\...)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path, PureWindowsPath
|
||||
|
||||
|
||||
def normalize_user_path(raw: str) -> str:
|
||||
"""规范化用户输入的路径,保留 UNC 反斜杠格式。
|
||||
|
||||
示例:
|
||||
- \\\\JIULUGNAS\\personal_folder\\Photos -> 原样保留
|
||||
- //JIULUGNAS/personal_folder/Photos -> 转为 UNC
|
||||
- D:/Pictures/Screenshots -> D:\\Pictures\\Screenshots
|
||||
"""
|
||||
raw = (raw or "").strip().strip('"').strip("'")
|
||||
if not raw:
|
||||
return raw
|
||||
|
||||
if sys.platform == "win32":
|
||||
# //server/share -> \\server\share
|
||||
if raw.startswith("//") and not raw.startswith("///"):
|
||||
raw = "\\\\" + raw.lstrip("/").replace("/", "\\")
|
||||
elif raw.startswith("\\\\"):
|
||||
pass
|
||||
else:
|
||||
raw = raw.replace("/", "\\")
|
||||
return str(PureWindowsPath(raw))
|
||||
|
||||
return str(Path(raw).expanduser())
|
||||
|
||||
|
||||
def path_from_storage(stored: str) -> Path:
|
||||
"""从数据库读出的路径转为 Path(修复历史 as_posix 导致的 //NAS/...)。"""
|
||||
if not stored:
|
||||
return Path(stored)
|
||||
if sys.platform == "win32":
|
||||
# 历史数据://JIULUGNAS/foo/bar -> \\JIULUGNAS\foo\bar
|
||||
if stored.startswith("//") and not stored.startswith("///"):
|
||||
stored = "\\\\" + stored.lstrip("/").replace("/", "\\")
|
||||
return Path(stored)
|
||||
|
||||
|
||||
def path_to_storage(path: Path | str) -> str:
|
||||
"""写入数据库 / 比较用的路径字符串;Windows 下保留反斜杠。"""
|
||||
if isinstance(path, Path):
|
||||
if sys.platform == "win32":
|
||||
return str(path)
|
||||
return path.as_posix()
|
||||
return normalize_user_path(str(path)) if sys.platform == "win32" else str(path)
|
||||
|
||||
|
||||
def is_accessible_dir(path: str | Path) -> bool:
|
||||
"""目录是否可访问(UNC / 本地均适用)。"""
|
||||
try:
|
||||
return os.path.isdir(str(path))
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def is_accessible_file(path: str | Path) -> bool:
|
||||
"""文件是否可访问。"""
|
||||
try:
|
||||
return os.path.isfile(str(path))
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def path_is_under(parent: str | Path, child: str | Path) -> bool:
|
||||
"""判断 child 是否在 parent 目录下(用于敏感目录检测)。"""
|
||||
try:
|
||||
parent_norm = os.path.normcase(os.path.normpath(str(parent)))
|
||||
child_norm = os.path.normcase(os.path.normpath(str(child)))
|
||||
if not parent_norm.endswith(os.sep):
|
||||
parent_norm += os.sep
|
||||
return child_norm.startswith(parent_norm) or child_norm == parent_norm.rstrip(os.sep)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def count_files_sample(root: str | Path, limit: int = 5) -> tuple[int, list[str]]:
|
||||
"""快速抽样统计目录下图片数量(网络路径可能较慢,limit 控制遍历深度)。"""
|
||||
from app.services.thumbnail import is_supported
|
||||
|
||||
root_p = path_from_storage(str(root)) if isinstance(root, str) else root
|
||||
total = 0
|
||||
samples: list[str] = []
|
||||
try:
|
||||
for dirpath, _, filenames in os.walk(str(root_p)):
|
||||
for name in filenames:
|
||||
p = Path(dirpath) / name
|
||||
if not is_supported(p):
|
||||
continue
|
||||
total += 1
|
||||
if len(samples) < limit:
|
||||
samples.append(path_to_storage(p))
|
||||
if total >= 1000:
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
return total, samples
|
||||
Reference in New Issue
Block a user