Files

311 lines
8.9 KiB
Python

"""D-InSAR 结果导出服务:将结果文件复制到用户指定目录。"""
from __future__ import annotations
import logging
import os
import re
import shutil
from typing import Any, Dict, List
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 _legacy_export_dinsar_results_flat(
file_paths: List[str],
target_dir: str,
) -> Dict[str, Any]:
"""
将指定的 D-InSAR 结果文件复制到目标目录。
支持本地路径和 UNC 路径(如 \\\\server\\share\\path)。
对每个主文件,同时复制同名的附属文件(.hdr, .sml 等)。
返回复制统计信息。
"""
# 规范化目标路径
target_dir = target_dir.strip()
if not target_dir:
raise ValueError("目标路径不能为空")
# 创建目标目录
try:
os.makedirs(target_dir, exist_ok=True)
except OSError as e:
raise ValueError(f"无法创建目标目录 '{target_dir}': {e}")
copied = 0
skipped = 0
failed = 0
details: List[Dict[str, Any]] = []
for file_path in file_paths:
file_path = file_path.strip()
if not file_path:
continue
if not os.path.isfile(file_path):
failed += 1
details.append({
"source": file_path,
"status": "error",
"message": "源文件不存在",
})
continue
base_name = os.path.basename(file_path)
src_dir = os.path.dirname(file_path)
item_copied = 0
item_skipped = 0
item_failed = 0
for ext in _ASSOCIATED_EXTENSIONS:
src = os.path.join(src_dir, base_name + ext) if ext else file_path
if not os.path.isfile(src):
continue
dst = os.path.join(target_dir, base_name + ext)
try:
if os.path.isfile(dst):
# 大小相同则跳过
if os.path.getsize(src) == os.path.getsize(dst):
item_skipped += 1
skipped += 1
continue
shutil.copy2(src, dst)
item_copied += 1
copied += 1
except OSError as e:
item_failed += 1
failed += 1
logger.warning("复制文件失败 %s -> %s: %s", src, dst, e)
status = "ok" if item_failed == 0 else "partial"
details.append({
"source": file_path,
"name": base_name,
"status": status,
"copied": item_copied,
"skipped": item_skipped,
"failed": item_failed,
})
return {
"total_files": len(file_paths),
"copied": copied,
"skipped": skipped,
"failed": failed,
"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,
}