Generic Joomla 3.x manifest -> deployed-path mapper (component/module/plugin/ library/template/package), used to author the pristine (upstream) commit of a packages/<slug> module before it is reconciled to the live bytes. 18/18 tests green; ruff/mypy-strict clean. Signed-off-by: LÁZÁR Imre <imre@illusion.hu> Assisted-by: claude-code@claude-opus-4-8
558 lines
23 KiB
Python
558 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
"""Joomla 3.x manifest -> deployed-path mapper.
|
|
|
|
Generikus baseline-generátor komponens: egy kicsomagolt Joomla-bővítmény-
|
|
csomagot (manifest-XML + a hozzá tartozó forrásfák) a Joomla-installer
|
|
mapping-szabályai szerint másolja a megfelelő deployed docroot-útvonalakra.
|
|
|
|
Támogatott ``<extension type="...">`` gyökér-típusok: component, module,
|
|
plugin, library, template, language, file, package (pkg_ csomag, rekurzívan
|
|
feldolgozott al-bővítményekkel).
|
|
|
|
A ``folder`` attribútum (``<files folder="...">``, ``<media folder="...">``,
|
|
``<languages folder="...">``) a manifest-fájl könyvtárához relatív forrás-
|
|
alkönyvtárat jelöli; ha nincs megadva, a manifest melletti könyvtár a forrás.
|
|
|
|
Csak a Python standard library-t használja (nincs külső dependency).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
class JoomlaManifestError(Exception):
|
|
"""A manifest-feldolgozás vagy a mapping-szabályok megsértése esetén dobott hiba.
|
|
|
|
Minden hibás/hiányzó manifest-tartalom, ismeretlen extension-típus vagy
|
|
hiányzó forrás-fájl ezt dobja — a hívó CLI ezt explicit elkapja és
|
|
érthető hibaüzenettel, nemnulla exit-kóddal fejezi be a futást.
|
|
"""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CopyOp:
|
|
"""Egyetlen másolási művelet: forrás -> cél. A src lehet fájl vagy könyvtár."""
|
|
|
|
src: Path
|
|
dest: Path
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# XML-segédfüggvények
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _text(elem: ET.Element | None) -> str:
|
|
"""Egy elem trimmelt szöveg-tartalma, vagy üres string, ha nincs elem/szöveg."""
|
|
if elem is None or elem.text is None:
|
|
return ""
|
|
return elem.text.strip()
|
|
|
|
|
|
def _slug(text: str) -> str:
|
|
"""Emberi-olvasható nevet gépi slug-gá normalizál (kisbetű, szóköz -> '_')."""
|
|
normalized = text.strip().lower()
|
|
return "_".join(normalized.split())
|
|
|
|
|
|
def _extension_name(root: ET.Element) -> str:
|
|
"""A bővítmény gépi-nevét adja vissza a gyökér ``element`` attribútumból,
|
|
vagy ennek hiányában a ``<name>`` elem szövegéből (slugolva).
|
|
"""
|
|
element_attr = root.get("element")
|
|
if element_attr:
|
|
return element_attr.strip()
|
|
name_text = _text(root.find("name"))
|
|
if not name_text:
|
|
raise JoomlaManifestError(
|
|
"nem határozható meg a bővítmény neve: hiányzik a <name> elem és az 'element' attribútum"
|
|
)
|
|
return _slug(name_text)
|
|
|
|
|
|
def _plugin_element(root: ET.Element) -> str:
|
|
"""A plugin element-slugját adja vissza.
|
|
|
|
Elsődleges forrás: ``<files>`` blokk ``<filename plugin="...">`` vagy
|
|
``<file plugin="...">`` gyereke (mindkét tag-alak előfordul a valós
|
|
csomagokban -- ez a Joomla-konvenció szerinti autoritatív érték). Ha ez
|
|
hiányzik, a ``<name>PLG_GROUP_ELEMENT</name>`` konvenció utolsó
|
|
'_'-tagját használja fallback-ként.
|
|
"""
|
|
files_elem = root.find("files")
|
|
if files_elem is not None:
|
|
for child in files_elem:
|
|
if child.tag not in ("filename", "file"):
|
|
continue
|
|
plugin_attr = child.get("plugin")
|
|
if plugin_attr:
|
|
return plugin_attr.strip()
|
|
name_text = _text(root.find("name"))
|
|
if name_text:
|
|
return _slug(name_text.rsplit("_", 1)[-1])
|
|
raise JoomlaManifestError(
|
|
'nem határozható meg a plugin element-je: nincs <filename plugin="..."> / '
|
|
'<file plugin="..."> és nincs <name>'
|
|
)
|
|
|
|
|
|
_COMPONENT_LANG_SYS_INI = re.compile(r"(com_[a-z0-9_]+)\.sys\.ini$", re.IGNORECASE)
|
|
_COMPONENT_SCRIPTFILE = re.compile(r"script\.([a-z0-9_]+)\.php$", re.IGNORECASE)
|
|
|
|
|
|
def _component_slug(text: str) -> str:
|
|
"""Komponens-display-nevet ('OneAll Social Login') Joomla-konvenció szerint
|
|
normalizál a végső fallback-ágon: szóköz-ELTÁVOLÍTÁS (nem aláhúzás-csere)
|
|
+ kisbetűsítés -> 'oneallsociallogin'.
|
|
"""
|
|
return "".join(text.strip().lower().split())
|
|
|
|
|
|
def _component_name(root: ET.Element) -> str:
|
|
"""A komponens deployed-elemnevét ('com_X') vezeti le a Joomla 3.x
|
|
installer-heurisztika prioritási sorrendjében:
|
|
|
|
1. explicit gyökér ``element`` attribútum
|
|
2. a fő ``<files folder="com_...">`` attribútuma, ha 'com_'-prefixű --
|
|
ez az autoritatív forrás, mert a ``<name>`` gyakran csak emberi
|
|
megjelenítő-cím (pl. "OneAll Social Login", nem "com_oneallsociallogin")
|
|
3. az adminisztratív, majd a site ``<languages><language>`` fájlnév-
|
|
mintázata ``en-GB.com_<elem>.sys.ini`` -> ``com_<elem>``
|
|
4. ``<scriptfile>install/script.<elem>.php</scriptfile>`` -> ``com_<elem>``
|
|
5. végső fallback: ``<name>`` Joomla-konvenció szerinti normalizálása
|
|
(szóköz-eltávolítás, kisbetűsítés); ha az eredmény már 'com_'-prefixű
|
|
(gépi-nevet tartalmazó manifest), változatlanul megy tovább, egyébként
|
|
'com_' prefixet kap.
|
|
"""
|
|
element_attr = root.get("element")
|
|
if element_attr:
|
|
return element_attr.strip()
|
|
|
|
files_elem = root.find("files")
|
|
if files_elem is not None:
|
|
folder_attr = files_elem.get("folder", "")
|
|
if folder_attr.lower().startswith("com_"):
|
|
return folder_attr
|
|
|
|
for languages_elem in (root.find("administration/languages"), root.find("languages")):
|
|
if languages_elem is None:
|
|
continue
|
|
for language_elem in languages_elem.findall("language"):
|
|
match = _COMPONENT_LANG_SYS_INI.search(_text(language_elem))
|
|
if match:
|
|
return match.group(1).lower()
|
|
|
|
scriptfile_text = _text(root.find("scriptfile"))
|
|
if scriptfile_text:
|
|
match = _COMPONENT_SCRIPTFILE.search(scriptfile_text)
|
|
if match:
|
|
return f"com_{match.group(1).lower()}"
|
|
|
|
name_text = _text(root.find("name"))
|
|
if name_text:
|
|
slug = _component_slug(name_text)
|
|
return slug if slug.startswith("com_") else f"com_{slug}"
|
|
|
|
raise JoomlaManifestError(
|
|
"nem határozható meg a komponens deployed-neve: nincs 'element' attribútum, "
|
|
'nincs <files folder="com_...">, nincs com_-mintázatú nyelvi fájl, '
|
|
"nincs <scriptfile>, és nincs <name>"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# <files> / <media> / <languages> blokk -> CopyOp lista
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _apply_files(files_elem: ET.Element | None, manifest_dir: Path, dest_dir: Path) -> list[CopyOp]:
|
|
"""Egy ``<files>``-szerű blokk (files/media) gyerekeit CopyOp-listává alakítja.
|
|
|
|
Az egyedi-fájl gyerek tag-neve a valós csomagokban ``<filename>`` VAGY
|
|
``<file>`` lehet (mindkettőt elfogadja a Joomla-installer, csak a szöveg-
|
|
tartalom és a ``target``/egyéb attribútumok számítanak); a ``<folder>``
|
|
gyerek egy teljes alfát másol. A ``folder`` attribútum a forrás-
|
|
alkönyvtárat jelöli a manifest könyvtárához képest; a ``target``
|
|
attribútum (ha van) felülírja a cél-relatív-útvonalat.
|
|
"""
|
|
if files_elem is None:
|
|
return []
|
|
folder_attr = files_elem.get("folder", "")
|
|
src_base = manifest_dir / folder_attr if folder_attr else manifest_dir
|
|
ops: list[CopyOp] = []
|
|
for child in files_elem:
|
|
if child.tag not in ("filename", "file", "folder"):
|
|
continue
|
|
rel_text = _text(child)
|
|
if not rel_text:
|
|
raise JoomlaManifestError(
|
|
f"<{child.tag}> elem üres útvonal-szöveggel a(z) {manifest_dir} manifest-ben"
|
|
)
|
|
rel_path = Path(rel_text)
|
|
target_attr = child.get("target")
|
|
dest_rel = Path(target_attr) if target_attr else rel_path
|
|
ops.append(CopyOp(src=src_base / rel_path, dest=dest_dir / dest_rel))
|
|
return ops
|
|
|
|
|
|
def _apply_media(media_elem: ET.Element | None, manifest_dir: Path, media_root: Path) -> list[CopyOp]:
|
|
"""``<media destination="..." folder="...">`` blokk -> ``media/<destination>/`` alá."""
|
|
if media_elem is None:
|
|
return []
|
|
destination = media_elem.get("destination")
|
|
if not destination:
|
|
raise JoomlaManifestError(
|
|
f"<media> elem hiányzó 'destination' attribútummal a(z) {manifest_dir} manifest-ben"
|
|
)
|
|
return _apply_files(media_elem, manifest_dir, media_root / destination)
|
|
|
|
|
|
def _apply_languages(languages_elem: ET.Element | None, manifest_dir: Path, lang_root: Path) -> list[CopyOp]:
|
|
"""``<languages folder="...">`` blokk -> ``<lang_root>/<tag>/<fájlnév>``.
|
|
|
|
A gyerek ``<language tag="...">relatív/út.ini</language>`` elemek szövege
|
|
a forrás-relatív útvonal; a cél a tag-alkönyvtárban a fájl alap-neve
|
|
(a Joomla-konvenció szerint a forrás már tag-alkönyvtárban van).
|
|
"""
|
|
if languages_elem is None:
|
|
return []
|
|
folder_attr = languages_elem.get("folder", "")
|
|
src_base = manifest_dir / folder_attr if folder_attr else manifest_dir
|
|
ops: list[CopyOp] = []
|
|
for language_elem in languages_elem.findall("language"):
|
|
tag = language_elem.get("tag")
|
|
if not tag:
|
|
raise JoomlaManifestError(
|
|
f"<language> elem hiányzó 'tag' attribútummal a(z) {manifest_dir} manifest-ben"
|
|
)
|
|
rel_text = _text(language_elem)
|
|
if not rel_text:
|
|
raise JoomlaManifestError(
|
|
f'<language tag="{tag}"> elem üres fájl-útvonallal a(z) {manifest_dir} manifest-ben'
|
|
)
|
|
rel_path = Path(rel_text)
|
|
ops.append(CopyOp(src=src_base / rel_path, dest=lang_root / tag / rel_path.name))
|
|
return ops
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Extension-típus -> mapping-szabály
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def map_component(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[CopyOp]:
|
|
"""com_X: files->components/com_X, administration/files->administrator/components/com_X,
|
|
media->media/com_X, languages(site+admin)->language/ ill. administrator/language/.
|
|
|
|
A manifestbe ágyazott ``<modules>``/``<plugins>`` szekciók (ha vannak --
|
|
pl. egy komponens saját manifestje telepít egy modult/plugint is, mint
|
|
az OneAll Social Login csomagnál) is feldolgozásra kerülnek; ez NEM a
|
|
``pkg_`` package-típus (annak külön al-manifestjei vannak), hanem a
|
|
komponens EGYETLEN manifestjébe ágyazott mapping-szekció.
|
|
"""
|
|
name = _component_name(root)
|
|
ops = _apply_files(root.find("files"), manifest_dir, out_root / "components" / name)
|
|
|
|
admin_elem = root.find("administration")
|
|
if admin_elem is not None:
|
|
ops += _apply_files(
|
|
admin_elem.find("files"), manifest_dir, out_root / "administrator" / "components" / name
|
|
)
|
|
ops += _apply_languages(
|
|
admin_elem.find("languages"), manifest_dir, out_root / "administrator" / "language"
|
|
)
|
|
|
|
ops += _apply_media(root.find("media"), manifest_dir, out_root / "media")
|
|
ops += _apply_languages(root.find("languages"), manifest_dir, out_root / "language")
|
|
ops += _apply_embedded_modules(root.find("modules"), manifest_dir, out_root)
|
|
ops += _apply_embedded_plugins(root.find("plugins"), manifest_dir, out_root)
|
|
return ops
|
|
|
|
|
|
def _apply_embedded_modules(
|
|
modules_elem: ET.Element | None, manifest_dir: Path, out_root: Path
|
|
) -> list[CopyOp]:
|
|
"""Egy komponens-manifestbe ágyazott ``<modules><module module="mod_X" client="...">``
|
|
szekció feldolgozása. A deployed-modulnév közvetlenül a ``module`` attribútumból jön
|
|
(nem a gyakran emberi-olvasható ``<name>``-ből).
|
|
"""
|
|
if modules_elem is None:
|
|
return []
|
|
ops: list[CopyOp] = []
|
|
for module_elem in modules_elem.findall("module"):
|
|
module_name = module_elem.get("module")
|
|
if not module_name:
|
|
raise JoomlaManifestError(f"beágyazott <module> hiányzó 'module' attribútummal: {manifest_dir}")
|
|
is_admin = module_elem.get("client") == "administrator"
|
|
base = out_root / ("administrator/modules" if is_admin else "modules") / module_name
|
|
ops += _apply_files(module_elem.find("files"), manifest_dir, base)
|
|
ops += _apply_media(module_elem.find("media"), manifest_dir, out_root / "media")
|
|
return ops
|
|
|
|
|
|
def _apply_embedded_plugins(
|
|
plugins_elem: ET.Element | None, manifest_dir: Path, out_root: Path
|
|
) -> list[CopyOp]:
|
|
"""Egy komponens-manifestbe ágyazott ``<plugins><plugin plugin="X" group="Y">``
|
|
szekció feldolgozása. Az elem-slug és a group közvetlenül a ``plugin``/``group``
|
|
attribútumokból jön (nem a gyakran emberi-olvasható ``<name>``-ből).
|
|
"""
|
|
if plugins_elem is None:
|
|
return []
|
|
ops: list[CopyOp] = []
|
|
for plugin_elem in plugins_elem.findall("plugin"):
|
|
element = plugin_elem.get("plugin")
|
|
group = plugin_elem.get("group")
|
|
if not element or not group:
|
|
raise JoomlaManifestError(
|
|
f"beágyazott <plugin> hiányzó 'plugin' vagy 'group' attribútummal: {manifest_dir}"
|
|
)
|
|
base = out_root / "plugins" / group / element
|
|
ops += _apply_files(plugin_elem.find("files"), manifest_dir, base)
|
|
ops += _apply_media(plugin_elem.find("media"), manifest_dir, out_root / "media")
|
|
return ops
|
|
|
|
|
|
def map_module(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[CopyOp]:
|
|
"""mod_X: files->modules/mod_X (vagy administrator/modules/mod_X ha client=administrator)."""
|
|
name = _extension_name(root)
|
|
is_admin = root.get("client") == "administrator"
|
|
base = out_root / ("administrator/modules" if is_admin else "modules") / name
|
|
ops = _apply_files(root.find("files"), manifest_dir, base)
|
|
ops += _apply_media(root.find("media"), manifest_dir, out_root / "media")
|
|
lang_root = out_root / ("administrator/language" if is_admin else "language")
|
|
ops += _apply_languages(root.find("languages"), manifest_dir, lang_root)
|
|
return ops
|
|
|
|
|
|
def map_plugin(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[CopyOp]:
|
|
"""plugin: files->plugins/<group>/<element>."""
|
|
group = root.get("group")
|
|
if not group:
|
|
raise JoomlaManifestError(f"plugin manifest hiányzó 'group' attribútummal: {manifest_dir}")
|
|
element = _plugin_element(root)
|
|
base = out_root / "plugins" / group / element
|
|
ops = _apply_files(root.find("files"), manifest_dir, base)
|
|
ops += _apply_media(root.find("media"), manifest_dir, out_root / "media")
|
|
ops += _apply_languages(root.find("languages"), manifest_dir, out_root / "language")
|
|
return ops
|
|
|
|
|
|
def map_library(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[CopyOp]:
|
|
"""library: files->libraries/<name>."""
|
|
name = _extension_name(root)
|
|
ops = _apply_files(root.find("files"), manifest_dir, out_root / "libraries" / name)
|
|
ops += _apply_media(root.find("media"), manifest_dir, out_root / "media")
|
|
return ops
|
|
|
|
|
|
def map_template(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[CopyOp]:
|
|
"""template: files->templates/<name> (vagy administrator/templates/<name>)."""
|
|
name = _extension_name(root)
|
|
is_admin = root.get("client") == "administrator"
|
|
base = out_root / ("administrator/templates" if is_admin else "templates") / name
|
|
ops = _apply_files(root.find("files"), manifest_dir, base)
|
|
ops += _apply_media(root.find("media"), manifest_dir, out_root / "media")
|
|
return ops
|
|
|
|
|
|
def map_language(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[CopyOp]:
|
|
"""language (nyelvi csomag): files -> language/<tag> (vagy administrator/language/<tag>)."""
|
|
tag = _text(root.find("tag")) or _slug(_text(root.find("name")))
|
|
if not tag:
|
|
raise JoomlaManifestError(f"language manifest hiányzó <tag> és <name> elemmel: {manifest_dir}")
|
|
is_admin = root.get("client") == "administrator"
|
|
lang_root = out_root / ("administrator/language" if is_admin else "language") / tag
|
|
return _apply_files(root.find("files"), manifest_dir, lang_root)
|
|
|
|
|
|
def map_file(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[CopyOp]:
|
|
"""type=file: a <files> gyerekek target-attribútuma (vagy a relatív útvonaluk)
|
|
közvetlenül a docroot gyökeréhez képest értelmezendő.
|
|
"""
|
|
return _apply_files(root.find("files"), manifest_dir, out_root)
|
|
|
|
|
|
def map_package(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[CopyOp]:
|
|
"""pkg_ csomag: a <files><file type="..." id="..."> bejegyzések al-csomag
|
|
alkönyvtárakra mutatnak; mindegyiket a saját manifestjével rekurzívan dolgozza fel.
|
|
"""
|
|
files_elem = root.find("files")
|
|
if files_elem is None:
|
|
raise JoomlaManifestError(f"package manifest hiányzó <files> elemmel: {manifest_dir}")
|
|
folder_attr = files_elem.get("folder", "")
|
|
src_base = manifest_dir / folder_attr if folder_attr else manifest_dir
|
|
|
|
ops: list[CopyOp] = []
|
|
for file_elem in sorted(files_elem.findall("file"), key=lambda e: (_text(e), e.get("id", ""))):
|
|
rel_text = _text(file_elem)
|
|
if not rel_text:
|
|
raise JoomlaManifestError(f"<file> al-csomag bejegyzés üres útvonal-szöveggel: {manifest_dir}")
|
|
sub_dir = src_base / rel_text
|
|
if not sub_dir.is_dir():
|
|
raise JoomlaManifestError(
|
|
f"az al-csomag könyvtára nem található: {sub_dir} (package manifest: {manifest_dir})"
|
|
)
|
|
sub_manifest = find_manifest(sub_dir)
|
|
ops += build_operations(sub_manifest, out_root)
|
|
return ops
|
|
|
|
|
|
TypeHandler = Callable[[ET.Element, Path, Path], list[CopyOp]]
|
|
|
|
TYPE_HANDLERS: dict[str, TypeHandler] = {
|
|
"component": map_component,
|
|
"module": map_module,
|
|
"plugin": map_plugin,
|
|
"library": map_library,
|
|
"template": map_template,
|
|
"language": map_language,
|
|
"file": map_file,
|
|
"package": map_package,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Manifest-keresés + dispatch + végrehajtás
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _is_extension_manifest(path: Path) -> bool:
|
|
"""True, ha a path egy jólformált XML, aminek gyökér-eleme <extension>."""
|
|
try:
|
|
# bandit B314: stdlib ElementTree XXE-re elméletileg sebezhető; a bemenet
|
|
# egy lokálisan kicsomagolt, adminisztrátor-szolgáltatta bővítmény-csomag
|
|
# (nem távoli/attacker-controlled forrás), és a feladat-kontraktus
|
|
# kifejezetten csak stdlib-et enged (defusedxml külső dependency lenne
|
|
# -> PO-jóváhagyás szükséges, jelen scope-ban nincs) — lásd záró-riport.
|
|
root = ET.parse(path).getroot() # nosec B314
|
|
except ET.ParseError:
|
|
return False
|
|
except OSError:
|
|
return False
|
|
return root.tag == "extension"
|
|
|
|
|
|
def find_manifest(directory: Path) -> Path:
|
|
"""A directory közvetlen szintjén (nem rekurzívan) megkeresi az egyetlen
|
|
Joomla extension-manifestet (<extension> gyökér-elemű XML-fájl).
|
|
|
|
Raises:
|
|
JoomlaManifestError: ha egy sincs, vagy ha egynél több található
|
|
(ez utóbbi esetben a mapping nem egyértelmű, a hívónak
|
|
szét kell választania a csomagot).
|
|
"""
|
|
candidates = sorted(
|
|
(p for p in directory.glob("*.xml") if _is_extension_manifest(p)),
|
|
key=lambda p: p.name,
|
|
)
|
|
if not candidates:
|
|
raise JoomlaManifestError(f"nem található Joomla extension-manifest itt: {directory}")
|
|
if len(candidates) > 1:
|
|
names = ", ".join(str(c) for c in candidates)
|
|
raise JoomlaManifestError(
|
|
f"több lehetséges manifest is található itt: {directory} ({names}); "
|
|
"egy csomag-könyvtárban pontosan egy gyökér-manifest várt"
|
|
)
|
|
return candidates[0]
|
|
|
|
|
|
def build_operations(manifest_path: Path, out_root: Path) -> list[CopyOp]:
|
|
"""A manifest-path-hoz tartozó extension teljes CopyOp-listáját építi fel
|
|
(a saját mapping-szabálya szerint; package-típus esetén rekurzívan az
|
|
al-bővítményekével együtt).
|
|
"""
|
|
try:
|
|
tree = ET.parse(manifest_path) # nosec B314 -- lásd _is_extension_manifest WHY-kommentje
|
|
except ET.ParseError as exc:
|
|
raise JoomlaManifestError(f"hibás XML a manifest-ben: {manifest_path} ({exc})") from exc
|
|
root = tree.getroot()
|
|
if root.tag != "extension":
|
|
raise JoomlaManifestError(f"{manifest_path}: a gyökér-elem <{root.tag}>, elvárt: <extension>")
|
|
ext_type = root.get("type")
|
|
if not ext_type:
|
|
raise JoomlaManifestError(f"{manifest_path}: <extension> hiányzó 'type' attribútummal")
|
|
handler = TYPE_HANDLERS.get(ext_type)
|
|
if handler is None:
|
|
supported = ", ".join(sorted(TYPE_HANDLERS))
|
|
raise JoomlaManifestError(
|
|
f"{manifest_path}: nem támogatott extension type '{ext_type}' (támogatott: {supported})"
|
|
)
|
|
return handler(root, manifest_path.parent, out_root)
|
|
|
|
|
|
def _copytree_merge(src: Path, dest: Path) -> None:
|
|
"""src könyvtárfát dest alá másolja rendezett bejárással, meglévő dest-et megtartva/bővítve."""
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
for entry in sorted(src.iterdir(), key=lambda p: p.name):
|
|
target = dest / entry.name
|
|
if entry.is_dir():
|
|
_copytree_merge(entry, target)
|
|
else:
|
|
shutil.copy2(entry, target)
|
|
|
|
|
|
def execute(ops: list[CopyOp]) -> None:
|
|
"""A CopyOp-listát ténylegesen végrehajtja: fájlokat/könyvtárfákat másol.
|
|
|
|
Raises:
|
|
JoomlaManifestError: ha egy forrás-útvonal nem létezik.
|
|
"""
|
|
for op in ops:
|
|
if not op.src.exists():
|
|
raise JoomlaManifestError(f"a forrás-útvonal nem létezik: {op.src}")
|
|
if op.src.is_dir():
|
|
_copytree_merge(op.src, op.dest)
|
|
else:
|
|
op.dest.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(op.src, op.dest)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""CLI belépőpont: ``python3 jmap.py --package <pkg_dir> --out <docroot_dir>``."""
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Joomla 3.x manifest-alapú bővítmény-deployer: kicsomagolt bővítmény-csomagot "
|
|
"másol a megfelelő deployed docroot-útvonalakra a Joomla-installer mapping-szabályai szerint."
|
|
)
|
|
)
|
|
parser.add_argument(
|
|
"--package", required=True, type=Path, help="kicsomagolt bővítmény-csomag gyökér-könyvtára"
|
|
)
|
|
parser.add_argument("--out", required=True, type=Path, help="cél deployed docroot könyvtár")
|
|
args = parser.parse_args(argv)
|
|
|
|
package_dir: Path = args.package
|
|
out_dir: Path = args.out
|
|
|
|
if not package_dir.is_dir():
|
|
parser.error(f"--package nem létező könyvtár: {package_dir}")
|
|
|
|
try:
|
|
manifest_path = find_manifest(package_dir)
|
|
ops = build_operations(manifest_path, out_dir)
|
|
execute(ops)
|
|
except JoomlaManifestError as exc:
|
|
print(f"hiba: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"kész: {len(ops)} másolási művelet végrehajtva -> {out_dir}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|