NC-Connector Verzeichnisfilter pro Storage ergaenzen

This commit is contained in:
Terranom674
2026-08-18 14:51:06 +02:00
parent eeb86722ae
commit 779dc3037f

View File

@@ -23,17 +23,20 @@ def validate_view(name: str) -> str:
return value return value
def read_config(path: Path) -> dict[str, tuple[str, Path]]: def read_config(path: Path) -> dict[str, list[tuple[str, Path, str]]]:
result: dict[str, tuple[str, Path]] = {} result: dict[str, list[tuple[str, Path, str]]] = {}
with path.open(encoding="utf-8") as handle: with path.open(encoding="utf-8") as handle:
for number, line in enumerate(handle, 1): for number, line in enumerate(handle, 1):
line=line.rstrip("\n") line=line.rstrip("\n")
if not line or line.startswith("#"):continue if not line or line.startswith("#"):continue
fields=line.split("\t") fields=line.split("\t")
if len(fields)!=3:raise ValueError(f"{path}:{number}: expected storage_id, source_prefix and local_mount") if len(fields) not in {3,4}:raise ValueError(f"{path}:{number}: expected storage_id, source_prefix, local_mount and optional include_prefix")
storage_id,prefix,mount=fields storage_id,prefix,mount=fields[:3]
if not storage_id.strip():raise ValueError(f"{path}:{number}: storage_id is empty") include_prefix=fields[3] if len(fields)==4 else ""
result[storage_id]=(prefix.strip("/"),Path(mount)) storage_id=storage_id.strip();prefix=prefix.strip("/");include_prefix=include_prefix.strip("/")
if not storage_id:raise ValueError(f"{path}:{number}: storage_id is empty")
if ".." in PurePosixPath(prefix).parts or ".." in PurePosixPath(include_prefix).parts:raise ValueError(f"{path}:{number}: unsafe prefix")
result.setdefault(storage_id,[]).append((prefix,Path(mount),include_prefix))
return result return result
@@ -65,21 +68,36 @@ def contained_join(root: Path, relative: str) -> Path:
return root.joinpath(*parts) return root.joinpath(*parts)
def matches_prefix(path: str, prefix: str) -> bool:
if not prefix:return True
return path==prefix or path.startswith(prefix+"/")
def resolve_adapter(adapters: dict[str,list[tuple[str,Path,str]]],storage_id: str,source_path: str) -> tuple[str,Path,str] | None:
relative=source_path.strip("/")
matches=[]
for prefix,mount,include_prefix in adapters.get(storage_id,[]):
if not matches_prefix(relative,prefix):continue
mapped_relative=relative[len(prefix):].lstrip("/") if prefix else relative
if not matches_prefix(mapped_relative,include_prefix):continue
matches.append((prefix,mount,include_prefix))
if not matches:return None
matches.sort(key=lambda item:(len(item[0]),len(item[2])),reverse=True)
return matches[0]
def build(args: argparse.Namespace) -> dict[str,object]: def build(args: argparse.Namespace) -> dict[str,object]:
validate_view(args.view);adapters=read_config(args.storage_config);rows=query_rows(args) validate_view(args.view);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") if not rows and not args.allow_empty:raise RuntimeError("Nextcloud returned no Showcase shares; refusing an empty manifest")
manifest=[];errors=[];folder_count=0;file_count=0 manifest=[];errors=[];folder_count=0;file_count=0
for row in rows: for row in rows:
if len(row)!=5:errors.append(f"invalid database row with {len(row)} columns");continue if len(row)!=5:errors.append(f"invalid database row with {len(row)} columns");continue
share_id,item_type,display_name,storage_id,source_path=row;item_type=item_type.strip().lower() share_id,item_type,display_name,storage_id,source_path=row;item_type=item_type.strip().lower();relative=source_path.strip("/")
if item_type and item_type not in {"folder","file"}:errors.append(f"share {share_id}: unsupported item_type {item_type!r}");continue if item_type and item_type not in {"folder","file"}:errors.append(f"share {share_id}: unsupported item_type {item_type!r}");continue
adapter=adapters.get(storage_id) adapter=resolve_adapter(adapters,storage_id,relative)
if not adapter:errors.append(f"share {share_id}: unknown storage {storage_id}");continue if not adapter:continue
prefix,mount=adapter;relative=source_path.strip("/") prefix,mount,_include_prefix=adapter
if prefix: if prefix:relative=relative[len(prefix):].lstrip("/")
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) source=contained_join(mount,relative)
if not mount.is_mount():errors.append(f"share {share_id}: storage mount unavailable: {mount}");continue if not mount.is_mount():errors.append(f"share {share_id}: storage mount unavailable: {mount}");continue
if not item_type: if not item_type:
@@ -97,7 +115,7 @@ def build(args: argparse.Namespace) -> dict[str,object]:
if "\t" in source_text or "\n" in source_text or "\r" in source_text:errors.append(f"share {share_id}: source path contains unsupported control characters");continue if "\t" in source_text or "\n" in source_text or "\r" in source_text:errors.append(f"share {share_id}: source path contains unsupported control characters");continue
manifest.append(f"{share_id}\t{item_type}\t{display_name.lstrip('/')}\t{source}") manifest.append(f"{share_id}\t{item_type}\t{display_name.lstrip('/')}\t{source}")
if errors:raise RuntimeError("; ".join(errors)) if errors:raise RuntimeError("; ".join(errors))
if len(manifest)!=len(rows):raise RuntimeError("not all Showcase shares could be resolved") if not manifest and rows and not args.allow_empty:raise RuntimeError("no Showcase shares match the selected directories")
args.output.parent.mkdir(parents=True,exist_ok=True) args.output.parent.mkdir(parents=True,exist_ok=True)
with tempfile.NamedTemporaryFile("w",encoding="utf-8",dir=args.output.parent,delete=False) as handle: 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) handle.write("\n".join(manifest)+("\n" if manifest else ""));temporary=Path(handle.name)
@@ -112,4 +130,4 @@ def main() -> int:
return 0 return 0
if __name__=="__main__":raise SystemExit(main()) if __name__=="__main__":raise SystemExit(main())