Files

176 lines
6.1 KiB
Python

"""Migrate repo-local runtime data to configured external roots."""
from __future__ import annotations
import argparse
import asyncio
import shutil
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
BACKEND_DIR = PROJECT_ROOT / "backend"
LEGACY_RUNTIME = BACKEND_DIR / "runtime"
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from backend.app.config import settings # noqa: E402
from backend.app import database # noqa: E402
from backend.app.services.sbas_insar_catalog_service import sbas_insar_catalog_service # noqa: E402
from backend.app.services.sbas_insar_production_service import sbas_insar_production_service # noqa: E402
def _norm(path: Path | str) -> Path:
return Path(path).expanduser().resolve()
def _is_under(path: Path, parent: Path) -> bool:
try:
path.relative_to(parent)
return True
except ValueError:
return False
def _measure(path: Path) -> tuple[int, int]:
if not path.exists():
return 0, 0
if path.is_file():
return path.stat().st_size, 1
size = 0
files = 0
for item in path.rglob("*"):
if item.is_file():
try:
size += item.stat().st_size
files += 1
except OSError:
continue
return size, files
def _gb(size: int) -> str:
return f"{size / (1024 ** 3):.3f} GB"
def _copy_tree(source: Path, target: Path, *, delete_source: bool) -> dict[str, object]:
if not source.exists():
return {"source": str(source), "target": str(target), "exists": False, "copied": False}
if _norm(target) == _norm(source):
return {"source": str(source), "target": str(target), "exists": True, "copied": False, "reason": "same_path"}
if _is_under(_norm(target), _norm(source)):
raise ValueError(f"refusing to copy {source} into itself: {target}")
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists():
raise FileExistsError(f"target already exists: {target}")
shutil.copytree(source, target)
size, files = _measure(target)
if delete_source:
shutil.rmtree(source)
return {
"source": str(source),
"target": str(target),
"exists": True,
"copied": True,
"deleted_source": bool(delete_source),
"size": _gb(size),
"files": files,
}
def migrate_pyint_dem(*, apply: bool, delete_source: bool) -> dict[str, object]:
source = LEGACY_RUNTIME / "pyint_dem"
target = Path(settings.PYINT_DEM_ROOT)
size, files = _measure(source)
plan = {
"source": str(source),
"target": str(target),
"source_exists": source.exists(),
"source_size": _gb(size),
"source_files": files,
"apply": apply,
"delete_source": delete_source,
}
if not apply:
return plan
return {**plan, "result": _copy_tree(source, target, delete_source=delete_source)}
def sync_sbas_products(*, apply: bool) -> dict[str, object]:
legacy_root = LEGACY_RUNTIME / "sbas_insar_production" / "runs"
configured_work_root = Path(settings.GAMMA_SBAS_WORK_ROOT) / "runs"
roots = []
for root in (legacy_root, configured_work_root):
if root.is_dir() and root not in roots:
roots.append(root)
run_ids = sorted(
{
path.parent.name
for root in roots
for path in root.glob("*/run_manifest.json")
}
)
plan: dict[str, object] = {
"source_roots": [str(root) for root in roots],
"product_root": str(Path(settings.GAMMA_SBAS_PRODUCT_ROOT) / "runs"),
"run_ids": run_ids,
"apply": apply,
}
if not apply:
return plan
synced = []
original_root = sbas_insar_production_service.production_root
try:
for run_id in run_ids:
selected_root = next((root for root in roots if (root / run_id / "run_manifest.json").is_file()), None)
if selected_root is None:
continue
sbas_insar_production_service.production_root = selected_root.parent
synced.append(sbas_insar_production_service.sync_product_package(run_id))
finally:
sbas_insar_production_service.production_root = original_root
plan["synced"] = synced
return plan
async def rebuild_sbas_catalog() -> dict[str, object]:
if database.AsyncSessionLocal is None:
database.init_db(settings.DATABASE_URL)
if database.AsyncSessionLocal is None:
raise RuntimeError("Database session factory is not initialized")
async with database.AsyncSessionLocal() as db:
return await sbas_insar_catalog_service.rebuild_catalog(db, full_rebuild=True)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--apply", action="store_true", help="perform the migration; default only prints the plan")
parser.add_argument("--move-pyint-dem", action="store_true", help="copy legacy pyint_dem to PYINT_DEM_ROOT")
parser.add_argument("--delete-legacy-pyint-dem", action="store_true", help="delete legacy pyint_dem after copy")
parser.add_argument("--sync-sbas-products", action="store_true", help="sync lightweight SBAS products into GAMMA_SBAS_PRODUCT_ROOT")
parser.add_argument("--rebuild-sbas-catalog", action="store_true", help="rebuild SBAS result catalog after syncing products")
args = parser.parse_args()
actions: dict[str, object] = {}
if args.move_pyint_dem:
actions["pyint_dem"] = migrate_pyint_dem(apply=args.apply, delete_source=args.delete_legacy_pyint_dem)
if args.sync_sbas_products:
actions["sbas_products"] = sync_sbas_products(apply=args.apply)
if args.rebuild_sbas_catalog:
if args.apply:
actions["sbas_catalog"] = asyncio.run(rebuild_sbas_catalog())
else:
actions["sbas_catalog"] = {"apply": False, "action": "rebuild_sbas_catalog"}
if not actions:
actions["message"] = "no actions selected"
import json
print(json.dumps(actions, indent=2, ensure_ascii=False, default=str))
return 0
if __name__ == "__main__":
raise SystemExit(main())