Support attaching existing Restic repositories
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
250d5afee9c2939738b74f0f6f719db12d9399068bf9452e08e2b03a67d381ad urbm-2026.06.15.r013-x86_64-1.txz
|
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
2cb10896997b03d476b52275aebe077114c77b14ac796c7ba6e608f85b324536 urbm-2026.06.15.r014-x86_64-1.txz
|
||||||
Vendored
+7
-2
@@ -2,13 +2,18 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "urbm">
|
<!ENTITY name "urbm">
|
||||||
<!ENTITY author "Michael Roll">
|
<!ENTITY author "Michael Roll">
|
||||||
<!ENTITY version "2026.06.15.r013">
|
<!ENTITY version "2026.06.15.r014">
|
||||||
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
|
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
|
||||||
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
|
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
|
||||||
<!ENTITY packageSHA256 "250d5afee9c2939738b74f0f6f719db12d9399068bf9452e08e2b03a67d381ad">
|
<!ENTITY packageSHA256 "2cb10896997b03d476b52275aebe077114c77b14ac796c7ba6e608f85b324536">
|
||||||
]>
|
]>
|
||||||
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
|
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
|
||||||
<CHANGES>
|
<CHANGES>
|
||||||
|
### 2026.06.15.r014
|
||||||
|
- Add a dedicated workflow for attaching and immediately testing an existing Restic repository without initialization.
|
||||||
|
- Detect a missing Restic config at local and mounted paths and suggest direct child directories that contain a repository.
|
||||||
|
- Explain that an existing repository path must directly contain config, data, index, keys, and snapshots.
|
||||||
|
|
||||||
### 2026.06.15.r013
|
### 2026.06.15.r013
|
||||||
- Replace invalid USB raw-image percentages, file counts, and multi-million-hour ETAs with streaming progress.
|
- Replace invalid USB raw-image percentages, file counts, and multi-million-hour ETAs with streaming progress.
|
||||||
- Show bytes read and transfer rate while the final image size remains unknown until EOF.
|
- Show bytes read and transfer rate while the final image size remains unknown until EOF.
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ func resticError(message string, commandErr error) error {
|
|||||||
case strings.Contains(lower, "wrong password") || strings.Contains(lower, "no key found"):
|
case strings.Contains(lower, "wrong password") || strings.Contains(lower, "no key found"):
|
||||||
return errors.New("authentication: wrong repository password. Enter the original Restic password under Edit and select Test")
|
return errors.New("authentication: wrong repository password. Enter the original Restic password under Edit and select Test")
|
||||||
case strings.Contains(lower, "repository does not exist") || strings.Contains(lower, "is there a repository at the following location?"):
|
case strings.Contains(lower, "repository does not exist") || strings.Contains(lower, "is there a repository at the following location?"):
|
||||||
return errors.New("repository: destination is not initialized; open Repositories and select Initialize once")
|
return errors.New("repository: Am konfigurierten Ziel wurde keine Restic-config gefunden. Für ein vorhandenes Repository den exakten Repository-Ordner eintragen; nur ein wirklich neues, leeres Ziel initialisieren")
|
||||||
default:
|
default:
|
||||||
message = strings.TrimSpace(strings.TrimPrefix(message, "Fatal:"))
|
message = strings.TrimSpace(strings.TrimPrefix(message, "Fatal:"))
|
||||||
return fmt.Errorf("restic: %s: %w", message, commandErr)
|
return fmt.Errorf("restic: %s: %w", message, commandErr)
|
||||||
|
|||||||
@@ -298,6 +298,13 @@ func TestResticErrorExplainsCiphertextVerification(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResticErrorExplainsMissingExistingRepositoryPath(t *testing.T) {
|
||||||
|
err := resticError("Fatal: repository does not exist", fmt.Errorf("exit status 10"))
|
||||||
|
if !strings.Contains(err.Error(), "exakten Repository-Ordner") || !strings.Contains(err.Error(), "wirklich neues") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunnerPausesResumesAndCancelsProcessGroup(t *testing.T) {
|
func TestRunnerPausesResumesAndCancelsProcessGroup(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
ticksPath := filepath.Join(dir, "ticks")
|
ticksPath := filepath.Join(dir, "ticks")
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateExistingRepositoryPathAcceptsRepositoryRoot(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "config"), []byte("restic"), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := validateExistingRepositoryPath(root); err != nil {
|
||||||
|
t.Fatalf("repository root rejected: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateExistingRepositoryPathSuggestsDirectChild(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repository := filepath.Join(root, "server-backup")
|
||||||
|
if err := os.Mkdir(repository, 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(repository, "config"), []byte("restic"), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err := validateExistingRepositoryPath(root)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), repository) {
|
||||||
|
t.Fatalf("missing repository suggestion: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateExistingRepositoryPathExplainsMissingConfig(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
err := validateExistingRepositoryPath(root)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "config, data, index, keys und snapshots") {
|
||||||
|
t.Fatalf("missing layout guidance: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -262,10 +262,43 @@ func (s *Service) TestRepository(ctx context.Context, repoID string, initialize
|
|||||||
if initialize {
|
if initialize {
|
||||||
return s.restic.Init(ctx, mounted.Repository)
|
return s.restic.Init(ctx, mounted.Repository)
|
||||||
}
|
}
|
||||||
|
if mounted.Repository.Type != model.RepositorySFTP {
|
||||||
|
if err := validateExistingRepositoryPath(mounted.Repository.Location); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
_, err = s.restic.Snapshots(ctx, mounted.Repository)
|
_, err = s.restic.Snapshots(ctx, mounted.Repository)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateExistingRepositoryPath(location string) error {
|
||||||
|
configPath := filepath.Join(location, "config")
|
||||||
|
if info, err := os.Stat(configPath); err == nil && !info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries, readErr := os.ReadDir(location)
|
||||||
|
if readErr != nil {
|
||||||
|
return fmt.Errorf("repository: Repository-Ordner %q kann nicht gelesen werden: %w", location, readErr)
|
||||||
|
}
|
||||||
|
candidates := make([]string, 0, 3)
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidate := filepath.Join(location, entry.Name())
|
||||||
|
if info, err := os.Stat(filepath.Join(candidate, "config")); err == nil && !info.IsDir() {
|
||||||
|
candidates = append(candidates, candidate)
|
||||||
|
if len(candidates) == 3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(candidates) > 0 {
|
||||||
|
return fmt.Errorf("repository: Im gewählten Ordner liegt keine Restic-config. Verwende direkt den Repository-Ordner: %s", strings.Join(candidates, ", "))
|
||||||
|
}
|
||||||
|
return fmt.Errorf("repository: Im gewählten Ordner %q wurde keine Restic-config gefunden. Ein vorhandenes Repository muss direkt auf den Ordner zeigen, der config, data, index, keys und snapshots enthält", location)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) UnlockRepository(ctx context.Context, repoID string) error {
|
func (s *Service) UnlockRepository(ctx context.Context, repoID string) error {
|
||||||
repo, ok := s.repository(repoID)
|
repo, ok := s.repository(repoID)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
+6
-1
@@ -2,13 +2,18 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "urbm">
|
<!ENTITY name "urbm">
|
||||||
<!ENTITY author "Michael Roll">
|
<!ENTITY author "Michael Roll">
|
||||||
<!ENTITY version "2026.06.15.r013">
|
<!ENTITY version "2026.06.15.r014">
|
||||||
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
|
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
|
||||||
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
|
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
|
||||||
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
|
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
|
||||||
]>
|
]>
|
||||||
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
|
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
|
||||||
<CHANGES>
|
<CHANGES>
|
||||||
|
### 2026.06.15.r014
|
||||||
|
- Add a dedicated workflow for attaching and immediately testing an existing Restic repository without initialization.
|
||||||
|
- Detect a missing Restic config at local and mounted paths and suggest direct child directories that contain a repository.
|
||||||
|
- Explain that an existing repository path must directly contain config, data, index, keys, and snapshots.
|
||||||
|
|
||||||
### 2026.06.15.r013
|
### 2026.06.15.r013
|
||||||
- Replace invalid USB raw-image percentages, file counts, and multi-million-hour ETAs with streaming progress.
|
- Replace invalid USB raw-image percentages, file counts, and multi-million-hour ETAs with streaming progress.
|
||||||
- Show bytes read and transfer rate while the final image size remains unknown until EOF.
|
- Show bytes read and transfer rate while the final image size remains unknown until EOF.
|
||||||
|
|||||||
+4
-4
@@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
|
|||||||
<?php
|
<?php
|
||||||
$pluginRoot = '/plugins/urbm';
|
$pluginRoot = '/plugins/urbm';
|
||||||
?>
|
?>
|
||||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260615r013">
|
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260615r014">
|
||||||
<div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php">
|
<div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php">
|
||||||
<header class="bu-header">
|
<header class="bu-header">
|
||||||
<div class="bu-brand">
|
<div class="bu-brand">
|
||||||
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260615r013" alt="URBM-Logo">
|
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260615r014" alt="URBM-Logo">
|
||||||
<div><h1>URBM <span class="bu-version">2026.06.15.r013</span></h1><p>Restic Backup Manager für Unraid</p></div>
|
<div><h1>URBM <span class="bu-version">2026.06.15.r014</span></h1><p>Restic Backup Manager für Unraid</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Verbindung wird hergestellt...</div>
|
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Verbindung wird hergestellt...</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
|
|||||||
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
|
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
<script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
|
<script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
|
||||||
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.15.r013.js"></script>
|
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.15.r014.js"></script>
|
||||||
|
|||||||
+14
-5
@@ -57,6 +57,7 @@
|
|||||||
'new-job': 'Öffnet den Assistenten für einen neuen Backup-Job. Beispiel: tägliches Appdata-Backup.',
|
'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.',
|
'submit-job': 'Prüft und speichert diesen Backup-Job.',
|
||||||
'new-repo': 'Legt ein neues, isoliertes Restic-Speicherziel an.',
|
'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.',
|
'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.',
|
'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.',
|
'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');
|
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>`;
|
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('');
|
}).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 = '';
|
state.repositoryBrowserPath = '/'; state.repositoryDirectories = repositoryRoots; state.repositoryBrowserError = '';
|
||||||
const passwordRef=repo.passwordRef||id('repo-password');
|
const passwordRef=repo.passwordRef||id('repo-password');
|
||||||
const credentialRef=repo.credentialRef||id('repo-credential');
|
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>
|
<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>
|
<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>
|
<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);
|
enhanceTooltips(content);
|
||||||
renderRepositorySpecific(repo.type || 'local', repo);
|
renderRepositorySpecific(repo.type || 'local', repo);
|
||||||
}
|
}
|
||||||
@@ -578,6 +580,7 @@
|
|||||||
if (name === 'new-job') return jobForm();
|
if (name === 'new-job') return jobForm();
|
||||||
if (name === 'edit-job') return jobForm(state.config.jobs.find(j=>j.id===a));
|
if (name === 'edit-job') return jobForm(state.config.jobs.find(j=>j.id===a));
|
||||||
if (name === 'new-repo') return repositoryForm();
|
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 === 'edit-repo') return repositoryForm(state.config.repositories.find(r=>r.id===a));
|
||||||
if (name === 'cancel-form') return render();
|
if (name === 'cancel-form') return render();
|
||||||
if (name === 'refresh') return load();
|
if (name === 'refresh') return load();
|
||||||
@@ -624,13 +627,14 @@
|
|||||||
}
|
}
|
||||||
if (kind === 'submit-repo') {
|
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 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');
|
let location=managed?value('mountPoint'):value('location');
|
||||||
if(!location) throw new Error(managed?'Ein Einhängepunkt ist erforderlich.':'Ein Repository-Speicherort ist erforderlich.');
|
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.');
|
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 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 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')||'');
|
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(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 (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})});
|
if(password) await api(`/v1/secrets/${encodeURIComponent(repo.passwordRef)}`,{method:'PUT',body:JSON.stringify({type:'restic-password',value:password})});
|
||||||
@@ -640,8 +644,13 @@
|
|||||||
state.config = await api('/v1/config');
|
state.config = await api('/v1/config');
|
||||||
const saved = state.config.repositories.find(r=>r.id===repo.id);
|
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}`);
|
if (!saved || saved.location !== repo.location) throw new Error(`Repository-Speicherort wurde nicht gespeichert. Erwartet: ${repo.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}`);
|
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-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(); }
|
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(); }
|
||||||
if (kind === 'submit-unraid-notify') { const f=new FormData(document.getElementById('bu-unraid-notify-form')); const target={schemaVersion:1,id:f.get('id')||'unraid',name:'Unraid',type:'unraid',enabled:f.get('enabled')==='on',events:f.getAll('events')}; if(target.enabled&&!target.events.length) throw new Error('Wähle mindestens ein Ereignis für Unraid-Benachrichtigungen aus.'); state.config.notifications=state.config.notifications.filter(n=>n.type!=='unraid'&&n.type!=='ntfy').concat(target); await saveConfig(); render(); }
|
if (kind === 'submit-unraid-notify') { const f=new FormData(document.getElementById('bu-unraid-notify-form')); const target={schemaVersion:1,id:f.get('id')||'unraid',name:'Unraid',type:'unraid',enabled:f.get('enabled')==='on',events:f.getAll('events')}; if(target.enabled&&!target.events.length) throw new Error('Wähle mindestens ein Ereignis für Unraid-Benachrichtigungen aus.'); state.config.notifications=state.config.notifications.filter(n=>n.type!=='unraid'&&n.type!=='ntfy').concat(target); await saveConfig(); render(); }
|
||||||
|
|||||||
Reference in New Issue
Block a user