Show restore progress details

This commit is contained in:
Mikei386
2026-06-21 22:59:56 +02:00
parent c014a5f502
commit 41ac4ba43e
10 changed files with 84 additions and 15 deletions
-1
View File
@@ -1 +0,0 @@
9c909e42cbe12ca04b7e27661f4f7c1152ca66904f39083d7782fd31c19701ef urbm-2026.06.21.r012-x86_64-1.txz
+1
View File
@@ -0,0 +1 @@
d357fa422c688189bca32be22e1de05de15c079fdcaaa52b6315072322dbea6f urbm-2026.06.21.r013-x86_64-1.txz
+6 -2
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.21.r012"> <!ENTITY version "2026.06.21.r013">
<!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 "9c909e42cbe12ca04b7e27661f4f7c1152ca66904f39083d7782fd31c19701ef"> <!ENTITY packageSHA256 "d357fa422c688189bca32be22e1de05de15c079fdcaaa52b6315072322dbea6f">
]> ]>
<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.21.r013
- Show Restic restore progress in activities with percent, transferred bytes, file counts, speed, ETA, and current file when Restic provides JSON status updates.
- Keep restore target details readable in the activity list instead of showing the internal restore payload.
### 2026.06.21.r012 ### 2026.06.21.r012
- Add emoji-labelled fields to Unraid and Gotify backup notifications. - Add emoji-labelled fields to Unraid and Gotify backup notifications.
- Shorten long Restic snapshot IDs in notifications to keep mobile notification cards readable. - Shorten long Restic snapshot IDs in notifications to keep mobile notification cards readable.
+8 -3
View File
@@ -280,12 +280,17 @@ func (r *Runner) List(ctx context.Context, repo model.Repository, snapshot, path
return items, scanner.Err() return items, scanner.Err()
} }
func (r *Runner) Restore(ctx context.Context, repo model.Repository, task model.RestoreTask) error { func (r *Runner) Restore(ctx context.Context, repo model.Repository, task model.RestoreTask, progress ProgressCallback) error {
args := []string{"restore", task.SnapshotID, "--target", task.Target} args := []string{"restore", task.SnapshotID, "--target", task.Target, "--json"}
for _, include := range task.Includes { for _, include := range task.Includes {
args = append(args, "--include", include) args = append(args, "--include", include)
} }
return r.run(ctx, repo, args, nil, nil) return r.run(ctx, repo, args, func(line []byte) {
var status Progress
if json.Unmarshal(line, &status) == nil && status.MessageType == "status" && progress != nil {
progress(status)
}
}, nil)
} }
func (r *Runner) run(ctx context.Context, repo model.Repository, args []string, onLine func([]byte), capture *[]byte) error { func (r *Runner) run(ctx context.Context, repo model.Repository, args []string, onLine func([]byte), capture *[]byte) error {
+26
View File
@@ -164,6 +164,32 @@ func TestBackupImageStreamsRawDeviceWithStableFilename(t *testing.T) {
} }
} }
func TestRestoreReportsProgress(t *testing.T) {
dir := t.TempDir()
argsPath := filepath.Join(dir, "args")
script := filepath.Join(dir, "restic")
body := fmt.Sprintf("#!/bin/sh\nprintf '%%s\\n' \"$@\" > '%s'\nprintf '%%s\\n' '{\"message_type\":\"status\",\"percent_done\":0.25,\"total_files\":8,\"files_done\":2,\"total_bytes\":4096,\"bytes_done\":1024,\"seconds_remaining\":30,\"current_files\":[\"/restore/file.txt\"]}'\n", argsPath)
if err := os.WriteFile(script, []byte(body), 0700); err != nil {
t.Fatal(err)
}
runner := &Runner{Binary: script, RuntimeDir: dir, Secrets: fakeSecrets{"password": "secret"}}
repo := model.Repository{Type: model.RepositoryLocal, Location: "/repo", PasswordRef: "password"}
task := model.RestoreTask{SnapshotID: "snap123", Target: "/target", Includes: []string{"/source/file.txt"}}
var progress Progress
if err := runner.Restore(context.Background(), repo, task, func(value Progress) { progress = value }); err != nil {
t.Fatal(err)
}
if progress.PercentDone != 0.25 || progress.FilesDone != 2 || progress.BytesDone != 1024 || progress.SecondsRemaining != 30 || len(progress.CurrentFiles) != 1 {
t.Fatalf("unexpected progress: %+v", progress)
}
args, _ := os.ReadFile(argsPath)
for _, expected := range []string{"restore", "snap123", "--target", "/target", "--json", "--include", "/source/file.txt"} {
if !strings.Contains(string(args), expected) {
t.Fatalf("arguments missing %q: %s", expected, args)
}
}
}
func TestStatsUsesRequestedRepositoryCountingMode(t *testing.T) { func TestStatsUsesRequestedRepositoryCountingMode(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
argsPath := filepath.Join(dir, "args") argsPath := filepath.Join(dir, "args")
+24 -2
View File
@@ -732,13 +732,35 @@ func (s *Service) executeRestore(ctx context.Context, run model.Run) model.Run {
defer s.mounts.Cleanup(context.Background(), mounted) defer s.mounts.Cleanup(context.Background(), mounted)
appendLiveLog(&live, "Restic stellt den Snapshot wieder her") appendLiveLog(&live, "Restic stellt den Snapshot wieder her")
s.updateActive(live) s.updateActive(live)
if err := s.restic.Restore(ctx, mounted.Repository, task); err != nil { lastLoggedPercent := -10
started := time.Now()
if err := s.restic.Restore(ctx, mounted.Repository, task, func(progress restic.Progress) {
live.ProgressPercent = progress.PercentDone * 100
live.ProgressBytes, live.ProgressTotal = progress.BytesDone, progress.TotalBytes
live.ProgressFiles, live.ProgressFileTotal = progress.FilesDone, progress.TotalFiles
live.SecondsRemaining = progress.SecondsRemaining
if elapsed := time.Since(started).Seconds(); elapsed > 0 {
live.BytesPerSecond = float64(progress.BytesDone) / elapsed
}
if len(progress.CurrentFiles) > 0 {
live.CurrentFile = progress.CurrentFiles[len(progress.CurrentFiles)-1]
}
percent := int(live.ProgressPercent)
if percent >= lastLoggedPercent+10 {
appendLiveLog(&live, fmt.Sprintf("Restore-Fortschritt: %.1f%%, %d/%d Dateien, %s/%s", live.ProgressPercent, live.ProgressFiles, live.ProgressFileTotal, formatBytes(live.ProgressBytes), formatBytes(live.ProgressTotal)))
lastLoggedPercent = percent - percent%10
}
s.updateActive(live)
}); err != nil {
failedRun := failedError(run, err) failedRun := failedError(run, err)
appendLiveLog(&live, "Restore fehlgeschlagen: "+err.Error()) appendLiveLog(&live, "Restore fehlgeschlagen: "+err.Error())
failedRun.LiveLog = live.LiveLog failedRun.LiveLog = live.LiveLog
return failedRun return failedRun
} }
run.Status, run.Message, run.JobID = "success", "restore completed", "" run.Status, run.Message = "success", "restore completed"
run.ProgressPercent, run.ProgressBytes, run.ProgressTotal = 100, live.ProgressBytes, live.ProgressTotal
run.ProgressFiles, run.ProgressFileTotal = live.ProgressFiles, live.ProgressFileTotal
run.BytesPerSecond, run.SecondsRemaining, run.CurrentFile = live.BytesPerSecond, 0, live.CurrentFile
appendLiveLog(&live, "Restore abgeschlossen") appendLiveLog(&live, "Restore abgeschlossen")
run.LiveLog = live.LiveLog run.LiveLog = live.LiveLog
return run return run
+5 -1
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.21.r012"> <!ENTITY version "2026.06.21.r013">
<!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.21.r013
- Show Restic restore progress in activities with percent, transferred bytes, file counts, speed, ETA, and current file when Restic provides JSON status updates.
- Keep restore target details readable in the activity list instead of showing the internal restore payload.
### 2026.06.21.r012 ### 2026.06.21.r012
- Add emoji-labelled fields to Unraid and Gotify backup notifications. - Add emoji-labelled fields to Unraid and Gotify backup notifications.
- Shorten long Restic snapshot IDs in notifications to keep mobile notification cards readable. - Shorten long Restic snapshot IDs in notifications to keep mobile notification cards readable.
+4 -4
View File
@@ -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=20260621r012"> <link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260621r013">
<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=20260621r012" alt="URBM-Logo"> <img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260621r013" alt="URBM-Logo">
<div><h1>URBM <span class="bu-version">2026.06.21.r012</span></h1><p>Restic Backup Manager für Unraid</p></div> <div><h1>URBM <span class="bu-version">2026.06.21.r013</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.21.r012.js"></script> <script src="<?= $pluginRoot ?>/assets/urbm-2026.06.21.r013.js"></script>
+10 -2
View File
@@ -180,6 +180,13 @@
const runTarget = run => { const runTarget = run => {
if(run.taskType==='backup'||run.taskType==='rsync') return state.config.jobs.find(job=>job.id===run.jobId)?.name || 'Unbekannter Job'; if(run.taskType==='backup'||run.taskType==='rsync') return state.config.jobs.find(job=>job.id===run.jobId)?.name || 'Unbekannter Job';
if(['check','prune'].includes(run.taskType)) return state.config.repositories.find(repo=>repo.id===run.jobId)?.name || 'Unbekanntes Repository'; if(['check','prune'].includes(run.taskType)) return state.config.repositories.find(repo=>repo.id===run.jobId)?.name || 'Unbekanntes Repository';
if(run.taskType==='restore') {
const parts=String(run.jobId||'').split('\u0000');
if(parts.length>=4) {
const repo=state.config.repositories.find(candidate=>candidate.id===parts[0])?.name || 'Repository';
return `${repo} · ${parts[1].slice(0,12)}${parts[2]}`;
}
}
return run.jobId || ''; return run.jobId || '';
}; };
const button = (label, action, kind = '') => `<button class="bu-button ${kind}" type="button" data-action="${esc(action)}"${tipAttr(actionTooltip(action, label))}>${esc(label)}</button>`; const button = (label, action, kind = '') => `<button class="bu-button ${kind}" type="button" data-action="${esc(action)}"${tipAttr(actionTooltip(action, label))}>${esc(label)}</button>`;
@@ -703,7 +710,7 @@
function renderActivity() { return `<div class="bu-toolbar"><div><h2>Aktivitäten</h2><div class="bu-muted">Laufende Aufgaben werden automatisch alle zwei Sekunden aktualisiert.</div></div><div class="bu-actions">${button('Aktualisieren','refresh','primary')}${button('Abgeschlossene Aktivitäten löschen','clear-activity','danger')}</div></div><section class="bu-card">${runsTable(newestRuns(state.runs))}</section>`; } function renderActivity() { return `<div class="bu-toolbar"><div><h2>Aktivitäten</h2><div class="bu-muted">Laufende Aufgaben werden automatisch alle zwei Sekunden aktualisiert.</div></div><div class="bu-actions">${button('Aktualisieren','refresh','primary')}${button('Abgeschlossene Aktivitäten löschen','clear-activity','danger')}</div></div><section class="bu-card">${runsTable(newestRuns(state.runs))}</section>`; }
function progressView(run) { function progressView(run) {
if(run.status==='queued') return '<span class="bu-muted">Wartet in der Warteschlange</span>'; if(run.status==='queued') return '<span class="bu-muted">Wartet in der Warteschlange</span>';
if(run.taskType!=='backup'&&run.taskType!=='rsync'&&!run.progressPercent) return '<span class="bu-muted">Kein Prozentwert verfügbar</span>'; if(!['backup','rsync','restore'].includes(run.taskType)&&!run.progressPercent) return '<span class="bu-muted">Kein Prozentwert verfügbar</span>';
const usbImage=run.status==='running'&&run.currentFile==='urbm-usb-flash.img'; const usbImage=run.status==='running'&&run.currentFile==='urbm-usb-flash.img';
if(usbImage) { if(usbImage) {
const details=[]; const details=[];
@@ -713,12 +720,13 @@
} }
const percent=Math.max(0,Math.min(100,Number(run.progressPercent)||0)); const percent=Math.max(0,Math.min(100,Number(run.progressPercent)||0));
const scanning=run.status==='running'&&percent===0; const scanning=run.status==='running'&&percent===0;
const waitingLabel=run.taskType==='restore'?'Wiederherstellung läuft…':'Wird eingelesen…';
const details=[]; const details=[];
if(run.progressTotal) details.push(`${formatBytes(run.progressBytes)} / ${formatBytes(run.progressTotal)}`); if(run.progressTotal) details.push(`${formatBytes(run.progressBytes)} / ${formatBytes(run.progressTotal)}`);
if(run.progressFileTotal) details.push(`${Number(run.progressFiles||0).toLocaleString()} / ${Number(run.progressFileTotal).toLocaleString()} Dateien`); if(run.progressFileTotal) details.push(`${Number(run.progressFiles||0).toLocaleString()} / ${Number(run.progressFileTotal).toLocaleString()} Dateien`);
if(run.bytesPerSecond) details.push(`${formatBytes(run.bytesPerSecond)}/s`); if(run.bytesPerSecond) details.push(`${formatBytes(run.bytesPerSecond)}/s`);
if(run.secondsRemaining>0) details.push(`Restzeit ${formatEta(run.secondsRemaining)}`); if(run.secondsRemaining>0) details.push(`Restzeit ${formatEta(run.secondsRemaining)}`);
return `<div class="bu-progress-wrap"><div class="bu-progress-label"><strong>${run.status==='paused'?'Pausiert':scanning?'Wird eingelesen…':`${percent.toFixed(1)}%`}</strong><span>${esc(details.join(' · '))}</span></div><div class="bu-progress ${scanning?'indeterminate':''}"><span style="width:${scanning?30:percent}%"></span></div>${run.currentFile?`<div class="bu-current-file" title="${esc(run.currentFile)}">${esc(run.currentFile)}</div>`:''}</div>`; return `<div class="bu-progress-wrap"><div class="bu-progress-label"><strong>${run.status==='paused'?'Pausiert':scanning?waitingLabel:`${percent.toFixed(1)}%`}</strong><span>${esc(details.join(' · '))}</span></div><div class="bu-progress ${scanning?'indeterminate':''}"><span style="width:${scanning?30:percent}%"></span></div>${run.currentFile?`<div class="bu-current-file" title="${esc(run.currentFile)}">${esc(run.currentFile)}</div>`:''}</div>`;
} }
function liveLogView(run) { function liveLogView(run) {
if(!(run.liveLog||[]).length) return ''; if(!(run.liveLog||[]).length) return '';