Log snapshot listing failures
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
70477362baaefac7a0f820a737a7af16efd107828f60a00ed5d9e02a9d2bb316 urbm-2026.07.10.r004-x86_64-1.txz
|
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
9c7e39a06a10ae9557796457e9ed754e9e0dcf65f12be39561276d9fce6cbc0b urbm-2026.07.10.r005-x86_64-1.txz
|
||||||
Vendored
+6
-2
@@ -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.07.10.r004">
|
<!ENTITY version "2026.07.10.r005">
|
||||||
<!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 "70477362baaefac7a0f820a737a7af16efd107828f60a00ed5d9e02a9d2bb316">
|
<!ENTITY packageSHA256 "9c7e39a06a10ae9557796457e9ed754e9e0dcf65f12be39561276d9fce6cbc0b">
|
||||||
]>
|
]>
|
||||||
<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.07.10.r005
|
||||||
|
- Log snapshot file-listing failures from the daemon and PHP bridge to `/var/log/urbm.log` so they appear in the Protokolle tab.
|
||||||
|
- Increase the daemon HTTP write timeout for large snapshot trees and return concrete backend errors instead of generic 500 responses where possible.
|
||||||
|
|
||||||
### 2026.07.10.r004
|
### 2026.07.10.r004
|
||||||
- Add a dedicated Protokolle tab showing the daemon log tail and recent saved run logs.
|
- Add a dedicated Protokolle tab showing the daemon log tail and recent saved run logs.
|
||||||
- Add a read-only `/v1/logs` API endpoint limited to `/var/log/urbm.log` and stored URBM run logs.
|
- Add a read-only `/v1/logs` API endpoint limited to `/var/log/urbm.log` and stored URBM run logs.
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ type Server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
var apiErrorLog *slog.Logger
|
||||||
|
|
||||||
func New(socket string, svc *service.Service, log *slog.Logger) *Server {
|
func New(socket string, svc *service.Service, log *slog.Logger) *Server {
|
||||||
s := &Server{service: svc, log: log, socket: socket}
|
s := &Server{service: svc, log: log, socket: socket}
|
||||||
|
apiErrorLog = log
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("GET /v1/health", s.health)
|
mux.HandleFunc("GET /v1/health", s.health)
|
||||||
mux.HandleFunc("GET /v1/config", s.getConfig)
|
mux.HandleFunc("GET /v1/config", s.getConfig)
|
||||||
@@ -55,7 +57,7 @@ func New(socket string, svc *service.Service, log *slog.Logger) *Server {
|
|||||||
mux.HandleFunc("GET /v1/repositories/{id}/snapshots", s.snapshots)
|
mux.HandleFunc("GET /v1/repositories/{id}/snapshots", s.snapshots)
|
||||||
mux.HandleFunc("GET /v1/repositories/{id}/snapshots/{snapshot}/files", s.snapshotFiles)
|
mux.HandleFunc("GET /v1/repositories/{id}/snapshots/{snapshot}/files", s.snapshotFiles)
|
||||||
mux.HandleFunc("POST /v1/restores", s.restore)
|
mux.HandleFunc("POST /v1/restores", s.restore)
|
||||||
s.http = &http.Server{Handler: requestLog(log, mux), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 5 * time.Minute, IdleTimeout: 30 * time.Second}
|
s.http = &http.Server{Handler: requestLog(log, mux), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 25 * time.Minute, IdleTimeout: 30 * time.Second}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +323,9 @@ func writeError(w http.ResponseWriter, err error) {
|
|||||||
if strings.Contains(message, "already queued") {
|
if strings.Contains(message, "already queued") {
|
||||||
status = 409
|
status = 409
|
||||||
}
|
}
|
||||||
|
if apiErrorLog != nil {
|
||||||
|
apiErrorLog.Warn("api error", "status", status, "error", message)
|
||||||
|
}
|
||||||
writeJSON(w, status, map[string]string{"error": message})
|
writeJSON(w, status, map[string]string{"error": message})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -385,16 +385,24 @@ func (s *Service) RepositoryStats(ctx context.Context) []model.RepositoryStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SnapshotFiles(ctx context.Context, repoID, snapshot, path string) ([]map[string]any, error) {
|
func (s *Service) SnapshotFiles(ctx context.Context, repoID, snapshot, path string) ([]map[string]any, error) {
|
||||||
|
started := time.Now()
|
||||||
repo, ok := s.repository(repoID)
|
repo, ok := s.repository(repoID)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("validation: unknown repository")
|
return nil, errors.New("validation: unknown repository")
|
||||||
}
|
}
|
||||||
mounted, err := s.mounts.Prepare(ctx, repo)
|
mounted, err := s.mounts.Prepare(ctx, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.log.Warn("snapshot file listing failed", "repoID", repoID, "snapshot", snapshot, "path", path, "durationMs", time.Since(started).Milliseconds(), "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer s.mounts.Cleanup(context.Background(), mounted)
|
defer s.mounts.Cleanup(context.Background(), mounted)
|
||||||
return s.restic.List(ctx, mounted.Repository, snapshot, path)
|
items, err := s.restic.List(ctx, mounted.Repository, snapshot, path)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("snapshot file listing failed", "repoID", repoID, "snapshot", snapshot, "path", path, "durationMs", time.Since(started).Milliseconds(), "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.log.Info("snapshot file listing loaded", "repoID", repoID, "snapshot", snapshot, "path", path, "items", len(items), "durationMs", time.Since(started).Milliseconds())
|
||||||
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) TestRepository(ctx context.Context, repoID string, initialize bool) error {
|
func (s *Service) TestRepository(ctx context.Context, repoID string, initialize bool) error {
|
||||||
|
|||||||
+5
-1
@@ -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.07.10.r004">
|
<!ENTITY version "2026.07.10.r005">
|
||||||
<!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.07.10.r005
|
||||||
|
- Log snapshot file-listing failures from the daemon and PHP bridge to `/var/log/urbm.log` so they appear in the Protokolle tab.
|
||||||
|
- Increase the daemon HTTP write timeout for large snapshot trees and return concrete backend errors instead of generic 500 responses where possible.
|
||||||
|
|
||||||
### 2026.07.10.r004
|
### 2026.07.10.r004
|
||||||
- Add a dedicated Protokolle tab showing the daemon log tail and recent saved run logs.
|
- Add a dedicated Protokolle tab showing the daemon log tail and recent saved run logs.
|
||||||
- Add a read-only `/v1/logs` API endpoint limited to `/var/log/urbm.log` and stored URBM run logs.
|
- Add a read-only `/v1/logs` API endpoint limited to `/var/log/urbm.log` and stored URBM run logs.
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ $pluginRoot = '/plugins/urbm';
|
|||||||
<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=20260710r004" alt="URBM-Logo">
|
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260710r004" alt="URBM-Logo">
|
||||||
<div><h1>URBM <span class="bu-version">2026.07.10.r004</span></h1><p>Restic Backup Manager für Unraid</p></div>
|
<div><h1>URBM <span class="bu-version">2026.07.10.r005</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>
|
||||||
@@ -33,4 +33,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.07.10.r004.js"></script>
|
<script src="<?= $pluginRoot ?>/assets/urbm-2026.07.10.r005.js"></script>
|
||||||
|
|||||||
+46
-10
@@ -3,21 +3,37 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
function urbm_bridge_log(string $level, string $message, array $context = []): void
|
||||||
|
{
|
||||||
|
$entry = array_merge([
|
||||||
|
'time' => gmdate('c'),
|
||||||
|
'level' => $level,
|
||||||
|
'component' => 'webgui',
|
||||||
|
'message' => $message,
|
||||||
|
], $context);
|
||||||
|
@file_put_contents('/var/log/urbm.log', json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
|
function urbm_json_error(int $status, string $message): void
|
||||||
|
{
|
||||||
|
http_response_code($status);
|
||||||
|
echo json_encode(['error' => $message], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$socket = '/run/urbm/urbm.sock';
|
$socket = '/run/urbm/urbm.sock';
|
||||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||||
$path = $_GET['path'] ?? '/v1/health';
|
$path = $_GET['path'] ?? '/v1/health';
|
||||||
$isSnapshotFileRequest = preg_match('#/snapshots/[^/]+/files(?:\?|$)#', $path) === 1;
|
$isSnapshotFileRequest = preg_match('#/snapshots/[^/]+/files(?:\?|$)#', $path) === 1;
|
||||||
|
|
||||||
if (!preg_match('#^/v1/[A-Za-z0-9_./?=&%:-]*$#', $path) || str_contains($path, '..')) {
|
if (!preg_match('#^/v1/[A-Za-z0-9_./?=&%:-]*$#', $path) || str_contains($path, '..')) {
|
||||||
http_response_code(400);
|
urbm_bridge_log('warn', 'invalid API path', ['path' => $path]);
|
||||||
echo json_encode(['error' => 'invalid API path']);
|
urbm_json_error(400, 'invalid API path');
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file_exists($socket)) {
|
if (!file_exists($socket)) {
|
||||||
http_response_code(503);
|
urbm_bridge_log('warn', 'daemon socket missing', ['path' => $path, 'socket' => $socket]);
|
||||||
echo json_encode(['error' => 'Der URBM-Daemon läuft nicht']);
|
urbm_json_error(503, 'Der URBM-Daemon läuft nicht');
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$curl = curl_init();
|
$curl = curl_init();
|
||||||
@@ -42,10 +58,30 @@ $error = curl_error($curl);
|
|||||||
curl_close($curl);
|
curl_close($curl);
|
||||||
|
|
||||||
if ($response === false) {
|
if ($response === false) {
|
||||||
http_response_code(502);
|
$message = 'Verbindung zum URBM-Daemon fehlgeschlagen: ' . $error;
|
||||||
echo json_encode(['error' => 'Verbindung zum URBM-Daemon fehlgeschlagen: ' . $error]);
|
urbm_bridge_log('warn', 'daemon request failed', ['path' => $path, 'status' => 502, 'error' => $message]);
|
||||||
exit;
|
urbm_json_error(502, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
http_response_code($status > 0 ? $status : 502);
|
if ($status <= 0) {
|
||||||
|
$message = 'URBM-Daemon lieferte keine HTTP-Antwort';
|
||||||
|
urbm_bridge_log('warn', 'daemon response missing', ['path' => $path, 'status' => 502, 'error' => $message]);
|
||||||
|
urbm_json_error(502, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status >= 400) {
|
||||||
|
$decoded = json_decode((string)$response, true);
|
||||||
|
if (is_array($decoded) && isset($decoded['error']) && is_string($decoded['error'])) {
|
||||||
|
$message = $decoded['error'];
|
||||||
|
} else {
|
||||||
|
$message = trim((string)$response);
|
||||||
|
}
|
||||||
|
if ($message === '') {
|
||||||
|
$message = 'URBM-Daemon lieferte HTTP ' . $status . ' ohne Fehlertext';
|
||||||
|
}
|
||||||
|
urbm_bridge_log('warn', 'api request failed', ['path' => $path, 'status' => $status, 'error' => $message]);
|
||||||
|
urbm_json_error($status, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code($status);
|
||||||
echo $response;
|
echo $response;
|
||||||
|
|||||||
@@ -822,7 +822,7 @@
|
|||||||
if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a==='check'?'Prüfung':'Bereinigung'} wurde eingereiht`); return load(); }
|
if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a==='check'?'Prüfung':'Bereinigung'} wurde eingereiht`); 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=newestSnapshots(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=newestSnapshots(await api(`/v1/repositories/${repo}/snapshots`)); state.snapshotRepo=repo; return render(); }
|
||||||
if (name === 'browse') { state.selectedSnapshot=a; state.restoreIncludes=[]; state.files=[]; state.snapshotTree=null; state.snapshotTreeError=''; state.snapshotTreeExpanded={'/':true}; state.snapshotTreeLoading=true; render(); try { state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${a}/files`); state.snapshotTree=buildSnapshotTree(state.files); } catch(error) { state.snapshotTreeError=`Snapshot-Struktur konnte nicht geladen werden: ${error.message}`; } finally { state.snapshotTreeLoading=false; return render(); } }
|
if (name === 'browse') { state.selectedSnapshot=a; state.restoreIncludes=[]; state.files=[]; state.snapshotTree=null; state.snapshotTreeError=''; state.snapshotTreeExpanded={'/':true}; state.snapshotTreeLoading=true; render(); try { state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${a}/files`); state.snapshotTree=buildSnapshotTree(state.files); } catch(error) { state.snapshotTreeError=`Snapshot-Struktur konnte nicht geladen werden: ${error.message}. Details stehen im Tab Protokolle.`; } finally { state.snapshotTreeLoading=false; return render(); } }
|
||||||
if (name === 'toggle-source-tree') { return toggleSourceTree(decodeURIComponent(a)); }
|
if (name === 'toggle-source-tree') { return toggleSourceTree(decodeURIComponent(a)); }
|
||||||
if (name === 'retry-source-tree') { const path=decodeURIComponent(a); delete state.sourceTreeChildren[path]; state.sourceTreeErrors[path]=''; return toggleSourceTree(path,true); }
|
if (name === 'retry-source-tree') { const path=decodeURIComponent(a); delete state.sourceTreeChildren[path]; state.sourceTreeErrors[path]=''; return toggleSourceTree(path,true); }
|
||||||
if (name === 'toggle-dir-tree') { return toggleDirectoryTree(a, decodeURIComponent(b)); }
|
if (name === 'toggle-dir-tree') { return toggleDirectoryTree(a, decodeURIComponent(b)); }
|
||||||
|
|||||||
Reference in New Issue
Block a user