mirror of
https://github.com/garethgeorge/backrest.git
synced 2025-12-16 10:35:32 +00:00
fix: crashing bug on partial backup (#39)
This commit is contained in:
@@ -193,11 +193,9 @@ func (s *Server) GetOperationEvents(ctx context.Context, req *connect.Request[em
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
if err := resp.Send(event); err != nil {
|
||||||
if err := resp.Send(event); err != nil {
|
errorChan <- fmt.Errorf("failed to send event: %w", err)
|
||||||
errorChan <- fmt.Errorf("failed to send event: %w", err)
|
}
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
s.oplog.Subscribe(&callback)
|
s.oplog.Subscribe(&callback)
|
||||||
defer s.oplog.Unsubscribe(&callback)
|
defer s.oplog.Unsubscribe(&callback)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCa
|
|||||||
|
|
||||||
summary, err := r.repo.Backup(ctx, progressCallback, opts...)
|
summary, err := r.repo.Backup(ctx, progressCallback, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to backup: %w", err)
|
return summary, fmt.Errorf("failed to backup: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.l.Debug("Backup completed", zap.String("repo", r.repoConfig.Id), zap.Duration("duration", time.Since(startTime)))
|
r.l.Debug("Backup completed", zap.String("repo", r.repoConfig.Id), zap.Duration("duration", time.Since(startTime)))
|
||||||
@@ -115,16 +115,12 @@ func (r *RepoOrchestrator) Forget(ctx context.Context, plan *v1.Plan) ([]*v1.Res
|
|||||||
return nil, fmt.Errorf("plan %q has no retention policy", plan.Id)
|
return nil, fmt.Errorf("plan %q has no retention policy", plan.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
l := r.l.With(zap.String("plan", plan.Id))
|
|
||||||
|
|
||||||
l.Debug("Forget snapshots", zap.Any("policy", policy))
|
|
||||||
result, err := r.repo.Forget(
|
result, err := r.repo.Forget(
|
||||||
ctx, protoutil.RetentionPolicyFromProto(plan.Retention),
|
ctx, protoutil.RetentionPolicyFromProto(plan.Retention),
|
||||||
restic.WithFlags("--tag", tagForPlan(plan)), restic.WithFlags("--group-by", "tag"))
|
restic.WithFlags("--tag", tagForPlan(plan)), restic.WithFlags("--group-by", "tag"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get snapshots for repo %v: %w", r.repoConfig.Id, err)
|
return nil, fmt.Errorf("get snapshots for repo %v: %w", r.repoConfig.Id, err)
|
||||||
}
|
}
|
||||||
l.Debug("Forget result", zap.Any("result", result))
|
|
||||||
|
|
||||||
var forgotten []*v1.ResticSnapshot
|
var forgotten []*v1.ResticSnapshot
|
||||||
for _, snapshot := range result.Remove {
|
for _, snapshot := range result.Remove {
|
||||||
@@ -135,6 +131,8 @@ func (r *RepoOrchestrator) Forget(ctx context.Context, plan *v1.Plan) ([]*v1.Res
|
|||||||
forgotten = append(forgotten, snapshotProto)
|
forgotten = append(forgotten, snapshotProto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zap.L().Debug("Forgot snapshots", zap.String("plan", plan.Id), zap.Int("count", len(forgotten)), zap.Any("policy", policy))
|
||||||
|
|
||||||
return forgotten, nil
|
return forgotten, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func (r *Repo) Backup(ctx context.Context, progressCallback func(*BackupProgress
|
|||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
var exitErr *exec.ExitError
|
var exitErr *exec.ExitError
|
||||||
if errors.As(err, &exitErr) {
|
if errors.As(err, &exitErr) {
|
||||||
if exitErr.ExitCode() == 1 {
|
if exitErr.ExitCode() == 3 {
|
||||||
cmdErr = ErrPartialBackup
|
cmdErr = ErrPartialBackup
|
||||||
} else {
|
} else {
|
||||||
cmdErr = fmt.Errorf("exit code %v: %w", exitErr.ExitCode(), ErrBackupFailed)
|
cmdErr = fmt.Errorf("exit code %v: %w", exitErr.ExitCode(), ErrBackupFailed)
|
||||||
@@ -151,7 +151,7 @@ func (r *Repo) Backup(ctx context.Context, progressCallback func(*BackupProgress
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if cmdErr != nil || readErr != nil {
|
if cmdErr != nil || readErr != nil {
|
||||||
return nil, newCmdErrorPreformatted(cmd, output.String(), errors.Join(cmdErr, readErr))
|
return summary, newCmdErrorPreformatted(cmd, output.String(), errors.Join(cmdErr, readErr))
|
||||||
}
|
}
|
||||||
|
|
||||||
return summary, nil
|
return summary, nil
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ export const OperationList = ({
|
|||||||
size="small"
|
size="small"
|
||||||
dataSource={backups}
|
dataSource={backups}
|
||||||
renderItem={(backup) => {
|
renderItem={(backup) => {
|
||||||
const ops = backup.operations;
|
const ops = [...backup.operations];
|
||||||
|
ops.reverse();
|
||||||
return (
|
return (
|
||||||
<Card size="small" style={{ margin: "5px" }}>
|
<Card size="small" style={{ margin: "5px" }}>
|
||||||
{ops.map((op) => {
|
{ops.map((op) => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
BackupInfo,
|
BackupInfo,
|
||||||
BackupInfoCollector,
|
BackupInfoCollector,
|
||||||
|
colorForStatus,
|
||||||
displayTypeToString,
|
displayTypeToString,
|
||||||
getOperations,
|
getOperations,
|
||||||
getTypeForDisplay,
|
getTypeForDisplay,
|
||||||
@@ -260,27 +261,9 @@ const buildTreeDay = (keyPrefix: string, operations: BackupInfo[]): OpTreeNode[]
|
|||||||
|
|
||||||
const buildTreeLeaf = (operations: BackupInfo[]): OpTreeNode[] => {
|
const buildTreeLeaf = (operations: BackupInfo[]): OpTreeNode[] => {
|
||||||
const entries = _.map(operations, (b): OpTreeNode => {
|
const entries = _.map(operations, (b): OpTreeNode => {
|
||||||
let iconColor = "grey";
|
let iconColor = colorForStatus(b.status);
|
||||||
let icon: React.ReactNode | null = <QuestionOutlined />;
|
let icon: React.ReactNode | null = <QuestionOutlined />;
|
||||||
|
|
||||||
switch (b.status) {
|
|
||||||
case OperationStatus.STATUS_PENDING:
|
|
||||||
iconColor = "grey";
|
|
||||||
break;
|
|
||||||
case OperationStatus.STATUS_SUCCESS:
|
|
||||||
iconColor = "green";
|
|
||||||
break;
|
|
||||||
case OperationStatus.STATUS_ERROR:
|
|
||||||
iconColor = "red";
|
|
||||||
break;
|
|
||||||
case OperationStatus.STATUS_INPROGRESS:
|
|
||||||
iconColor = "blue";
|
|
||||||
break;
|
|
||||||
case OperationStatus.STATUS_USER_CANCELLED:
|
|
||||||
iconColor = "orange";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (b.status === OperationStatus.STATUS_ERROR) {
|
if (b.status === OperationStatus.STATUS_ERROR) {
|
||||||
icon = <ExclamationOutlined style={{ color: iconColor }} />;
|
icon = <ExclamationOutlined style={{ color: iconColor }} />;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export class BackupInfoCollector {
|
|||||||
private createBackup(operations: Operation[]): BackupInfo {
|
private createBackup(operations: Operation[]): BackupInfo {
|
||||||
// deduplicate and sort operations.
|
// deduplicate and sort operations.
|
||||||
operations.sort((a, b) => {
|
operations.sort((a, b) => {
|
||||||
return Number(b.unixTimeStartMs - a.unixTimeStartMs);
|
return Number(a.unixTimeStartMs - b.unixTimeStartMs);
|
||||||
});
|
});
|
||||||
|
|
||||||
// use the lowest ID of all operations as the ID of the backup, this will be the first created operation.
|
// use the lowest ID of all operations as the ID of the backup, this will be the first created operation.
|
||||||
@@ -258,6 +258,26 @@ export const displayTypeToString = (type: DisplayType) => {
|
|||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const colorForStatus = (status: OperationStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case OperationStatus.STATUS_PENDING:
|
||||||
|
return "grey";
|
||||||
|
case OperationStatus.STATUS_INPROGRESS:
|
||||||
|
return "blue";
|
||||||
|
case OperationStatus.STATUS_ERROR:
|
||||||
|
return "red";
|
||||||
|
case OperationStatus.STATUS_WARNING:
|
||||||
|
return "orange";
|
||||||
|
case OperationStatus.STATUS_SUCCESS:
|
||||||
|
return "green";
|
||||||
|
case OperationStatus.STATUS_USER_CANCELLED:
|
||||||
|
return "orange";
|
||||||
|
default:
|
||||||
|
return "grey";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// detailsForOperation returns derived display information for a given operation.
|
// detailsForOperation returns derived display information for a given operation.
|
||||||
export const detailsForOperation = (
|
export const detailsForOperation = (
|
||||||
op: Operation
|
op: Operation
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export const AddPlanModal = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let config = await fetchConfig();
|
let config = await fetchConfig();
|
||||||
config.plans = config.plans || [];
|
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new Error("template not found");
|
throw new Error("template not found");
|
||||||
@@ -82,10 +81,9 @@ export const AddPlanModal = ({
|
|||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let plan = await validateForm<Plan>(form);
|
let plan = new Plan(await validateForm<Plan>(form));
|
||||||
|
|
||||||
let config = await fetchConfig();
|
let config = await fetchConfig();
|
||||||
config.plans = config.plans || [];
|
|
||||||
|
|
||||||
// Merge the new plan (or update) into the config
|
// Merge the new plan (or update) into the config
|
||||||
if (template) {
|
if (template) {
|
||||||
@@ -103,6 +101,7 @@ export const AddPlanModal = ({
|
|||||||
showModal(null);
|
showModal(null);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alertsApi.error("Operation failed: " + e.message, 15);
|
alertsApi.error("Operation failed: " + e.message, 15);
|
||||||
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user