mirror of
https://github.com/Terranom674/Piwigo_Bratonien_Tools.git
synced 2026-09-19 11:44:31 +00:00
Add connector manifest builder runtime
This commit is contained in:
120
runtime/lib/build_manifest.py
Normal file
120
runtime/lib/build_manifest.py
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve Nextcloud view rows through explicitly configured storage mounts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
|
||||
def read_config(path: Path) -> dict[str, tuple[str, Path]]:
|
||||
result: dict[str, tuple[str, Path]] = {}
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
for number, line in enumerate(handle, 1):
|
||||
line = line.rstrip("\n")
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
fields = line.split("\t")
|
||||
if len(fields) != 3:
|
||||
raise ValueError(f"{path}:{number}: expected storage_id, source_prefix and local_mount")
|
||||
storage_id, prefix, mount = fields
|
||||
result[storage_id] = (prefix.strip("/"), Path(mount))
|
||||
return result
|
||||
|
||||
|
||||
def query_rows(args: argparse.Namespace) -> list[list[str]]:
|
||||
password = args.password_file.read_text(encoding="utf-8").strip()
|
||||
env = os.environ.copy()
|
||||
env["PGPASSWORD"] = password
|
||||
sql = f"SELECT share_id, display_name, storage_id, source_path FROM {args.view} ORDER BY share_id"
|
||||
command = [
|
||||
"psql", "-X", "-A", "-F", "\t", "-t",
|
||||
"-h", args.host, "-p", str(args.port), "-U", args.user, "-d", args.database,
|
||||
"-c", sql,
|
||||
]
|
||||
completed = subprocess.run(command, env=env, check=True, text=True, capture_output=True)
|
||||
return list(csv.reader(completed.stdout.splitlines(), delimiter="\t"))
|
||||
|
||||
|
||||
def contained_join(root: Path, relative: str) -> Path:
|
||||
parts = PurePosixPath(relative).parts
|
||||
if relative.startswith("/") or ".." in parts:
|
||||
raise ValueError(f"unsafe source path: {relative}")
|
||||
return root.joinpath(*parts)
|
||||
|
||||
|
||||
def build(args: argparse.Namespace) -> dict[str, object]:
|
||||
adapters = read_config(args.storage_config)
|
||||
rows = query_rows(args)
|
||||
if not rows and not args.allow_empty:
|
||||
raise RuntimeError("Nextcloud returned no Showcase shares; refusing an empty manifest")
|
||||
|
||||
manifest: list[str] = []
|
||||
errors: list[str] = []
|
||||
for row in rows:
|
||||
if len(row) != 4:
|
||||
errors.append(f"invalid database row with {len(row)} columns")
|
||||
continue
|
||||
share_id, display_name, storage_id, source_path = row
|
||||
adapter = adapters.get(storage_id)
|
||||
if not adapter:
|
||||
errors.append(f"share {share_id}: unknown storage {storage_id}")
|
||||
continue
|
||||
prefix, mount = adapter
|
||||
relative = source_path.strip("/")
|
||||
if prefix:
|
||||
expected = prefix + "/"
|
||||
if relative != prefix and not relative.startswith(expected):
|
||||
errors.append(f"share {share_id}: path does not match configured prefix")
|
||||
continue
|
||||
relative = relative[len(prefix):].lstrip("/")
|
||||
source = contained_join(mount, relative)
|
||||
if not mount.is_mount():
|
||||
errors.append(f"share {share_id}: storage mount unavailable: {mount}")
|
||||
continue
|
||||
if not source.is_dir():
|
||||
errors.append(f"share {share_id}: source directory unavailable: {source}")
|
||||
continue
|
||||
manifest.append(f"{share_id}\t{display_name.lstrip('/')}\t{source}")
|
||||
|
||||
if errors:
|
||||
raise RuntimeError("; ".join(errors))
|
||||
if len(manifest) != len(rows):
|
||||
raise RuntimeError("not all Showcase shares could be resolved")
|
||||
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=args.output.parent, delete=False) as handle:
|
||||
handle.write("\n".join(manifest) + ("\n" if manifest else ""))
|
||||
temporary = Path(handle.name)
|
||||
temporary.replace(args.output)
|
||||
return {"shares": len(manifest), "manifest": str(args.output)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--host", required=True)
|
||||
parser.add_argument("--port", type=int, default=5432)
|
||||
parser.add_argument("--database", required=True)
|
||||
parser.add_argument("--user", required=True)
|
||||
parser.add_argument("--password-file", required=True, type=Path)
|
||||
parser.add_argument("--view", default="piwigo_showcase_sources")
|
||||
parser.add_argument("--storage-config", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--allow-empty", action="store_true")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
print(json.dumps(build(args), ensure_ascii=False))
|
||||
except Exception as error:
|
||||
print(f"manifest: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user