-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub_installer.py
More file actions
320 lines (263 loc) · 10.5 KB
/
Copy pathgithub_installer.py
File metadata and controls
320 lines (263 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
"""
github_installer.py — ProFiler Suite Module Installer
Lädt fehlende Module direkt aus GitHub Releases herunter und installiert
sie in die Geschwister-Verzeichnisstruktur neben dem ProFiler-Ordner.
Verwendung (CLI):
python github_installer.py list
python github_installer.py install prosync
python github_installer.py install-all
Verwendung (API):
from github_installer import install_module, list_modules, GITHUB_REPOS
"""
from __future__ import annotations
import json
import shutil
import sys
import tempfile
import urllib.error
import urllib.request
import zipfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Optional, Tuple
# ---------------------------------------------------------------------------
# GitHub-Repo-Konfiguration
# ---------------------------------------------------------------------------
# key -> (org, repo) oder None wenn kein Repo verfügbar
GITHUB_REPOS: Dict[str, Optional[Tuple[str, str]]] = {
"prosync": ("file-bricks", "ProSync"),
"sqliteviewer": ("file-bricks", "SQLiteViewer"),
"datenschutzampel": None, # kein öffentliches GitHub-Repo verifiziert (file-bricks hat nur AmpelClip)
"formconstructor": None, # kein Git-Repo vorhanden
"pythonbox": ("dev-bricks", "pythonbox"),
}
# Sibling-Verzeichnisnamen (identisch mit module_registry._KNOWN sibling_dir_hint)
_SIBLING_DIRS: Dict[str, str] = {
"prosync": "REL-PUB_ProSync",
"sqliteviewer": "REL-PUB_SQLiteViewer",
"datenschutzampel": "REL-PUB_Datenschutzampel",
"formconstructor": "REL-PUB_FormConstructor",
"pythonbox": "REL-PUB_PythonBox",
}
_GITHUB_API = "https://api.github.com"
_TIMEOUT = 30 # Sekunden
# ---------------------------------------------------------------------------
# Datenklassen
# ---------------------------------------------------------------------------
@dataclass
class ReleaseInfo:
tag_name: str
name: str
zipball_url: str
zip_asset_url: Optional[str] = None
zip_asset_name: Optional[str] = None
@dataclass
class InstallResult:
key: str
success: bool
installed_path: Optional[Path] = None
message: str = ""
error: Optional[str] = None
# ---------------------------------------------------------------------------
# GitHub-API-Abfragen
# ---------------------------------------------------------------------------
def fetch_latest_release(
org: str,
repo: str,
token: Optional[str] = None,
) -> Optional[dict]:
"""Ruft das neueste Release von GitHub ab. Gibt None zurück wenn keins existiert."""
url = f"{_GITHUB_API}/repos/{org}/{repo}/releases/latest"
headers = {"Accept": "application/vnd.github+json", "User-Agent": "ProFiler-Installer/1.0"}
if token:
headers["Authorization"] = f"Bearer {token}"
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None # Kein Release vorhanden
raise
except urllib.error.URLError:
raise
def find_zip_asset(release: dict) -> Tuple[str, Optional[str]]:
"""
Gibt (download_url, asset_name) zurück.
Bevorzugt einen expliziten .zip-Asset; fällt auf zipball_url zurück.
"""
for asset in release.get("assets", []):
name = asset.get("name", "")
if name.endswith(".zip"):
return asset["browser_download_url"], name
return release["zipball_url"], None
def parse_release(data: dict) -> ReleaseInfo:
zip_url, zip_name = find_zip_asset(data)
return ReleaseInfo(
tag_name=data.get("tag_name", ""),
name=data.get("name", ""),
zipball_url=data.get("zipball_url", ""),
zip_asset_url=zip_url,
zip_asset_name=zip_name,
)
# ---------------------------------------------------------------------------
# Download und Extraktion
# ---------------------------------------------------------------------------
def download_file(url: str, dest: Path, token: Optional[str] = None) -> None:
"""Lädt eine Datei von url nach dest herunter."""
headers = {"User-Agent": "ProFiler-Installer/1.0"}
if token:
headers["Authorization"] = f"Bearer {token}"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
with open(dest, "wb") as f:
shutil.copyfileobj(resp, f)
def extract_zip_to_sibling(zip_path: Path, sibling_dir: Path) -> None:
"""
Entpackt zip_path in sibling_dir.
GitHub-ZIPs enthalten typischerweise einen Unterordner (repo-tag/),
dessen Inhalt direkt in sibling_dir landet.
"""
sibling_dir.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(zip_path, "r") as zf:
names = zf.namelist()
# Gemeinsames Präfix ermitteln (GitHub erzeugt repo-tag/ als Top-Level)
prefix = ""
if names:
first_parts = names[0].split("/")
if len(first_parts) > 1:
candidate = first_parts[0] + "/"
if all(n.startswith(candidate) for n in names):
prefix = candidate
for member in names:
member_path = member[len(prefix):] # Präfix abschneiden
if not member_path:
continue
dest = sibling_dir / member_path
if member.endswith("/"):
dest.mkdir(parents=True, exist_ok=True)
else:
dest.parent.mkdir(parents=True, exist_ok=True)
with zf.open(member) as src, open(dest, "wb") as out:
shutil.copyfileobj(src, out)
# ---------------------------------------------------------------------------
# Haupt-Installer
# ---------------------------------------------------------------------------
def install_module(
key: str,
parent_dir: Optional[Path] = None,
token: Optional[str] = None,
) -> InstallResult:
"""
Installiert ein Modul aus dem GitHub-Release in das Geschwister-Verzeichnis.
Args:
key: Modul-Schlüssel (z. B. "prosync")
parent_dir: Übergeordnetes Verzeichnis für die Sibling-Struktur.
Standard: Elternordner von github_installer.py
token: Optionales GitHub-API-Token für höhere Rate-Limits
Returns:
InstallResult mit Ergebnis und ggf. Fehlermeldung
"""
if key not in GITHUB_REPOS:
return InstallResult(key=key, success=False, error=f"Unbekannter Modul-Schlüssel: {key!r}")
repo_info = GITHUB_REPOS[key]
if repo_info is None:
return InstallResult(
key=key,
success=False,
message=f"Modul '{key}' hat kein GitHub-Repository. Bitte manuell installieren.",
)
org, repo = repo_info
if parent_dir is None:
parent_dir = Path(__file__).parent.parent
sibling_name = _SIBLING_DIRS.get(key, f"REL-PUB_{key.capitalize()}")
sibling_dir = parent_dir / sibling_name
try:
release_data = fetch_latest_release(org, repo, token=token)
except Exception as exc:
return InstallResult(key=key, success=False, error=f"Netzwerkfehler: {exc}")
if release_data is None:
return InstallResult(
key=key,
success=False,
message=f"Kein Release für {org}/{repo} gefunden. Bitte manuell installieren.",
)
release = parse_release(release_data)
download_url = release.zip_asset_url or release.zipball_url
with tempfile.TemporaryDirectory() as tmp:
zip_path = Path(tmp) / "module.zip"
try:
download_file(download_url, zip_path, token=token)
except Exception as exc:
return InstallResult(key=key, success=False, error=f"Download-Fehler: {exc}")
try:
extract_zip_to_sibling(zip_path, sibling_dir)
except Exception as exc:
return InstallResult(key=key, success=False, error=f"Extraktions-Fehler: {exc}")
return InstallResult(
key=key,
success=True,
installed_path=sibling_dir,
message=f"Installiert: {org}/{repo} {release.tag_name} → {sibling_dir}",
)
def install_all(parent_dir: Optional[Path] = None, token: Optional[str] = None) -> list:
"""Installiert alle Module mit verfügbarem GitHub-Repo."""
results = []
for key in GITHUB_REPOS:
print(f" Installiere {key}...")
result = install_module(key, parent_dir=parent_dir, token=token)
results.append(result)
if result.success:
print(f" ✓ {result.message}")
else:
msg = result.error or result.message
print(f" ✗ {msg}")
return results
def list_modules(parent_dir: Optional[Path] = None) -> None:
"""Gibt eine Statusübersicht aller Module aus."""
if parent_dir is None:
parent_dir = Path(__file__).parent.parent
print("ProFiler Suite — Modul-Status")
print("=" * 50)
for key, repo_info in GITHUB_REPOS.items():
sibling_name = _SIBLING_DIRS.get(key, f"REL-PUB_{key.capitalize()}")
sibling_dir = parent_dir / sibling_name
installed = "✓" if sibling_dir.exists() else "✗"
if repo_info:
org, repo = repo_info
github = f"github.com/{org}/{repo}"
else:
github = "(kein Repo)"
print(f" {installed} {key:<18} {sibling_name:<25} {github}")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def _cli(args: list) -> int:
if not args or args[0] in ("-h", "--help"):
print(__doc__)
return 0
cmd = args[0]
if cmd == "list":
list_modules()
return 0
if cmd == "install" and len(args) >= 2:
key = args[1]
print(f"Installiere Modul '{key}'...")
result = install_module(key)
if result.success:
print(f"Erfolgreich: {result.message}")
return 0
else:
msg = result.error or result.message
print(f"Fehler: {msg}", file=sys.stderr)
return 1
if cmd == "install-all":
print("Installiere alle Module...")
results = install_all()
failed = [r for r in results if not r.success and r.error]
return 1 if failed else 0
print(f"Unbekannter Befehl: {cmd!r}", file=sys.stderr)
print("Befehle: list | install <key> | install-all")
return 2
if __name__ == "__main__":
sys.exit(_cli(sys.argv[1:]))