mirror of
https://github.com/Terranom674/Piwigo_Bratonien_Tools.git
synced 2026-09-19 19:54:31 +00:00
NC-Connector Verzeichnisfilter pro Storage ergaenzen
This commit is contained in:
@@ -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())
|
||||||
Reference in New Issue
Block a user