feat(1c): add complete config storage manifest collector
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
"""Read-only manifest collector for a 1C SQL configuration storage.
|
||||||
|
|
||||||
|
The REST endpoint caps a single storage.files.list response. GUID-prefix
|
||||||
|
partitioning keeps every request below that cap and makes the manifest
|
||||||
|
reproducible without SQL credentials in source control.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
from collections import Counter
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
PREFIXES = [*"0123456789abcdef", "root"]
|
||||||
|
|
||||||
|
|
||||||
|
def rpc(url: str, method: str, payload: dict) -> dict:
|
||||||
|
request = urllib.request.Request(
|
||||||
|
url.rstrip("/") + "/rpc",
|
||||||
|
data=json.dumps({"method": method, "payload": payload}).encode("utf-8"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(request, timeout=180) as response:
|
||||||
|
return json.loads(response.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--url", required=True)
|
||||||
|
parser.add_argument("--base-id", required=True)
|
||||||
|
parser.add_argument("--table", default="Config")
|
||||||
|
parser.add_argument("--output", required=True, type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
files: list[dict] = []
|
||||||
|
partitions: list[dict] = []
|
||||||
|
for prefix in PREFIXES:
|
||||||
|
result = rpc(args.url, "storage.files.list", {
|
||||||
|
"base_id": args.base_id, "table": args.table, "prefix": prefix,
|
||||||
|
"limit": 5000, "diagnostic": True,
|
||||||
|
})
|
||||||
|
rows = result.get("files") if result.get("status") == "ok" else None
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
raise RuntimeError(f"{prefix}: {result}")
|
||||||
|
if len(rows) >= 5000:
|
||||||
|
raise RuntimeError(f"{prefix}: response reached the cap; split this prefix further")
|
||||||
|
files.extend(rows)
|
||||||
|
partitions.append({"prefix": prefix, "files": len(rows)})
|
||||||
|
|
||||||
|
suffixes = Counter(Path(str(row.get("FileName") or "")).suffix or "<descriptor>" for row in files)
|
||||||
|
manifest = {
|
||||||
|
"schema": "onec_config_storage_manifest.v1",
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"source": {"base_id": args.base_id, "table": args.table},
|
||||||
|
"partitions": partitions,
|
||||||
|
"counts": {"files": len(files), "compressed_bytes": sum(int(row.get("Bytes") or 0) for row in files), "suffixes": dict(sorted(suffixes.items()))},
|
||||||
|
"files": files,
|
||||||
|
}
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user