feat: FritzBox-Sicherung herunterladen + maximale Anzahl je Box einstellbar
- Satellit: fritzbox.backup_get liefert eine vorhandene Sicherung (nur aus dem Sicherungsordner, bis 20 MB). Bei ARIA landet sie unter /shared/uploads/ und kann in den Chat. - Zugangsdaten FritzBox: "Maximale Sicherungen" (1–100, leer = 10). Bei jeder neuen Sicherung fliegt die aelteste dieser Box raus; die Liste meldet die Zahl mit (Kopfzeile im 💾-Fenster). - Diagnostic: ⬇ je Sicherung laedt die .export-Datei herunter. Getestet: Satellit (Limit 2 → die zwei neuesten bleiben, Download, Pfad- schutz), Diagnostic in headless Chromium (Download-Inhalt, Feld laden, 0 abgelehnt, 20 gespeichert). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -1325,7 +1325,9 @@ META_TOOLS = [
|
||||
"Box uebernimmt alles und startet neu (1–3 min kein Netz); vorher Datum der "
|
||||
"Sicherung nennen und Stefans ausdrueckliches Ja holen, ebenso ob er an der "
|
||||
"Box steht. 'fritzbox.backup_delete' (params={'ip':..,'file':..,'confirmed':true}) "
|
||||
"loescht eine Sicherung (nach Ja). ALLE BOXEN SICHERN: 'fritzbox.backup_all' "
|
||||
"loescht eine Sicherung (nach Ja). 'fritzbox.backup_get' (params={'ip':..,"
|
||||
"'file':..}) holt eine VORHANDENE Sicherung — sie liegt danach unter "
|
||||
"/shared/uploads/ und kommt mit [FILE: <pfad>] in den Chat. ALLE BOXEN SICHERN: 'fritzbox.backup_all' "
|
||||
"(params={} oder {'button_wait':true}) — pro Satellit einmal aufrufen (jeder "
|
||||
"Satellit sichert seine Boxen mit hinterlegtem Login); nicht erreichbare oder "
|
||||
"fehlerhafte werden uebersprungen, es geht mit der naechsten weiter — am Ende "
|
||||
|
||||
+31
-4
@@ -330,7 +330,9 @@
|
||||
<div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;"><input id="dc-fb-user" placeholder="FritzBox-Benutzer (z.B. fritz1234)" style="flex:1;">
|
||||
<button type="button" id="dc-fb-detect" class="btn secondary" onclick="dcDetectFritzUser()" title="Fragt die FritzBox, welche Benutzer es gibt (geht ohne Anmeldung)" style="padding:2px 8px;font-size:11px;white-space:nowrap;">Benutzer ermitteln</button></div>
|
||||
<div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;"><input id="dc-fb-pass" type="password" placeholder="FritzBox-Passwort" style="flex:1;"><button type="button" class="btn secondary" onclick="dcEye('dc-fb-pass')" title="Anzeigen / verbergen" style="padding:2px 8px;font-size:12px;">👁</button></div>
|
||||
<div style="display:flex;gap:4px;align-items:center;"><input id="dc-fb-backuppass" type="password" placeholder="Kennwort für Sicherungen (mind. 12 Zeichen, Aa, 1, !)" title="Damit verschlüsselt ARIA Sicherungen der Box. ARIA nutzt es, kennt es aber nicht — zum Wiederherstellen brauchst du es." style="flex:1;"><button type="button" class="btn secondary" onclick="dcEye('dc-fb-backuppass')" title="Anzeigen / verbergen" style="padding:2px 8px;font-size:12px;">👁</button></div>
|
||||
<div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;"><input id="dc-fb-backuppass" type="password" placeholder="Kennwort für Sicherungen (mind. 12 Zeichen, Aa, 1, !)" title="Damit verschlüsselt ARIA Sicherungen der Box. ARIA nutzt es, kennt es aber nicht — zum Wiederherstellen brauchst du es." style="flex:1;"><button type="button" class="btn secondary" onclick="dcEye('dc-fb-backuppass')" title="Anzeigen / verbergen" style="padding:2px 8px;font-size:12px;">👁</button></div>
|
||||
<div style="display:flex;gap:6px;align-items:center;"><label for="dc-fb-keep" style="font-size:11px;color:#8888AA;">Maximale Sicherungen</label>
|
||||
<input id="dc-fb-keep" type="number" min="1" max="100" placeholder="10" title="So viele Sicherungen dieser Box bleiben auf dem Satelliten — bei jeder neuen fliegt die älteste raus" style="width:70px;"></div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid #1E1E2E;border-radius:6px;padding:10px;">
|
||||
@@ -6351,11 +6353,12 @@
|
||||
const _fbFmtDate = iso => { try { return new Date(iso).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }); } catch (_) { return iso; } };
|
||||
const _fbPwSrc = { backupPass: 'Sicherungs-Kennwort', login: 'FritzBox-Kennwort', angegeben: 'eigenes Kennwort' };
|
||||
async function openFbBackups(sat, ip, name) {
|
||||
fbB = { sat, ip, name, list: [], busy: '', sf: null };
|
||||
fbB = { sat, ip, name, list: [], busy: '', sf: null, keep: 0 };
|
||||
_fbRender('Lade Sicherungen …');
|
||||
const [r, sf] = await Promise.all([fbCmd('fritzbox.backups', {}), fbCmd('fritzbox.second_factor', {})]);
|
||||
if (!fbB) return;
|
||||
fbB.list = (r.ok && r.result && r.result.sicherungen) || [];
|
||||
fbB.keep = (r.ok && r.result && r.result.behalten) || 0;
|
||||
fbB.sf = sf.ok ? sf.result : { fehler: sf.error };
|
||||
_fbRender(r.ok ? '' : '⚠ ' + (r.error || 'Fehler'));
|
||||
}
|
||||
@@ -6368,12 +6371,13 @@
|
||||
<td style="padding:6px;color:#9a9ab5;">${escapeHtml(_fbPwSrc[b.kennwort] || b.kennwort || '')}</td>
|
||||
<td style="padding:6px;white-space:nowrap;">
|
||||
<button class="btn secondary" onclick="fbViewBackup(${i})" title="Inhalt ansehen" style="padding:2px 8px;font-size:11px;">ℹ</button>
|
||||
<button class="btn secondary" onclick="fbDownloadBackup(${i})" title="Herunterladen (.export, verschlüsselt mit dem Sicherungs-Kennwort)" style="padding:2px 8px;font-size:11px;">⬇</button>
|
||||
<button class="btn secondary" onclick="fbRestoreBackup(${i})" title="Diese Sicherung einspielen" style="padding:2px 8px;font-size:11px;">Wiederherstellen</button>
|
||||
<button class="btn secondary" onclick="fbDeleteBackup(${i})" title="Löschen" style="padding:2px 8px;font-size:11px;color:#FF6E6E;">🗑</button></td></tr>`).join('');
|
||||
_fbModal(`<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:8px;">
|
||||
<h3 style="margin:0;">💾 Sicherungen: ${escapeHtml(fbB.name)} <span style="color:#8888AA;font-size:12px;">(${escapeHtml(fbB.ip)})</span></h3>
|
||||
<button onclick="closeFbBackups()" style="background:none;border:none;color:#8888AA;font-size:22px;cursor:pointer;">×</button></div>
|
||||
<p style="color:#8888AA;font-size:11px;margin:0 0 10px;">Liegen auf dem Satelliten, die letzten 10 je Box. Verschlüsselt mit dem hinterlegten Sicherungs-Kennwort (🔑).</p>
|
||||
<p style="color:#8888AA;font-size:11px;margin:0 0 10px;">Liegen auf dem Satelliten, die letzten ${fbB.keep || 10} je Box (einstellbar unter 🔑). Verschlüsselt mit dem hinterlegten Sicherungs-Kennwort (🔑).</p>
|
||||
${_fbSfLine()}
|
||||
<div style="display:flex;gap:8px;margin-bottom:10px;">
|
||||
<button class="btn" onclick="fbCreateBackup()" ${fbB.busy ? 'disabled' : ''}>Jetzt sichern</button>
|
||||
@@ -6468,6 +6472,23 @@
|
||||
const r = await _fbRun('Wiederherstellen läuft', 'fritzbox.restore', { file: b.datei, confirmed: true });
|
||||
if (fbB) _fbShowResult(r, 'Sicherung eingespielt');
|
||||
}
|
||||
async function fbDownloadBackup(i) {
|
||||
const b = fbB.list[i];
|
||||
if (!b) return;
|
||||
_fbRender('Lade ' + b.datei + ' …');
|
||||
const r = await fbCmd('fritzbox.backup_get', { file: b.datei }, 120000);
|
||||
if (!fbB) return;
|
||||
if (!r.ok || !r.result || !r.result.file_b64) { _fbRender('⚠ ' + (r.error || 'Download fehlgeschlagen')); return; }
|
||||
const bin = atob(r.result.file_b64);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let k = 0; k < bin.length; k++) bytes[k] = bin.charCodeAt(k);
|
||||
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/octet-stream' }));
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = r.result.filename || b.datei;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
||||
_fbRender('⬇ ' + (r.result.filename || b.datei) + ' heruntergeladen.');
|
||||
}
|
||||
async function fbDeleteBackup(i) {
|
||||
const b = fbB.list[i];
|
||||
if (!b || !await uiConfirm(`Sicherung vom ${_fbFmtDate(b.erstellt)} löschen?\n\nSie ist danach endgültig weg.`)) return;
|
||||
@@ -6528,7 +6549,7 @@
|
||||
credsModalSat = satId; credsModalIp = ip;
|
||||
dcStored = {}; dcRevealed = false; dcFilled = false; dcFbReq = '';
|
||||
_dcEl('dev-creds-title').textContent = (name || ip) + ' (' + ip + ')';
|
||||
['dc-snmp-community','dc-v3-user','dc-v3-authkey','dc-v3-privkey','dc-http-user','dc-http-pass','dc-fb-user','dc-fb-pass','dc-fb-backuppass','dc-ssh-user','dc-ssh-port','dc-ssh-pass','dc-ssh-key'].forEach(id => {
|
||||
['dc-snmp-community','dc-v3-user','dc-v3-authkey','dc-v3-privkey','dc-http-user','dc-http-pass','dc-fb-user','dc-fb-pass','dc-fb-backuppass','dc-fb-keep','dc-ssh-user','dc-ssh-port','dc-ssh-pass','dc-ssh-key'].forEach(id => {
|
||||
const el = _dcEl(id);
|
||||
el.value = '';
|
||||
if (el.dataset.ph == null) el.dataset.ph = el.placeholder;
|
||||
@@ -6555,6 +6576,7 @@
|
||||
if (v3.privProto) _dcEl('dc-v3-privproto').value = v3.privProto;
|
||||
_dcEl('dc-http-user').value = (detail.http || {}).user || '';
|
||||
_dcEl('dc-fb-user').value = (detail.fritzbox || {}).user || '';
|
||||
_dcEl('dc-fb-keep').value = (detail.fritzbox || {}).backupKeep || '';
|
||||
_dcEl('dc-ssh-user').value = (detail.ssh || {}).user || '';
|
||||
_dcEl('dc-ssh-port').value = (detail.ssh || {}).port || '';
|
||||
}
|
||||
@@ -6638,8 +6660,13 @@
|
||||
if (!/[^A-Za-z0-9]/.test(fbp)) miss.push('ein Sonderzeichen');
|
||||
if (miss.length) { document.getElementById('dc-status').textContent = '⚠ Sicherungs-Kennwort zu schwach — es fehlt: ' + miss.join(', ') + '.'; return; }
|
||||
}
|
||||
const fkeep = parseInt(document.getElementById('dc-fb-keep').value, 10);
|
||||
if (document.getElementById('dc-fb-keep').value && !(fkeep >= 1 && fkeep <= 100)) {
|
||||
document.getElementById('dc-status').textContent = '⚠ Maximale Sicherungen: eine Zahl von 1 bis 100.'; return;
|
||||
}
|
||||
if (fu || fp || fbp || dcStored['dc-fb-pass'] || dcStored['dc-fb-backuppass']) {
|
||||
creds.fritzbox = { user: fu };
|
||||
if (fkeep) creds.fritzbox.backupKeep = fkeep;
|
||||
if (fp) creds.fritzbox.pass = fp;
|
||||
if (fbp) creds.fritzbox.backupPass = fbp;
|
||||
}
|
||||
|
||||
+31
-3
@@ -1465,6 +1465,16 @@ FB_BACKUP_DIR = os.environ.get("FB_BACKUP_DIR", "/data/fritzbox-backups")
|
||||
FB_BACKUP_KEEP = int(os.environ.get("FB_BACKUP_KEEP", "10") or "10")
|
||||
|
||||
|
||||
def _fb_backup_keep(ip: str) -> int:
|
||||
"""Wie viele Sicherungen je Box bleiben: Zugangsdaten (Feld "Maximale
|
||||
Sicherungen") > FB_BACKUP_KEEP (Standard 10). Aelteste fliegen zuerst."""
|
||||
try:
|
||||
n = int(_creds_for(ip).get("fritzbox", {}).get("backupKeep") or 0)
|
||||
except (TypeError, ValueError):
|
||||
n = 0
|
||||
return max(1, min(100, n)) if n else FB_BACKUP_KEEP
|
||||
|
||||
|
||||
def _fb_backup(ip: str, user: str, pw: str, params: dict) -> dict:
|
||||
"""Einstellungen der Box sichern (.export) — landet auf dem Satelliten unter
|
||||
FB_BACKUP_DIR, die letzten FB_BACKUP_KEEP je Box bleiben. Kennwort der
|
||||
@@ -1517,8 +1527,9 @@ def _fb_backup(ip: str, user: str, pw: str, params: dict) -> dict:
|
||||
"kennwort": "angegeben" if params.get("password") else ("backupPass" if stored else "login")}
|
||||
with open(path[:-len(".export")] + ".json", "w", encoding="utf-8") as fh:
|
||||
json.dump(meta, fh, ensure_ascii=False, indent=1)
|
||||
keep = _fb_backup_keep(ip)
|
||||
old = sorted(f for f in os.listdir(FB_BACKUP_DIR) if f.startswith(label + "_") and f.endswith(".export"))
|
||||
for f in old[:-FB_BACKUP_KEEP]:
|
||||
for f in old[:-keep]:
|
||||
for ext in (".export", ".json"):
|
||||
try:
|
||||
os.remove(os.path.join(FB_BACKUP_DIR, f[:-len(".export")] + ext))
|
||||
@@ -1537,7 +1548,7 @@ def _fb_backup(ip: str, user: str, pw: str, params: dict) -> dict:
|
||||
"kennwort": ("das angegebene Kennwort" if params.get("password")
|
||||
else "das im Satelliten hinterlegte Sicherungs-Kennwort" if stored
|
||||
else "das FritzBox-Login-Kennwort"),
|
||||
"hinweis": f"Liegt auf dem Satelliten, die letzten {FB_BACKUP_KEEP} je Box bleiben. "
|
||||
"hinweis": f"Liegt auf dem Satelliten, die letzten {keep} je Box bleiben. "
|
||||
"Zum Wiederherstellen in der FritzBox: System → Sicherung → Wiederherstellen, mit dem Kennwort."}}
|
||||
|
||||
|
||||
@@ -1600,7 +1611,8 @@ def _fb_backups(ip: str, params: dict) -> dict:
|
||||
out.append({"datei": f, "erstellt": m.get("created", ""), "bytes": m.get("bytes") or
|
||||
os.path.getsize(os.path.join(FB_BACKUP_DIR, f)), "modell": m.get("model", ""),
|
||||
"firmware": m.get("firmware", ""), "kennwort": m.get("kennwort", "")})
|
||||
return {"ok": True, "result": {"ip": ip, "count": len(out), "sicherungen": out}}
|
||||
return {"ok": True, "result": {"ip": ip, "count": len(out), "sicherungen": out,
|
||||
"behalten": _fb_backup_keep(ip)}}
|
||||
|
||||
|
||||
# Lesbare Hinweise aus der Sicherung: AVM-Konfig "schluessel = wert;"
|
||||
@@ -1819,6 +1831,20 @@ def _fb_second_factor_set(ip: str, user: str, pw: str, params: dict) -> dict:
|
||||
"button_wait": params.get("button_wait")})
|
||||
|
||||
|
||||
def _fb_backup_get(params: dict) -> dict:
|
||||
"""Eine vorhandene Sicherung als Datei zurueckgeben (Download im Diagnostic,
|
||||
bei ARIA landet sie unter /shared/uploads/ fuer den Chat)."""
|
||||
path = _fb_backup_path(params.get("file") or params.get("datei") or "")
|
||||
if not path:
|
||||
return {"ok": False, "error": "Sicherung nicht gefunden (Dateiname aus fritzbox.backups)."}
|
||||
data = open(path, "rb").read()
|
||||
if len(data) > 20 * 1024 * 1024:
|
||||
return {"ok": False, "error": "Sicherung zu gross fuer die Uebertragung (> 20 MB)."}
|
||||
return {"ok": True, "result": {"datei": os.path.basename(path), "bytes": len(data),
|
||||
"filename": os.path.basename(path),
|
||||
"file_b64": base64.b64encode(data).decode()}}
|
||||
|
||||
|
||||
def _fb_backup_delete(params: dict) -> dict:
|
||||
path = _fb_backup_path(params.get("file") or params.get("datei") or "")
|
||||
if not path:
|
||||
@@ -1850,6 +1876,8 @@ def _do_fritzbox(action: str, params: dict) -> dict:
|
||||
return _fb_backup_view(params)
|
||||
if action == "fritzbox.backup_delete":
|
||||
return _fb_backup_delete(params)
|
||||
if action == "fritzbox.backup_get":
|
||||
return _fb_backup_get(params)
|
||||
fb = _creds_for(ip).get("fritzbox", {})
|
||||
user = str(params.get("user") or fb.get("user") or "")
|
||||
pw = str(params.get("pass") or params.get("password") or fb.get("pass") or "")
|
||||
|
||||
Reference in New Issue
Block a user