From 3b2db2cabac18d5a968d7a48c1de60ca0d47443e Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 15 Aug 2026 00:31:29 +0300 Subject: [PATCH] feat(1c): add complete config storage manifest collector --- plugins/1c/tools/inventory_config_storage.py | 69 ++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 plugins/1c/tools/inventory_config_storage.py diff --git a/plugins/1c/tools/inventory_config_storage.py b/plugins/1c/tools/inventory_config_storage.py new file mode 100644 index 0000000..5cb9c14 --- /dev/null +++ b/plugins/1c/tools/inventory_config_storage.py @@ -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 "" 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())