diff --git a/dist/backupper-0.1.6-x86_64-1.txz.sha256 b/dist/backupper-0.1.6-x86_64-1.txz.sha256 deleted file mode 100644 index 38cea1d..0000000 --- a/dist/backupper-0.1.6-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -22d71250e0e7336fa9cc6a1a09115c9b7740ab21275163694ca3986b76a81a21 backupper-0.1.6-x86_64-1.txz diff --git a/dist/backupper-0.1.6-x86_64-1.txz b/dist/backupper-0.1.7-x86_64-1.txz similarity index 55% rename from dist/backupper-0.1.6-x86_64-1.txz rename to dist/backupper-0.1.7-x86_64-1.txz index 6f34905..21cbe87 100644 Binary files a/dist/backupper-0.1.6-x86_64-1.txz and b/dist/backupper-0.1.7-x86_64-1.txz differ diff --git a/dist/backupper-0.1.7-x86_64-1.txz.sha256 b/dist/backupper-0.1.7-x86_64-1.txz.sha256 new file mode 100644 index 0000000..3dfbf73 --- /dev/null +++ b/dist/backupper-0.1.7-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +39bbc54f47b643569b81d1d1fc7b65c49b8387ea03d8d8889c8db1427fbfafe4 backupper-0.1.7-x86_64-1.txz diff --git a/dist/backupper.plg b/dist/backupper.plg index e64cceb..81773ec 100644 --- a/dist/backupper.plg +++ b/dist/backupper.plg @@ -2,13 +2,16 @@ - + - + ]> +### 0.1.7 +- Add an explicit repository initialization action and a clear error for uninitialized destinations. + ### 0.1.6 - Fix false CSRF errors by relying on Unraid's native request validation. diff --git a/internal/restic/restic.go b/internal/restic/restic.go index dec9abd..679f938 100644 --- a/internal/restic/restic.go +++ b/internal/restic/restic.go @@ -208,6 +208,9 @@ func (r *Runner) run(ctx context.Context, repo model.Repository, args []string, if errors.Is(ctx.Err(), context.Canceled) { return fmt.Errorf("cancelled: %w", ctx.Err()) } + if strings.Contains(message, "repository does not exist") || strings.Contains(message, "Is there a repository at the following location?") { + return errors.New("repository: destination is not initialized; open Repositories and select Initialize") + } return fmt.Errorf("restic: %s: %w", strings.TrimSpace(message), err) } return nil diff --git a/plugin/backupper.plg b/plugin/backupper.plg index 17ef128..8ee198a 100644 --- a/plugin/backupper.plg +++ b/plugin/backupper.plg @@ -2,13 +2,16 @@ - + ]> +### 0.1.7 +- Add an explicit repository initialization action and a clear error for uninitialized destinations. + ### 0.1.6 - Fix false CSRF errors by relying on Unraid's native request validation. diff --git a/webgui/Backupper.page b/webgui/Backupper.page index a4cc0c9..4df712c 100644 --- a/webgui/Backupper.page +++ b/webgui/Backupper.page @@ -9,7 +9,7 @@ Tag="backup restic snapshots restore" - +

Backupper

Encrypted Restic backups for Unraid

@@ -29,4 +29,4 @@ $pluginRoot = '/plugins/backupper';
- + diff --git a/webgui/assets/backupper.js b/webgui/assets/backupper.js index 0bb7663..9e86fc6 100644 --- a/webgui/assets/backupper.js +++ b/webgui/assets/backupper.js @@ -50,6 +50,7 @@ 'submit-job': 'Prüft und speichert diesen Backup-Job.', 'new-repo': 'Legt ein neues, isoliertes Restic-Speicherziel an.', 'submit-repo': 'Speichert Ziel und Zugangsdaten. Das Repository wird dadurch noch nicht automatisch initialisiert.', + 'init-repo': 'Initialisiert am konfigurierten Ziel ein neues, leeres Restic-Repository. Beispiel: nach dem erstmaligen Speichern eines neuen Backup-Ziels. Nicht für ein bereits vorhandenes Repository verwenden.', 'load-snapshots': 'Lädt alle Sicherungsstände des ausgewählten Repositorys.', 'restore-selected': 'Übernimmt die markierten Dateien oder Ordner in den Restore-Assistenten.', 'submit-restore': 'Stellt den Restore in die globale Warteschlange; Backups und Restores laufen nacheinander.', @@ -88,6 +89,7 @@ 'duplicate-job': 'Erstellt eine deaktivierte Kopie als Vorlage. Danach Name, Repository und Quellen anpassen.', 'delete-job': 'Löscht die Job-Konfiguration. Vorhandene Restic-Snapshots im Repository bleiben erhalten.', 'test-repo': 'Prüft Erreichbarkeit, Passwort und Restic-Zugriff, ohne ein Backup zu starten.', + 'init-repo': 'Erstellt die Restic-Struktur am Speicherziel. Diese Aktion ist einmalig für ein neues, leeres Ziel erforderlich.', 'repo-snapshots': 'Wechselt zu den Sicherungsständen dieses Repositorys.', 'maint': action.includes(':check:') ? 'Prüft Struktur und Integrität des Restic-Repositorys.' : 'Entfernt nach Retention nicht mehr benötigte Datenblöcke und gibt Speicher frei.', 'edit-repo': 'Öffnet Ziel-, Mount- und Zugangseinstellungen dieses Repositorys.', @@ -225,7 +227,7 @@ } function renderRepositories() { - const rows = state.config.repositories.map(r => `${esc(r.name)}
${esc(r.id)}${esc(r.type)}${esc(r.location)}${r.mount?.managed?'Managed':'Direct / external'}${button('Test',`test-repo:${r.id}`)}${button('Snapshots',`repo-snapshots:${r.id}`)}${button('Check',`maint:check:${r.id}`)}${button('Prune',`maint:prune:${r.id}`)}${button('Edit',`edit-repo:${r.id}`)}${button('Delete',`delete-repo:${r.id}`,'danger')}`).join(''); + const rows = state.config.repositories.map(r => `${esc(r.name)}
${esc(r.id)}${esc(r.type)}${esc(r.location)}${r.mount?.managed?'Managed':'Direct / external'}${button('Initialize',`init-repo:${r.id}`,'primary')}${button('Test',`test-repo:${r.id}`)}${button('Snapshots',`repo-snapshots:${r.id}`)}${button('Check',`maint:check:${r.id}`)}${button('Prune',`maint:prune:${r.id}`)}${button('Edit',`edit-repo:${r.id}`)}${button('Delete',`delete-repo:${r.id}`,'danger')}`).join(''); return `

Repositories

Local, SMB, NFS and SFTP destinations.
${button('New repository','new-repo','primary')}
${rows ? `${rows}
NameTypeLocationMountActions
` : '
No repositories configured.
'}
`; } @@ -286,6 +288,7 @@ if (name === 'run-job') { await api(`/v1/jobs/${a}/run`,{method:'POST'}); notify('Backup queued'); return load(); } if (name === 'cancel-run') { await api(`/v1/runs/${a}/cancel`,{method:'POST'}); return load(); } if (name === 'test-repo') { await api(`/v1/repositories/${a}/test`,{method:'POST'}); return notify('Repository connection succeeded'); } + if (name === 'init-repo') { if(confirm('Initialize a new Restic repository at this destination? Use this only for a new, empty backup destination.')) { await api(`/v1/repositories/${a}/init`,{method:'POST'}); notify('Repository initialized successfully'); } return; } if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a} queued`); return load(); } if (name === 'repo-snapshots') { state.view='snapshots'; document.querySelector('[data-view="snapshots"]').click(); render(); setTimeout(()=>{document.getElementById('bu-snapshot-repo').value=a; handle('load-snapshots');},0); return; } if (name === 'load-snapshots') { const repo=document.getElementById('bu-snapshot-repo').value; state.snapshots=await api(`/v1/repositories/${repo}/snapshots`); state.snapshotRepo=repo; return render(); }