Add repository initialization action

This commit is contained in:
Mikei386
2026-06-13 23:58:52 +02:00
parent d14dd51584
commit c1987b7721
8 changed files with 19 additions and 7 deletions
-1
View File
@@ -1 +0,0 @@
22d71250e0e7336fa9cc6a1a09115c9b7740ab21275163694ca3986b76a81a21 backupper-0.1.6-x86_64-1.txz
+1
View File
@@ -0,0 +1 @@
39bbc54f47b643569b81d1d1fc7b65c49b8387ea03d8d8889c8db1427fbfafe4 backupper-0.1.7-x86_64-1.txz
+5 -2
View File
@@ -2,13 +2,16 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "backupper"> <!ENTITY name "backupper">
<!ENTITY author "Backupper Project"> <!ENTITY author "Backupper Project">
<!ENTITY version "0.1.6"> <!ENTITY version "0.1.7">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "22d71250e0e7336fa9cc6a1a09115c9b7740ab21275163694ca3986b76a81a21"> <!ENTITY packageSHA256 "39bbc54f47b643569b81d1d1fc7b65c49b8387ea03d8d8889c8db1427fbfafe4">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues">
<CHANGES> <CHANGES>
### 0.1.7
- Add an explicit repository initialization action and a clear error for uninitialized destinations.
### 0.1.6 ### 0.1.6
- Fix false CSRF errors by relying on Unraid's native request validation. - Fix false CSRF errors by relying on Unraid's native request validation.
+3
View File
@@ -208,6 +208,9 @@ func (r *Runner) run(ctx context.Context, repo model.Repository, args []string,
if errors.Is(ctx.Err(), context.Canceled) { if errors.Is(ctx.Err(), context.Canceled) {
return fmt.Errorf("cancelled: %w", ctx.Err()) 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 fmt.Errorf("restic: %s: %w", strings.TrimSpace(message), err)
} }
return nil return nil
+4 -1
View File
@@ -2,13 +2,16 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "backupper"> <!ENTITY name "backupper">
<!ENTITY author "Backupper Project"> <!ENTITY author "Backupper Project">
<!ENTITY version "0.1.6"> <!ENTITY version "0.1.7">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&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/BackUpper/issues"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues">
<CHANGES> <CHANGES>
### 0.1.7
- Add an explicit repository initialization action and a clear error for uninitialized destinations.
### 0.1.6 ### 0.1.6
- Fix false CSRF errors by relying on Unraid's native request validation. - Fix false CSRF errors by relying on Unraid's native request validation.
+2 -2
View File
@@ -9,7 +9,7 @@ Tag="backup restic snapshots restore"
<?php <?php
$pluginRoot = '/plugins/backupper'; $pluginRoot = '/plugins/backupper';
?> ?>
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=6"> <link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=7">
<div id="backupper-app" data-api="<?= $pluginRoot ?>/api.php"> <div id="backupper-app" data-api="<?= $pluginRoot ?>/api.php">
<header class="bu-header"> <header class="bu-header">
<div><h1>Backupper</h1><p>Encrypted Restic backups for Unraid</p></div> <div><h1>Backupper</h1><p>Encrypted Restic backups for Unraid</p></div>
@@ -29,4 +29,4 @@ $pluginRoot = '/plugins/backupper';
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div> <div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
</div> </div>
<script>window.BACKUPPER_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script> <script>window.BACKUPPER_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
<script src="<?= $pluginRoot ?>/assets/backupper.js?v=6"></script> <script src="<?= $pluginRoot ?>/assets/backupper.js?v=7"></script>
+4 -1
View File
@@ -50,6 +50,7 @@
'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.',
'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 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.', 'load-snapshots': 'Lädt alle Sicherungsstände des ausgewählten Repositorys.',
'restore-selected': 'Übernimmt die markierten Dateien oder Ordner in den Restore-Assistenten.', '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.', '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.', '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.', '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.', '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.', '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.', '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.', 'edit-repo': 'Öffnet Ziel-, Mount- und Zugangseinstellungen dieses Repositorys.',
@@ -225,7 +227,7 @@
} }
function renderRepositories() { function renderRepositories() {
const rows = state.config.repositories.map(r => `<tr><td><strong>${esc(r.name)}</strong><br><span class="bu-muted">${esc(r.id)}</span></td><td>${esc(r.type)}</td><td>${esc(r.location)}</td><td>${r.mount?.managed?'Managed':'Direct / external'}</td><td class="bu-actions">${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')}</td></tr>`).join(''); const rows = state.config.repositories.map(r => `<tr><td><strong>${esc(r.name)}</strong><br><span class="bu-muted">${esc(r.id)}</span></td><td>${esc(r.type)}</td><td>${esc(r.location)}</td><td>${r.mount?.managed?'Managed':'Direct / external'}</td><td class="bu-actions">${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')}</td></tr>`).join('');
return `<div class="bu-toolbar"><div><h2>Repositories</h2><div class="bu-muted">Local, SMB, NFS and SFTP destinations.</div></div>${button('New repository','new-repo','primary')}</div><section class="bu-card">${rows ? `<table class="bu-table"><thead><tr><th>Name</th><th>Type</th><th>Location</th><th>Mount</th><th>Actions</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="bu-empty">No repositories configured.</div>'}</section>`; return `<div class="bu-toolbar"><div><h2>Repositories</h2><div class="bu-muted">Local, SMB, NFS and SFTP destinations.</div></div>${button('New repository','new-repo','primary')}</div><section class="bu-card">${rows ? `<table class="bu-table"><thead><tr><th>Name</th><th>Type</th><th>Location</th><th>Mount</th><th>Actions</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="bu-empty">No repositories configured.</div>'}</section>`;
} }
@@ -286,6 +288,7 @@
if (name === 'run-job') { await api(`/v1/jobs/${a}/run`,{method:'POST'}); notify('Backup queued'); return load(); } 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 === '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 === '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 === '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 === '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(); } 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(); }