directory.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. """
  2. 目录浏览 API 路由
  3. 提供文件系统浏览和图片+JSON文件扫描功能
  4. """
  5. import os
  6. import re
  7. from pathlib import Path
  8. from fastapi import APIRouter, HTTPException, Query
  9. from pydantic import BaseModel
  10. from typing import List, Optional, Dict
  11. from models.schemas import HomeDirectoryResponse
  12. router = APIRouter(prefix="/api/directories", tags=["directories"])
  13. class DirectoryItem(BaseModel):
  14. """目录项"""
  15. name: str
  16. path: str
  17. is_dir: bool
  18. size: Optional[int] = None
  19. class BrowseResponse(BaseModel):
  20. """浏览响应"""
  21. current_path: str
  22. parent_path: Optional[str]
  23. items: List[DirectoryItem]
  24. class FilePair(BaseModel):
  25. """图片+JSON文件对"""
  26. index: int
  27. image_path: str
  28. image_name: str
  29. json_path: str
  30. json_name: str
  31. has_structure: bool = False # 是否已有结构文件
  32. structure_path: Optional[str] = None
  33. class ScanRequest(BaseModel):
  34. """扫描请求"""
  35. image_dir: str
  36. json_dir: str
  37. image_pattern: Optional[str] = r".*\.(png|jpg|jpeg)$"
  38. json_pattern: Optional[str] = r".*\.json$"
  39. output_dir: Optional[str] = None # 用于检查已有结构文件
  40. class ScanResponse(BaseModel):
  41. """扫描响应"""
  42. total: int
  43. pairs: List[FilePair]
  44. labeled_count: int # 已标注数量
  45. @router.get("/browse", response_model=BrowseResponse)
  46. async def browse_directory(
  47. path: str = Query(default="~", description="要浏览的目录路径"),
  48. show_hidden: bool = Query(default=False, description="是否显示隐藏文件")
  49. ):
  50. """
  51. 浏览目录结构
  52. Args:
  53. path: 目录路径,支持 ~ 表示用户主目录
  54. show_hidden: 是否显示隐藏文件(以.开头的文件)
  55. """
  56. # 展开用户目录
  57. if path.startswith("~"):
  58. path = os.path.expanduser(path)
  59. target_path = Path(path).resolve()
  60. if not target_path.exists():
  61. raise HTTPException(status_code=404, detail=f"目录不存在: {path}")
  62. if not target_path.is_dir():
  63. raise HTTPException(status_code=400, detail=f"不是目录: {path}")
  64. items = []
  65. try:
  66. for item in sorted(target_path.iterdir()):
  67. # 跳过隐藏文件
  68. if not show_hidden and item.name.startswith('.'):
  69. continue
  70. try:
  71. is_dir = item.is_dir()
  72. size = None
  73. if not is_dir:
  74. try:
  75. size = item.stat().st_size
  76. except:
  77. pass
  78. items.append(DirectoryItem(
  79. name=item.name,
  80. path=str(item),
  81. is_dir=is_dir,
  82. size=size
  83. ))
  84. except PermissionError:
  85. continue
  86. except PermissionError:
  87. raise HTTPException(status_code=403, detail=f"无权限访问目录: {path}")
  88. # 目录排在前面
  89. items.sort(key=lambda x: (not x.is_dir, x.name.lower()))
  90. # 计算父目录
  91. parent = target_path.parent
  92. parent_path = str(parent) if parent != target_path else None
  93. return BrowseResponse(
  94. current_path=str(target_path),
  95. parent_path=parent_path,
  96. items=items
  97. )
  98. @router.post("/scan", response_model=ScanResponse)
  99. async def scan_directory(request: ScanRequest):
  100. """
  101. 扫描目录下的图片+JSON文件对
  102. 扫描 image_dir 中匹配 image_pattern 的图片,
  103. 然后在 json_dir 中查找对应的 JSON 文件。
  104. """
  105. image_dir = Path(request.image_dir)
  106. json_dir = Path(request.json_dir)
  107. if not image_dir.exists():
  108. raise HTTPException(status_code=404, detail=f"图片目录不存在: {request.image_dir}")
  109. if not json_dir.exists():
  110. raise HTTPException(status_code=404, detail=f"JSON目录不存在: {request.json_dir}")
  111. # 编译正则表达式
  112. try:
  113. image_re = re.compile(request.image_pattern or r".*\.(png|jpg|jpeg)$", re.IGNORECASE)
  114. json_re = re.compile(request.json_pattern or r".*\.json$", re.IGNORECASE)
  115. except re.error as e:
  116. raise HTTPException(status_code=400, detail=f"无效的正则表达式: {e}")
  117. # 扫描图片文件
  118. image_files = []
  119. for f in image_dir.iterdir():
  120. if f.is_file() and image_re.match(f.name):
  121. image_files.append(f)
  122. # 按名称排序
  123. image_files.sort(key=lambda x: x.name)
  124. # 构建文件对
  125. pairs = []
  126. labeled_count = 0
  127. output_dir = Path(request.output_dir) if request.output_dir else None
  128. for idx, image_file in enumerate(image_files):
  129. # 查找对应的 JSON 文件
  130. base_name = image_file.stem
  131. json_file = None
  132. # 尝试多种匹配方式
  133. for jf in json_dir.iterdir():
  134. if jf.is_file() and json_re.match(jf.name):
  135. # 完全匹配
  136. if jf.stem == base_name:
  137. json_file = jf
  138. break
  139. # 前缀匹配
  140. if jf.stem.startswith(base_name) or base_name.startswith(jf.stem):
  141. json_file = jf
  142. break
  143. if json_file:
  144. # 检查是否已有结构文件
  145. has_structure = False
  146. structure_path = None
  147. if output_dir and output_dir.exists():
  148. structure_file = output_dir / f"{base_name}_structure.json"
  149. if structure_file.exists():
  150. has_structure = True
  151. structure_path = str(structure_file)
  152. labeled_count += 1
  153. pairs.append(FilePair(
  154. index=idx + 1,
  155. image_path=str(image_file),
  156. image_name=image_file.name,
  157. json_path=str(json_file),
  158. json_name=json_file.name,
  159. has_structure=has_structure,
  160. structure_path=structure_path
  161. ))
  162. return ScanResponse(
  163. total=len(pairs),
  164. pairs=pairs,
  165. labeled_count=labeled_count
  166. )
  167. @router.get("/home", response_model=HomeDirectoryResponse)
  168. async def get_home_directory():
  169. """获取用户主目录"""
  170. home = os.path.expanduser("~")
  171. return HomeDirectoryResponse(path=home)