Support attaching existing Restic repositories

This commit is contained in:
Mikei386
2026-06-15 12:01:24 +02:00
parent e97dde0ed8
commit 3327f21881
11 changed files with 115 additions and 15 deletions
+15 -6
View File
@@ -57,6 +57,7 @@
'new-job': 'Öffnet den Assistenten für einen neuen Backup-Job. Beispiel: tägliches Appdata-Backup.',
'submit-job': 'Prüft und speichert diesen Backup-Job.',
'new-repo': 'Legt ein neues, isoliertes Restic-Speicherziel an.',
'existing-repo': 'Bindet ein bereits initialisiertes Restic-Repository mit seinem ursprünglichen Passwort ein. URBM initialisiert oder überschreibt es nicht.',
'submit-repo': 'Speichert Ziel und Zugangsdaten. Das Repository wird dadurch noch nicht automatisch initialisiert.',
'init-repo': 'Initialisiert am Ziel einmalig ein neues, leeres Restic-Repository. Bei einem vorhandenen Repository nur das ursprüngliche Passwort eintragen und Test verwenden.',
'unlock-repo': 'Entfernt ausschließlich von Restic als veraltet erkannte Repository-Sperren. Aktive Sperren werden nicht gelöscht.',
@@ -445,18 +446,19 @@
const actions = initialize+button('Testen',`test-repo:${r.id}`)+button('Entsperren',`unlock-repo:${r.id}`)+button('Snapshots',`repo-snapshots:${r.id}`)+button('Prüfen',`maint:check:${r.id}`)+button('Bereinigen',`maint:prune:${r.id}`)+button('Bearbeiten',`edit-repo:${r.id}`)+button('Löschen',`delete-repo:${r.id}`,'danger');
return `<tr><td><strong>${esc(r.name)}</strong><br><span class="bu-muted">${esc(r.id)}</span></td><td>${esc(typeLabels[r.type]||r.type)}</td><td>${esc(r.location)}${volatileLocation(r)?'<span class="bu-warning">Flüchtiger Pfad: Die Daten gehen beim Neustart verloren. Verwende /mnt/user/...</span>':''}</td><td>${r.mount?.managed?'Durch URBM':'Direkt / extern'}</td><td>${actionGroup(actions)}</td></tr>`;
}).join('');
return `<div class="bu-toolbar"><div><h2>Repositorys</h2><div class="bu-muted">Lokale, SMB-, NFS- und SFTP-Ziele.</div></div>${button('Neues Repository','new-repo','primary')}</div><section class="bu-card">${rows ? `<table class="bu-table"><thead><tr><th>Name</th><th>Typ</th><th>Speicherort</th><th>Einbindung</th><th>Aktionen</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="bu-empty">Keine Repositorys konfiguriert.</div>'}</section>`;
return `<div class="bu-toolbar"><div><h2>Repositorys</h2><div class="bu-muted">Lokale, SMB-, NFS- und SFTP-Ziele.</div></div><div class="bu-actions">${button('Vorhandenes einbinden','existing-repo')}${button('Neues Repository','new-repo','primary')}</div></div><section class="bu-card">${rows ? `<table class="bu-table"><thead><tr><th>Name</th><th>Typ</th><th>Speicherort</th><th>Einbindung</th><th>Aktionen</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="bu-empty">Keine Repositorys konfiguriert.</div>'}</section>`;
}
function repositoryForm(repo = {}) {
function repositoryForm(repo = {}, existingRepository = false) {
state.repositoryBrowserPath = '/'; state.repositoryDirectories = repositoryRoots; state.repositoryBrowserError = '';
const passwordRef=repo.passwordRef||id('repo-password');
const credentialRef=repo.credentialRef||id('repo-credential');
content.innerHTML = `<section class="bu-card"><h2>Repository ${repo.id?'bearbeiten':'erstellen'}</h2><form id="bu-repo-form" class="bu-form"><input type="hidden" name="id" value="${esc(repo.id||'')}"><input type="hidden" name="passwordRef" value="${esc(passwordRef)}"><input type="hidden" name="credentialRef" value="${esc(credentialRef)}"><input type="hidden" name="hasCredential" value="${repo.credentialRef?'true':'false'}">
content.innerHTML = `<section class="bu-card"><h2>${repo.id?'Repository bearbeiten':existingRepository?'Vorhandenes Repository einbinden':'Neues Repository erstellen'}</h2><form id="bu-repo-form" class="bu-form"><input type="hidden" name="id" value="${esc(repo.id||'')}"><input type="hidden" name="existingRepository" value="${existingRepository?'true':'false'}"><input type="hidden" name="passwordRef" value="${esc(passwordRef)}"><input type="hidden" name="credentialRef" value="${esc(credentialRef)}"><input type="hidden" name="hasCredential" value="${repo.credentialRef?'true':'false'}">
${existingRepository?'<div class="wide bu-inline-info">Wähle den exakten Ordner des vorhandenen Restic-Repositorys. Darin müssen direkt die Einträge <strong>config</strong>, <strong>data</strong>, <strong>index</strong>, <strong>keys</strong> und <strong>snapshots</strong> liegen. Verwende das ursprüngliche Repository-Passwort. Es wird nichts initialisiert.</div>':''}
<label>Name<input name="name" required value="${esc(repo.name||'')}"></label><label>Typ<select name="type">${['local','smb','nfs','sftp'].map(v=>`<option value="${v}" ${v===repo.type?'selected':''}>${esc(typeLabels[v])}</option>`).join('')}</select></label>
<div id="bu-repository-specific" class="wide"></div>
<fieldset class="wide bu-fieldset"><legend>Restic-Verschlüsselung</legend><div class="bu-form-grid"><label>Repository-Passwort<input name="password" type="password" autocomplete="new-password" ${repo.id?'':'required'} placeholder="${repo.id?'Leer lassen, um das vorhandene Passwort zu behalten':'Ein starkes Repository-Passwort eingeben'}"></label><label>Repository-Passwort wiederholen<input name="passwordConfirm" type="password" autocomplete="new-password" ${repo.id?'':'required'} placeholder="${repo.id?'Bei einer Passwortänderung erneut eingeben':'Repository-Passwort erneut eingeben'}"></label></div><p class="bu-muted">Das Passwort wird von URBM geschützt gespeichert. ${repo.id?'Beide Felder leer lassen, um das vorhandene Passwort zu behalten.':'Bewahre dieses Passwort sicher auf; ohne das Passwort können Backups nicht wiederhergestellt werden.'}</p></fieldset>
<div class="wide bu-actions">${button('Speichern','submit-repo','primary')}${button('Abbrechen','cancel-form')}</div></form></section>`;
<div class="wide bu-actions">${button(existingRepository?'Einbinden und testen':'Speichern','submit-repo','primary')}${button('Abbrechen','cancel-form')}</div></form></section>`;
enhanceTooltips(content);
renderRepositorySpecific(repo.type || 'local', repo);
}
@@ -578,6 +580,7 @@
if (name === 'new-job') return jobForm();
if (name === 'edit-job') return jobForm(state.config.jobs.find(j=>j.id===a));
if (name === 'new-repo') return repositoryForm();
if (name === 'existing-repo') return repositoryForm({}, true);
if (name === 'edit-repo') return repositoryForm(state.config.repositories.find(r=>r.id===a));
if (name === 'cancel-form') return render();
if (name === 'refresh') return load();
@@ -624,13 +627,14 @@
}
if (kind === 'submit-repo') {
const f=new FormData(document.getElementById('bu-repo-form')); const value=name=>String(f.get(name)||'').trim(); const type=value('type'); const managed=(type==='smb'||type==='nfs')&&f.get('managed')==='on';
const existingRepository=value('existingRepository')==='true';
let location=managed?value('mountPoint'):value('location');
if(!location) throw new Error(managed?'Ein Einhängepunkt ist erforderlich.':'Ein Repository-Speicherort ist erforderlich.');
if(managed&&!value('remote')) throw new Error('Für eine verwaltete Einbindung ist eine Netzwerkfreigabe erforderlich.');
const usesCredential=type==='sftp'||(type==='smb'&&managed); const credentialRef=usesCredential&&(value('hasCredential')==='true'||Boolean(f.get('credential')))?value('credentialRef'):'';
const repo={schemaVersion:1,id:value('id')||id('repo'),name:value('name'),type,location,passwordRef:value('passwordRef'),credentialRef,mount:managed?{managed:true,remote:value('remote'),mountPoint:value('mountPoint')}:null,options:type==='sftp'?{knownHostsPath:value('knownHostsPath')}: {}};
const password=String(f.get('password')||''); const passwordConfirm=String(f.get('passwordConfirm')||'');
if(!value('id')&&!password) throw new Error('Für ein neues Repository ist ein Passwort erforderlich.');
if(!value('id')&&!password) throw new Error(existingRepository?'Zum Einbinden ist das ursprüngliche Repository-Passwort erforderlich.':'Für ein neues Repository ist ein Passwort erforderlich.');
if(password!==passwordConfirm) throw new Error('Die beiden Repository-Passwörter stimmen nicht überein.');
if (volatileLocation(repo) && !confirm('WARNUNG: Dieses Ziel liegt unter /tmp oder /run und geht nach einem Unraid-Neustart verloren. Trotzdem speichern?')) return;
if(password) await api(`/v1/secrets/${encodeURIComponent(repo.passwordRef)}`,{method:'PUT',body:JSON.stringify({type:'restic-password',value:password})});
@@ -640,7 +644,12 @@
state.config = await api('/v1/config');
const saved = state.config.repositories.find(r=>r.id===repo.id);
if (!saved || saved.location !== repo.location) throw new Error(`Repository-Speicherort wurde nicht gespeichert. Erwartet: ${repo.location}`);
render(); notify(`Repository gespeichert: ${saved.location}`);
if(existingRepository) {
await api(`/v1/repositories/${repo.id}/test`,{method:'POST'});
render(); notify(`Vorhandenes Repository erfolgreich eingebunden: ${saved.location}`);
} else {
render(); notify(`Repository gespeichert: ${saved.location}`);
}
}
if (kind === 'submit-restore') { const f=new FormData(document.getElementById('bu-restore-form')); await api('/v1/restores',{method:'POST',body:JSON.stringify({schemaVersion:1,id:'',repositoryId:f.get('repositoryId'),snapshotId:f.get('snapshotId'),includes:lines(f.get('includes')),target:f.get('target'),inPlace:f.get('inPlace')==='on',confirmed:f.get('confirmed')==='on'})}); notify('Wiederherstellung wurde eingereiht'); state.view='activity'; render(); }
if (kind === 'submit-settings') { const f=new FormData(document.getElementById('bu-settings-form')); state.config.settings={resticPath:f.get('resticPath'),restoreRoot:f.get('restoreRoot'),persistentLogDir:f.get('persistentLogDir'),showLiveLog:f.get('showLiveLog')==='on',catchUpWindowHours:Number(f.get('catchUp')),checkCron:f.get('checkCron'),pruneCron:f.get('pruneCron')}; await saveConfig(); render(); }