#!/usr/bin/env python3 """ OpenSim Library Generator - Modernized Version Features: - Type hints throughout - pathlib for path handling - argparse instead of optparse - Secure path traversal protection - Context managers for file operations - Configurable file type mappings - Proper logging system - XML security improvements """ """ ### **OpenSim Library Generator - Modernisierte Version** **Was dieses Skript macht:** Erstellt eine Bibliothek für OpenSim (eine virtuelle Welt) mit Dateien und Ordnern. 👉 *Für absolute Anfänger: Stell dir vor, du baust einen Katalog für digitale Objekte wie Bilder, Notizen und Skripte.* --- ### **Wichtige Funktionen (einfach erklärt)** | Funktion | Erklärung | |----------|-----------| | **Type hints** | Sagt dem Code, welche Art von Daten (Zahlen, Text etc.) erwartet wird → weniger Fehler | | **pathlib** | Moderner Weg, mit Dateipfaden umzugehen (besser als `os.path`) | | **argparse** | Verarbeitet Eingaben von der Kommandozeile (z. B. `--name MeineBibliothek`) | | **Sicherheit** | Schützt vor Hackern, die bösartige Dateipfade eingeben könnten | | **Context Manager** | Macht Dateioperationen (Öffnen/Schließen) sicherer → keine vergessenen offenen Dateien | | **Logging** | Schreibt Protokolle, was passiert (gut für Fehlersuche!) | --- ### **Wie die Haupt-Klassen arbeiten** #### **1. `AssetSet` – Verwaltet digitale Objekte** - **Aufgabe**: Speichert Infos über Dateien (z. B. Texturen, Skripte). - **Wichtige Methoden**: - `addasset()`: Fügt neue Datei hinzu (mit Name, Typ, UUID). - `findbyfilename()`: Sucht Datei nach ihrem Namen. #### **2. `InvFolders` – Verwaltet Ordner** - **Aufgabe**: Erstellt die Ordner-Struktur in der Bibliothek. - **Wichtige Methoden**: - `ensureexists()`: Erstellt Ordner, falls noch nicht da. #### **3. `InvItems` – Verwaltet Objekte in Ordnern** - **Aufgabe**: Verknüpft Dateien (`AssetSet`) mit Ordnern (`InvFolders`). - **Wichtige Methoden**: - `ensureexists()`: Legt ein Objekt im richtigen Ordner ab. --- ### **Wie man das Skript benutzt** **Beispiel-Befehl**: ```bash python library_generator.py --lib-name "Meine Bibliothek" --short-name meine_bib ``` **Optionen**: - `--wipe-inventory`: Lösche alte Bibliothek und starte neu. - `--asset-dir`: Pfad zu den Dateien (z. B. Bilder/Skripte). - `--skip`: Überspringe bestimmte Dateien (z. B. `.git`). --- ### **Für Neulinge: Wichtige Konzepte** | Begriff | Erklärung | |---------|-----------| | **UUID** | Eindeutige ID für jedes Objekt (wie ein Barcode) | | **Asset** | Digitale Datei (z. B. `.j2k` = Bild, `.lsl` = Skript) | | **Inventory** | "Bibliotheks-Katalog" mit Ordnern und Objekten | | **Nini XML** | Dateiformat für OpenSim-Bibliotheken | --- ### **Fehlerbehandlung** - Das Skript **meldet Fehler** sofort (z. B. ungültige Dateien). - **Automatische Backups**: Bevor etwas überschrieben wird, gibt es eine Sicherungskopie. --- ### **Beispiel: So wird eine Datei hinzugefügt** 1. Benutzer gibt ein: ```bash python library_generator.py --lib-name "Meine Bilder" --short-name bilder ``` 2. Skript: - Sucht alle `.j2k`-Dateien im Ordner `assets/bilderAssetSet`. - Erstellt einen Eintrag im "Katalog" (`AssetSet`). - Legt die Datei in den richtigen Ordner (`InvFolders` + `InvItems`). """ import argparse # Für das Lesen von Befehlszeilen-Argumenten (z.B. --input datei.txt) import logging # Zum Schreiben von Log-Dateien (Fehlerprotokollierung) import re # Für reguläre Ausdrücke (Mustererkennung in Texten) import shutil # Für Dateioperationen (Kopieren, Verschieben von Dateien/Ordnern) import stat # Für Dateiberechtigungen (Lesen/Schreiben/Execute-Rechte) import sys # Für Systemfunktionen (z.B. Programm beenden) import traceback # Für detaillierte Fehlermeldungen import uuid # Zum Erzeugen einzigartiger IDs (z.B. für Objekte) import xml.dom.minidom # Zum Lesen/Schreiben von XML-Dateien from contextlib import contextmanager # Für sichere Ressourcenverwaltung (z.B. automatisches Schließen von Dateien) from pathlib import Path # Moderner Weg mit Dateipfaden zu arbeiten from typing import ( # Für Typ-Hinweise (macht Code lesbarer, aber optional): Dict, # Für Wörterbücher {key: value} List, # Für Listen [item1, item2] Optional, # Für Variablen die None sein können Tuple, # Für Tupel (unveränderliche Listen) Iterator, # Für wiederholbare Objekte Any # Für beliebige Datentypen ) # Konstanten # Eindeutige ID für den Wurzelordner der OpenSim-Bibliothek OPENSIM_LIBRARY_ROOT_UUID = "00000112-000f-0000-0000-000100bba000" # Standard-Berechtigungen (Vollzugriff in Hex: 0x7FFFFFFF) DEFAULT_PERMISSIONS = "2147483647" # Kennzeichnet Ordner ohne Icon FOLDER_TYPE_NO_ICON = -1 # Logging einrichten: # - Level: INFO (zeigt wichtige Meldungen) # - Format: "Zeit - Level - Nachricht" # - Ausgabe: Fehlerausgabe (sys.stderr) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[logging.StreamHandler(sys.stderr)] ) # Logger-Objekt für dieses Modul erstellen logger = logging.getLogger(__name__) class AssetExistsException(Exception): """Wird geworfen, wenn ein Asset bereits existiert""" def __init__(self, value: str): self.value = value # Speichert die Fehlermeldung super().__init__(value) # Ruft Basisklassen-Konstruktor auf def __str__(self) -> str: return repr(self.value) # Gibt Fehlermeldung formatiert zurück class InventoryException(Exception): """Wird bei Inventar-Fehlern geworfen""" # (Aufbau identisch zu AssetExistsException) def __init__(self, value: str): self.value = value super().__init__(value) def __str__(self) -> str: return repr(self.value) class NiniSection: """Repräsentiert einen Abschnitt in einer Nini-XML-Datei""" def __init__(self, sectionname: str, keyvalues: Dict[str, str]): # Initialisiert mit: self._sectionname = sectionname # Abschnittsname self._keyvalues = keyvalues # Schlüssel-Wert-Paare als Dictionary # Eigenschaften (Properties): @property def name(self) -> str: return self._sectionname # Gibt Abschnittsnamen zurück @property def keyvalues(self) -> Dict[str, str]: return self._keyvalues.copy() # Kopie der Schlüssel-Wert-Paare # Methoden: def value(self, key: str) -> str: """Gibt Wert für einen Schlüssel zurück (oder leeren String)""" return self._keyvalues.get(key, "") def setvalue(self, key: str, value: str) -> None: """Setzt einen neuen Wert für einen Schlüssel""" self._keyvalues[key] = str(value) # Wert wird immer als String gespeichert class NiniException(Exception): """Spezialfehler für Nini-XML-Probleme""" # (Aufbau identisch zu AssetExistsException) def __init__(self, value: str): self.value = value super().__init__(value) def __str__(self) -> str: return repr(self.value) class NiniThing: """Basisklasse zum Lesen/Schreiben von Nini-XML-Dateien""" def __init__(self, keynames: List[str], ninitype: str): # Schlüsselnamen, die in der XML-Datei erwartet werden self.keynames = keynames # Typbeschreibung (für Fehlermeldungen) self.ninitype = ninitype def readXML(self, filename: Path) -> List[NiniSection]: """Liest eine Nini-XML-Datei und gibt Abschnitte zurück""" sections = [] # Hier kommen die geparsten Daten rein # Datei existiert nicht? → Warnung + leere Liste if not filename.exists(): logger.warning("%s-Datei %s existiert nicht!", self.ninitype, filename) return sections try: # Datei öffnen und parsen with filename.open('r', encoding='utf-8') as fh: xmlblob = xml.dom.minidom.parse(fh) # XML in Speicher laden # Prüfe Root-Element topelem = xmlblob.documentElement if topelem.tagName != "Nini": raise NiniException(f"{filename} ist keine Nini-Datei!") # Alle Abschnitte durchgehen for sec in topelem.childNodes: if sec.nodeType == xml.dom.Node.ELEMENT_NODE: if sec.tagName != 'Section': raise NiniException(f"Abschnitt '{sec.tagName}' ungültig!") # Neuen Abschnitt erstellen item = NiniSection(sec.getAttribute("Name"), {}) # Schlüssel mit Leerwerten vorbelegen for keyname in self.keynames: item.setvalue(keyname, "") # Schlüssel-Werte-Paare extrahieren for seckey in sec.childNodes: if seckey.nodeType == xml.dom.Node.ELEMENT_NODE: if seckey.tagName != "Key": raise NiniException(f"Schlüssel '{seckey.tagName}' ungültig!") item.setvalue( seckey.getAttribute("Name"), seckey.getAttribute("Value") ) sections.append(item) except Exception as e: raise NiniException(f"Fehler beim Lesen von {filename}: {str(e)}") finally: xmlblob.unlink() # Speicher freigeben return sections def writeXML(self, filename: Path, sections: List[NiniSection]) -> None: """Schreibt Daten in Nini-XML-Datei""" try: with filename.open('w', encoding='utf-8') as ofh: ofh.write("\n") for item in sections: # Abschnitt-Header ofh.write(f'
\n') # Alle Schlüssel-Werte-Paare for keyname in self.keynames: ofh.write( f' \n' ) ofh.write("
\n\n") ofh.write("
\n") except OSError as e: raise NiniException(f"Fehler beim Schreiben von {filename}: {str(e)}") @staticmethod def _escape_xml(text: str) -> str: """Ersetzt Sonderzeichen durch XML-Entities""" return (text.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """)) class AssetSet(NiniThing): """Verwaltet Asset-Metadaten (Dateien/Objekte)""" def __init__(self): # Initialisiert mit Standard-Schlüsseln für Assets super().__init__( ["assetID", "name", "assetType", "inventoryType", "fileName"], "Asset Set" ) self.assets = [] # Liste aller Assets def __iter__(self): return iter(self.assets) # Ermöglicht for-Schleifen über Assets def addasset(self, uuid_str: str, name: str, asstype: int, invtype: int, filename: str) -> NiniSection: """Fügt ein neues Asset hinzu""" ass = NiniSection( filename, { 'assetID': uuid_str, 'name': name, 'assetType': str(asstype), 'inventoryType': str(invtype), 'fileName': filename } ) self.assets.append(ass) return ass def findbyfilename(self, filename: str) -> Optional[NiniSection]: """Sucht Asset nach Dateiname""" return next((a for a in self.assets if a.value('fileName') == filename), None) def findbyuuid(self, uuid_str: str) -> Optional[NiniSection]: """Sucht Asset nach UUID""" return next((a for a in self.assets if a.value('assetID') == uuid_str), None) class InvFolders(NiniThing): """Verwaltet die Ordnerstruktur im Inventar""" def __init__(self): # Initialisiert mit Standard-Schlüsseln für Ordner super().__init__( ["folderID", "parentFolderID", "name", "type"], # XML-Schlüssel "Inventory Folders" # Typbezeichnung ) self.folders = [] # Liste aller Ordner def __iter__(self): return iter(self.folders) # Ermöglicht for-Schleifen über Ordner def findbyuuid(self, uuid_str: str) -> Optional[NiniSection]: """Findet Ordner anhand seiner UUID""" return next((f for f in self.folders if f.value('folderID') == uuid_str), None) def findbyname(self, name: str, parentid: str) -> Optional[NiniSection]: """Findet Ordner anhand von Name und Eltern-Ordner""" return next( (f for f in self.folders if f.value('parentFolderID') == parentid and f.value('name') == name), None ) def ensureexists(self, path: str, name: str, parentid: str) -> NiniSection: """Erstellt Ordner falls nicht vorhanden""" # Prüfe ob Eltern-Ordner existiert (außer Root) if parentid != OPENSIM_LIBRARY_ROOT_UUID and not self.findbyuuid(parentid): raise InventoryException(f"Eltern-Ordner existiert nicht: {parentid}") # Falls Ordner existiert: gib zurück if (folder := self.findbyname(name, parentid)) is not None: return folder # Neuen Ordner erstellen folder = NiniSection( path, { 'folderID': str(uuid.uuid4()), # Neue UUID generieren 'parentFolderID': parentid, 'name': name, 'type': str(FOLDER_TYPE_NO_ICON) # Ohne spezielles Icon } ) self.folders.append(folder) return folder class InvItems(NiniThing): """Verwaltet Objekte in Inventar-Ordnern""" def __init__(self, folders: InvFolders): # Initialisiert mit Standard-Schlüsseln für Objekte super().__init__( [ "inventoryID", "assetID", "folderID", "name", "description", "assetType", "inventoryType", "currentPermissions", "nextPermissions", "everyonePermissions", "basePermissions" ], "Inventory Items" ) self.folders = folders # Referenz zur Ordnerstruktur self.items = [] # Liste aller Objekte def findbyinvid(self, uuid_str: str) -> Optional[NiniSection]: """Findet Objekt anhand Inventar-ID""" return next((i for i in self.items if i.value('inventoryID') == uuid_str), None) def findbyname(self, name: str, folderid: str) -> Optional[NiniSection]: """Findet Objekt anhand Namen im Ordner""" return next( (i for i in self.items if i.value('name') == name and i.value('folderID') == folderid), None ) def ensureexists(self, path: str, name: str, assetid: str, assettype: int, invtype: int, folderid: str) -> NiniSection: """Erstellt Objekt falls nicht vorhanden""" # Prüfe ob Zielordner existiert if folderid != OPENSIM_LIBRARY_ROOT_UUID and not self.folders.findbyuuid(folderid): raise InventoryException(f"Zielordner existiert nicht: {folderid}") # Falls Objekt existiert: gib zurück existing = next( (i for i in self.items if (i.value('assetID') == assetid and i.value('folderID') == folderid and i.value('name') == name)), None ) if existing: return existing # Neues Objekt erstellen item = NiniSection( path, { 'inventoryID': str(uuid.uuid4()), 'assetID': assetid, 'folderID': folderid, 'name': name, 'description': "", 'assetType': str(assettype), 'inventoryType': str(invtype), 'currentPermissions': DEFAULT_PERMISSIONS, # Vollzugriff 'nextPermissions': DEFAULT_PERMISSIONS, 'everyonePermissions': DEFAULT_PERMISSIONS, 'basePermissions': DEFAULT_PERMISSIONS } ) self.items.append(item) return item class LibraryGenerator: """ Hauptklasse zur Generierung von OpenSim-Bibliotheken. Verarbeitet Dateiverzeichnisse und erstellt die benötigten XML-Dateien: - AssetSet.xml: Enthält alle Dateimetadaten - InvFolders.xml: Definiert die Ordnerstruktur - InvItems.xml: Verknüpft Assets mit Inventarordnern """ def __init__(self, libname: str, shortname: str): """ Initialisiert den Bibliotheks-Generator. Args: libname: Anzeigename der Bibliothek (z.B. "Meine Texturen") shortname: Kurzname für Dateien (ohne Leerzeichen/Sonderzeichen) """ # Grundkonfiguration self.libname = libname # Anzeigename der Bibliothek self.shortname = shortname # Basis für Dateinamen # Pfadkonfiguration self.asset_dir = Path("assets") / f"{shortname}AssetSet" # Quelle: Hier liegen die Asset-Dateien self.inv_dir = Path("inventory") / shortname # Ziel: Hier werden XML-Dateien gespeichert # Dateinamen der Output-XMLs self.asset_xmlfile = f"{shortname}AssetSet.xml" # Asset-Metadaten self.folders_xmlfile = f"{shortname}InvFolders.xml" # Ordnerhierarchie self.items_xmlfile = f"{shortname}InvItems.xml" # Inventareinträge # Systemkonfiguration self.skips = [r'^\.hg$', r'^\.git$', r'^\.svn$'] # Zu ignorierende Ordner (Regex) self.wipeinv = False # True = Existierendes Inventar löschen vor Neuerstellung # Dateityp-Mapping (Erweiterung -> (assetType, inventoryType)) self.file_types = { '.j2k': (0, 0), # 0 = Texture, 0 = Texture '.txt': (7, 7), # 7 = Notecard, 7 = Notecard '.lsl': (10, 10) # 10 = LSL Text, 10 = Script } # RegEx-Patterns für die Verarbeitung self.endsinslash = re.compile(r'/$') # Erkennung von Pfaden mit Endslash self.commentline = re.compile(r'^\s*#') # Erkennung von Kommentarzeilen self.blankline = re.compile(r'^\s*$') # Erkennung von Leerzeilen self.assetlistline = re.compile( r'^\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})' r'\s*(\d+)\s*(\d+)\s*(.*\S)\s*$' ) # Pattern für addassets.lis-Zeilen (UUID, AssetType, InvType, Name) def set_skip_patterns(self, skips: List[str]) -> None: """ Setzt zusätzliche Ignorier-Muster für Dateien/Ordner. Args: skips: Liste von Regex-Patterns (z.B. [r'^temp_', r'\.bak$']) """ self.skips = skips def set_asset_dir(self, asset_dir: Path) -> None: """ Setzt ein benutzerdefiniertes Asset-Quellverzeichnis. Args: asset_dir: Pfad zum Verzeichnis mit den Asset-Dateien """ self.asset_dir = Path(asset_dir) def set_inv_dir(self, inv_dir: Path) -> None: """ Setzt ein benutzerdefiniertes Zielverzeichnis für die XML-Dateien. Args: inv_dir: Pfad zum Output-Verzeichnis """ self.inv_dir = Path(inv_dir) def wipe_inventory(self) -> None: """Aktiviert den Wipe-Modus (löscht bestehende Inventardaten vor Neuerstellung).""" self.wipeinv = True @contextmanager def _backup_file(self, filepath: Path) -> Iterator[None]: """ Context Manager für sichere Dateioperationen mit Backup. Erstellt vor Änderungen eine Kopie mit '-old' Suffix. Args: filepath: Zu sichernde Datei """ if filepath.exists(): backup_path = filepath.with_name(f"{filepath.name}-old") try: shutil.copy2(filepath, backup_path) logger.info("Backup erstellt: %s", backup_path) except OSError as e: logger.error("Backup fehlgeschlagen für %s: %s", filepath, e) raise yield logger.info("Datei gespeichert: %s", filepath) def run(self) -> None: """ Hauptmethode zur Bibliotheksgenerierung. Durchläuft den kompletten Workflow: 1. Initialisierung 2. Laden existierender Daten (falls nicht im Wipe-Modus) 3. Verarbeitung der Verzeichnisstruktur 4. Speicherung der Ergebnisse """ logger.info("Starte Generierung für Bibliothek '%s'", self.libname) # Datenstrukturen initialisieren self.assets = AssetSet() self.folders = InvFolders() self.items = InvItems(self.folders) # Existierende Daten laden (falls gewünscht) asset_xml_path = self.asset_dir / self.asset_xmlfile if asset_xml_path.exists(): self.assets.readXML(asset_xml_path) if not self.wipeinv: folders_xml_path = self.inv_dir / self.folders_xmlfile items_xml_path = self.inv_dir / self.items_xmlfile if folders_xml_path.exists(): self.folders.readXML(folders_xml_path) if items_xml_path.exists(): self.items.readXML(items_xml_path) # Verzeichnisstruktur verarbeiten self._process_directory(OPENSIM_LIBRARY_ROOT_UUID, "", "") # Ergebnisse mit Backup-Mechanismus speichern try: with self._backup_file(asset_xml_path): self.assets.writeXML(asset_xml_path) with self._backup_file(self.inv_dir / self.folders_xmlfile): self.folders.writeXML(self.inv_dir / self.folders_xmlfile) with self._backup_file(self.inv_dir / self.items_xmlfile): self.items.writeXML(self.inv_dir / self.items_xmlfile) logger.info("Bibliotheksgenerierung erfolgreich abgeschlossen") except Exception as e: logger.error("Fehler beim Speichern: %s", e) raise def _should_skip(self, path: Path) -> bool: """ Prüft, ob ein Datei/Ordner ignoriert werden soll. Args: path: Zu prüfender Pfad Returns: True wenn übersprungen werden soll """ if any(re.search(pattern, path.name) for pattern in self.skips): return True if path.is_symlink(): logger.warning("Symlink wird übersprungen: %s", path) return True return False def _detect_asset_type(self, filename: str) -> Optional[Tuple[int, int]]: """ Ermittelt den Asset-Typ anhand der Dateiendung. Args: filename: Dateiname Returns: Tuple (assetType, inventoryType) oder None wenn unbekannt """ ext = Path(filename).suffix.lower() return self.file_types.get(ext) def _process_directory(self, parent_uuid: str, dir_name: str, rel_path: str) -> None: """ Verarbeitet ein Verzeichnis rekursiv. Args: parent_uuid: UUID des Elternordners dir_name: Name des aktuellen Verzeichnisses rel_path: Relativer Pfad vom Wurzelverzeichnis """ abs_path = self.asset_dir / rel_path if rel_path else self.asset_dir logger.info("Verarbeite Verzeichnis: %s", abs_path) try: # Ordner im Inventar anlegen/abrufen folder_name = dir_name if dir_name else self.libname folder_path = rel_path if rel_path else self.libname cur_folder = self.folders.ensureexists( folder_path, folder_name, parent_uuid ) folder_uuid = cur_folder.value("folderID") # Verzeichnisinhalt verarbeiten for entry in abs_path.iterdir(): if self._should_skip(entry): continue if entry.is_dir(): new_rel_path = str(Path(rel_path) / entry.name) if rel_path else entry.name self._process_directory(folder_uuid, entry.name, new_rel_path) continue self._process_file(entry, rel_path, folder_uuid) # Zusätzliche Assets aus addassets.lis verarbeiten addassets_path = abs_path / "addassets.lis" if addassets_path.exists(): self._process_addassets_file(addassets_path, rel_path, folder_uuid) except Exception as e: logger.error("Fehler bei %s: %s", abs_path, e) raise def _process_file(self, filepath: Path, rel_path: str, folder_uuid: str) -> None: """ Verarbeitet eine einzelne Datei. Args: filepath: Vollständiger Dateipfad rel_path: Relativer Pfad vom Asset-Root folder_uuid: Zielordner-UUID """ # Systemdateien überspringen if filepath.name == self.asset_xmlfile or filepath.name == "addassets.lis": return # Dateityp ermitteln file_type = self._detect_asset_type(filepath.name) if file_type is None: logger.warning("Unbekannter Dateityp: %s", filepath.name) return asset_type, inv_type = file_type rel_filepath = str(Path(rel_path) / filepath.name) if rel_path else filepath.name # Asset erstellen/abrufen name = filepath.stem # Dateiname ohne Erweiterung asset = self.assets.findbyfilename(rel_filepath) if asset is None: asset = self.assets.addasset( str(uuid.uuid4()), name, asset_type, asset_type, rel_filepath ) # Inventareintrag erstellen self.items.ensureexists( str(Path(rel_path) / name) if rel_path else name, name, asset.value('assetID'), asset_type, inv_type, folder_uuid ) def _process_addassets_file(self, filepath: Path, rel_path: str, folder_uuid: str) -> None: """ Verarbeitet eine addassets.lis-Datei mit manuellen Asset-Definitionen. Args: filepath: Pfad zur addassets.lis rel_path: Relativer Pfad vom Asset-Root folder_uuid: Zielordner-UUID """ logger.info("Verarbeite zusätzliche Assets aus: %s", filepath) try: with filepath.open('r', encoding='utf-8') as f: for line in f: line = line.rstrip("\r\n") if self.blankline.match(line) or self.commentline.match(line): continue match = self.assetlistline.match(line) if not match: logger.warning("Ungültige Zeile in %s: %s", filepath, line) continue uuid_str, asset_type, inv_type, name = match.groups() self.items.ensureexists( str(Path(rel_path) / name) if rel_path else name, name, uuid_str, int(asset_type), int(inv_type), folder_uuid ) except OSError as e: logger.error("Lesefehler bei %s: %s", filepath, e) raise def main() -> int: """ Haupt-Einstiegspunkt für die Kommandozeile. Verarbeitet Argumente und startet die Bibliotheksgenerierung. Returns: int: Exit-Code (0 bei Erfolg, 20 bei Fehlern) """ # Argumentparser initialisieren parser = argparse.ArgumentParser( description="OpenSim Bibliotheks-Generator", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""\ Dokumentation: http://opensimulator.org/wiki/Custom_Libraries Nach der Generierung müssen manuell folgende Schritte durchgeführt werden: 1. Asset-Verzeichnis in AssetSets.xml eintragen 2. Inventar-XMLs in Libraries.xml verlinken """ ) # Pflichtargumente parser.add_argument("-n", "--lib-name", required=True, help="Anzeigename der Bibliothek (z.B. 'Meine Texturen')") parser.add_argument("-s", "--short-name", required=True, help="Kurzname für Dateien (ohne Leerzeichen, z.B. 'texturen')") # Optionale Argumente parser.add_argument("-w", "--wipe-inventory", action="store_true", help="Existierendes Inventar vor Generierung löschen") parser.add_argument("-a", "--asset-dir", type=Path, help="Alternatives Asset-Verzeichnis (Standard: assets/[Kurzname]AssetSet)") parser.add_argument("-i", "--inv-dir", type=Path, help="Alternatives Zielverzeichnis für XMLs (Standard: inventory/[Kurzname])") parser.add_argument("--skip", action="append", help="Zusätzliche Regex-Muster für zu ignorierende Dateien/Ordner") parser.add_argument("--debug", action="store_true", help="Debug-Logging aktivieren (detaillierte Ausgaben)") args = parser.parse_args() try: # Logging konfigurieren logging.basicConfig( level=logging.DEBUG if args.debug else logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[logging.StreamHandler(sys.stderr)] # Ausgabe auf stderr ) # Generator initialisieren generator = LibraryGenerator(args.lib_name, args.short_name) # Optionen setzen if args.wipe_inventory: generator.wipe_inventory() # Löschmodus aktivieren if args.asset_dir: generator.set_asset_dir(args.asset_dir) # Custom Asset-Pfad if args.inv_dir: generator.set_inv_dir(args.inv_dir) # Custom Output-Pfad if args.skip: generator.set_skip_patterns(args.skip) # Ignorier-Muster hinzufügen # Generierung starten generator.run() return 0 # Erfolg except Exception as e: logger.critical( "Schwerer Fehler: %s%s", e, traceback.format_exc() if args.debug else "" ) return 20 # Fehlercode if __name__ == "__main__": sys.exit(main()) # Exit-Code an Shell zurückgeben