Fix D-InSAR export UI and task folder isolation

This commit is contained in:
2026-04-17 00:18:03 +08:00
parent bc5ed74d4d
commit 6e98bbe8b0
7 changed files with 335 additions and 97 deletions
+12 -4
View File
@@ -371,7 +371,7 @@ async def export_dinsar_results_endpoint(
if not records:
raise HTTPException(status_code=404, detail="未找到任何匹配的结果记录")
file_paths = []
export_items = []
for record in records:
compat_row = record.compat_row
file_path = (
@@ -380,15 +380,23 @@ async def export_dinsar_results_endpoint(
or (str(compat_row.file_path).strip() if compat_row is not None and compat_row.file_path else "")
)
if file_path:
file_paths.append(file_path)
export_items.append(
{
"source": file_path,
"task_alias": str(record.product.task_alias or "").strip(),
"task_name": str(record.product.task_name or "").strip(),
"display_name": str(record.display_name or "").strip(),
"product_id": str(record.product.product_id or "").strip(),
}
)
if not file_paths:
if not export_items:
raise HTTPException(status_code=400, detail="选中的结果没有关联的文件路径")
try:
import asyncio
export_result = await asyncio.to_thread(
export_dinsar_results, file_paths, request.target_dir
export_dinsar_results, export_items, request.target_dir
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
+209 -1
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import logging
import os
import re
import shutil
from typing import Any, Dict, List
@@ -10,9 +11,34 @@ logger = logging.getLogger(__name__)
# 允许的文件扩展名(主文件 + 附属文件)
_ASSOCIATED_EXTENSIONS = ["", ".hdr", ".sml", ".xml", ".aux.xml", ".prj"]
_INVALID_SEGMENT_RE = re.compile(r'[<>:"/\\|?*\x00-\x1F]+')
_WINDOWS_RESERVED_NAMES = {
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
}
def export_dinsar_results(
def _legacy_export_dinsar_results_flat(
file_paths: List[str],
target_dir: str,
) -> Dict[str, Any]:
@@ -100,3 +126,185 @@ def export_dinsar_results(
"target_dir": target_dir,
"details": details,
}
def _derive_folder_hint(file_path: str) -> str:
base_name = os.path.basename(str(file_path or "").strip())
stem, _ = os.path.splitext(base_name)
for suffix in ("_geo_disp", "_disp", "_coh"):
if stem.lower().endswith(suffix):
return stem[: -len(suffix)] or stem
return stem
def _sanitize_path_segment(value: Any, *, default: str) -> str:
text = str(value or "").strip()
text = _INVALID_SEGMENT_RE.sub("_", text).strip(" .")
if not text:
text = default
if text.upper() in _WINDOWS_RESERVED_NAMES:
text = f"_{text}"
return text[:120]
def _normalize_export_item(raw_item: Any, index: int) -> Dict[str, Any]:
if isinstance(raw_item, dict):
source_path = str(
raw_item.get("source")
or raw_item.get("file_path")
or raw_item.get("path")
or ""
).strip()
folder_hint = (
raw_item.get("task_alias")
or raw_item.get("task_name")
or raw_item.get("display_name")
or raw_item.get("name")
or ""
)
else:
source_path = str(raw_item or "").strip()
folder_hint = ""
base_name = os.path.basename(source_path) if source_path else ""
folder_name = _sanitize_path_segment(
folder_hint or _derive_folder_hint(source_path),
default=f"result_{index}",
)
return {
"source_path": source_path,
"base_name": base_name,
"folder_name": folder_name,
}
def _same_file_size(left: str, right: str) -> bool:
try:
return os.path.getsize(left) == os.path.getsize(right)
except OSError:
return False
def _reserve_target_dir(
*,
target_root: str,
folder_name: str,
base_name: str,
source_path: str,
planned_primary_paths: Dict[str, str],
) -> str:
suffix = 1
while True:
candidate_name = folder_name if suffix == 1 else f"{folder_name}__{suffix}"
candidate_dir = os.path.join(target_root, candidate_name)
candidate_primary = os.path.join(candidate_dir, base_name)
normalized_primary = os.path.normcase(os.path.abspath(candidate_primary))
planned_source = planned_primary_paths.get(normalized_primary)
if planned_source and os.path.normcase(planned_source) != os.path.normcase(source_path):
suffix += 1
continue
if os.path.isfile(candidate_primary) and not _same_file_size(source_path, candidate_primary):
suffix += 1
continue
planned_primary_paths[normalized_primary] = source_path
return candidate_dir
def export_dinsar_results(
items: List[Any],
target_dir: str,
) -> Dict[str, Any]:
"""Copy selected D-InSAR results into task-based subdirectories."""
target_dir = target_dir.strip()
if not target_dir:
raise ValueError("目标路径不能为空")
try:
os.makedirs(target_dir, exist_ok=True)
except OSError as exc:
raise ValueError(f"无法创建目标目录 '{target_dir}': {exc}") from exc
copied = 0
skipped = 0
failed = 0
details: List[Dict[str, Any]] = []
planned_primary_paths: Dict[str, str] = {}
for index, raw_item in enumerate(items, start=1):
item = _normalize_export_item(raw_item, index)
source_path = item["source_path"]
base_name = item["base_name"]
folder_name = item["folder_name"]
if not source_path:
continue
if not os.path.isfile(source_path):
failed += 1
details.append(
{
"source": source_path,
"folder_name": folder_name,
"status": "error",
"message": "源文件不存在",
}
)
continue
target_item_dir = _reserve_target_dir(
target_root=target_dir,
folder_name=folder_name,
base_name=base_name,
source_path=source_path,
planned_primary_paths=planned_primary_paths,
)
src_dir = os.path.dirname(source_path)
item_copied = 0
item_skipped = 0
item_failed = 0
target_files: List[str] = []
for ext in _ASSOCIATED_EXTENSIONS:
src = os.path.join(src_dir, base_name + ext) if ext else source_path
if not os.path.isfile(src):
continue
dst = os.path.join(target_item_dir, base_name + ext)
target_files.append(dst)
try:
os.makedirs(target_item_dir, exist_ok=True)
if os.path.isfile(dst) and _same_file_size(src, dst):
item_skipped += 1
skipped += 1
continue
shutil.copy2(src, dst)
item_copied += 1
copied += 1
except OSError as exc:
item_failed += 1
failed += 1
logger.warning("Failed to copy D-InSAR result %s -> %s: %s", src, dst, exc)
status = "ok" if item_failed == 0 else "partial"
details.append(
{
"source": source_path,
"name": base_name,
"folder_name": os.path.basename(target_item_dir),
"target_dir": target_item_dir,
"target_files": target_files,
"status": status,
"copied": item_copied,
"skipped": item_skipped,
"failed": item_failed,
}
)
return {
"total_files": len(items),
"copied": copied,
"skipped": skipped,
"failed": failed,
"target_dir": target_dir,
"details": details,
}