mirror of
https://github.com/garethgeorge/backrest.git
synced 2025-12-17 02:55:39 +00:00
feat: support keep-all retention policy for append-only backups
This commit is contained in:
@@ -408,6 +408,12 @@ func (s *BackrestHandler) ClearHistory(ctx context.Context, req *connect.Request
|
|||||||
func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error) {
|
func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error) {
|
||||||
data, err := s.logStore.Read(req.Msg.GetRef())
|
data, err := s.logStore.Read(req.Msg.GetRef())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, rotatinglog.ErrFileNotFound) {
|
||||||
|
return connect.NewResponse(&types.BytesValue{
|
||||||
|
Value: []byte(fmt.Sprintf("file associated with log %v not found, it may have rotated out of the log history", req.Msg.GetRef())),
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("get log data %v: %w", req.Msg.GetRef(), err)
|
return nil, fmt.Errorf("get log data %v: %w", req.Msg.GetRef(), err)
|
||||||
}
|
}
|
||||||
return connect.NewResponse(&types.BytesValue{Value: data}), nil
|
return connect.NewResponse(&types.BytesValue{Value: data}), nil
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/garethgeorge/backrest/pkg/restic"
|
"github.com/garethgeorge/backrest/pkg/restic"
|
||||||
"github.com/gitploy-io/cronexpr"
|
"github.com/gitploy-io/cronexpr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BackupTask is a scheduled backup operation.
|
// BackupTask is a scheduled backup operation.
|
||||||
@@ -162,7 +163,7 @@ func backupHelper(ctx context.Context, t Task, orchestrator *Orchestrator, plan
|
|||||||
|
|
||||||
// schedule followup tasks
|
// schedule followup tasks
|
||||||
at := time.Now()
|
at := time.Now()
|
||||||
if plan.Retention != nil {
|
if plan.Retention != nil && !proto.Equal(plan.Retention, &v1.RetentionPolicy{}) {
|
||||||
orchestrator.ScheduleTask(NewOneoffForgetTask(orchestrator, plan, op.SnapshotId, at), TaskPriorityForget)
|
orchestrator.ScheduleTask(NewOneoffForgetTask(orchestrator, plan, op.SnapshotId, at), TaskPriorityForget)
|
||||||
}
|
}
|
||||||
orchestrator.ScheduleTask(NewOneoffIndexSnapshotsTask(orchestrator, plan.Repo, at), TaskPriorityIndexSnapshots)
|
orchestrator.ScheduleTask(NewOneoffIndexSnapshotsTask(orchestrator, plan.Repo, at), TaskPriorityIndexSnapshots)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/garethgeorge/backrest/internal/oplog/indexutil"
|
"github.com/garethgeorge/backrest/internal/oplog/indexutil"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ForgetTask tracks a forget operation.
|
// ForgetTask tracks a forget operation.
|
||||||
@@ -59,7 +60,7 @@ func (t *ForgetTask) Next(now time.Time) *time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ForgetTask) Run(ctx context.Context) error {
|
func (t *ForgetTask) Run(ctx context.Context) error {
|
||||||
if t.plan.Retention == nil {
|
if t.plan.Retention == nil || proto.Equal(t.plan.Retention, &v1.RetentionPolicy{}) {
|
||||||
return errors.New("plan does not have a retention policy")
|
return errors.New("plan does not have a retention policy")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Col,
|
Col,
|
||||||
Collapse,
|
Collapse,
|
||||||
|
FormInstance,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useShowModal } from "../components/ModalManager";
|
import { useShowModal } from "../components/ModalManager";
|
||||||
@@ -329,7 +330,7 @@ export const AddPlanModal = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Plan.retention */}
|
{/* Plan.retention */}
|
||||||
<RetentionPolicyView policy={template?.retention} />
|
<RetentionPolicyView policy={template?.retention} form={form} />
|
||||||
|
|
||||||
|
|
||||||
{/* Plan.hooks */}
|
{/* Plan.hooks */}
|
||||||
@@ -364,17 +365,23 @@ export const AddPlanModal = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
|
const RetentionPolicyView = ({ form, policy }: { policy?: RetentionPolicy, form: FormInstance }) => {
|
||||||
enum PolicyType {
|
enum PolicyType {
|
||||||
TimeBased,
|
TimeBased,
|
||||||
CountBased,
|
CountBased,
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
policy = policy || new RetentionPolicy();
|
policy = policy || new RetentionPolicy();
|
||||||
|
|
||||||
const [policyType, setPolicyType] = useState<PolicyType>(
|
let defaultPolicyType = PolicyType.None;
|
||||||
policy.keepLastN ? PolicyType.CountBased : PolicyType.TimeBased
|
if (policy.keepLastN) {
|
||||||
);
|
defaultPolicyType = PolicyType.CountBased;
|
||||||
|
} else if (policy) {
|
||||||
|
defaultPolicyType = PolicyType.TimeBased;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [policyType, setPolicyType] = useState<PolicyType>(defaultPolicyType);
|
||||||
|
|
||||||
let elem = null;
|
let elem = null;
|
||||||
switch (policyType) {
|
switch (policyType) {
|
||||||
@@ -464,6 +471,8 @@ const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case PolicyType.None:
|
||||||
|
elem = <p>All backups are retained e.g. for append-only repos.</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -474,6 +483,9 @@ const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
|
|||||||
value={policyType}
|
value={policyType}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setPolicyType(e.target.value);
|
setPolicyType(e.target.value);
|
||||||
|
if (e.target.value === PolicyType.None) {
|
||||||
|
form.resetFields(["retention"]);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Radio.Button value={PolicyType.CountBased}>
|
<Radio.Button value={PolicyType.CountBased}>
|
||||||
@@ -486,6 +498,11 @@ const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
|
|||||||
By Time Period
|
By Time Period
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
|
<Radio.Button value={PolicyType.None}>
|
||||||
|
<Tooltip title="All backups will be retained. Note that this may result in slow backups if the set of snapshots grows very large.">
|
||||||
|
None
|
||||||
|
</Tooltip>
|
||||||
|
</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Row>
|
</Row>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -119,10 +119,9 @@ export const App: React.FC = () => {
|
|||||||
<small style={{ color: "rgba(255,255,255,0.3)", fontSize: "0.6em" }}>
|
<small style={{ color: "rgba(255,255,255,0.3)", fontSize: "0.6em" }}>
|
||||||
{config && config.host ? "Host: " + config.host : undefined}
|
{config && config.host ? "Host: " + config.host : undefined}
|
||||||
</small>
|
</small>
|
||||||
</h1>
|
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
style={{ position: "absolute", right: "10px" }}
|
style={{ marginLeft: "10px" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAuthToken("");
|
setAuthToken("");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -130,6 +129,8 @@ export const App: React.FC = () => {
|
|||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
|
</h1>
|
||||||
|
|
||||||
</Header>
|
</Header>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Sider width={300} style={{ background: colorBgContainer }}>
|
<Sider width={300} style={{ background: colorBgContainer }}>
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export const SettingsModal = () => {
|
|||||||
showModal(null);
|
showModal(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const users = config.auth?.users || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -101,17 +103,17 @@ export const SettingsModal = () => {
|
|||||||
labelCol={{ span: 6 }}
|
labelCol={{ span: 6 }}
|
||||||
wrapperCol={{ span: 16 }}
|
wrapperCol={{ span: 16 }}
|
||||||
>
|
>
|
||||||
{config.auth?.users?.length === 0 ? (
|
{users.length > 0 ? null : (
|
||||||
<>
|
<>
|
||||||
<strong>Initial backrest setup! </strong>
|
<strong>Initial backrest setup! </strong>
|
||||||
<p>
|
<p>
|
||||||
Backrest has detected that you do not have any users configured, please add at least one user to secure the web interface.
|
Backrest has detected that you do not have any users configured, please add at least one user to secure the web interface.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can add more users later or can reset users by editing the configuration file.
|
You can add more users later or, if you forget your password, reset users by editing the configuration file (typically in $HOME/.backrest/config.json)
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : null}
|
)}
|
||||||
<Form.Item label="Users" required={true}>
|
<Form.Item label="Users" required={true}>
|
||||||
<Form.List
|
<Form.List
|
||||||
name={["auth", "users"]}
|
name={["auth", "users"]}
|
||||||
@@ -135,7 +137,7 @@ export const SettingsModal = () => {
|
|||||||
<Col span={11}>
|
<Col span={11}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={[field.name, "name"]}
|
name={[field.name, "name"]}
|
||||||
rules={[{ required: true }, { pattern: namePattern, message: "Name must be alphanumeric with dashes or underscores as separators" }]}
|
rules={[{ required: true, message: "Name is required" }, { pattern: namePattern, message: "Name must be alphanumeric with dashes or underscores as separators" }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="Username" />
|
<Input placeholder="Username" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -143,7 +145,7 @@ export const SettingsModal = () => {
|
|||||||
<Col span={11}>
|
<Col span={11}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={[field.name, "passwordBcrypt"]}
|
name={[field.name, "passwordBcrypt"]}
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true, message: "Password is required" }]}
|
||||||
>
|
>
|
||||||
<Input.Password placeholder="Password" onFocus={() => {
|
<Input.Password placeholder="Password" onFocus={() => {
|
||||||
form.setFieldValue(["auth", "users", index, "needsBcrypt"], true);
|
form.setFieldValue(["auth", "users", index, "needsBcrypt"], true);
|
||||||
|
|||||||
Reference in New Issue
Block a user