mirror of
https://github.com/garethgeorge/backrest.git
synced 2025-12-16 02:25:37 +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) {
|
||||
data, err := s.logStore.Read(req.Msg.GetRef())
|
||||
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 connect.NewResponse(&types.BytesValue{Value: data}), nil
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/garethgeorge/backrest/pkg/restic"
|
||||
"github.com/gitploy-io/cronexpr"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// BackupTask is a scheduled backup operation.
|
||||
@@ -162,7 +163,7 @@ func backupHelper(ctx context.Context, t Task, orchestrator *Orchestrator, plan
|
||||
|
||||
// schedule followup tasks
|
||||
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(NewOneoffIndexSnapshotsTask(orchestrator, plan.Repo, at), TaskPriorityIndexSnapshots)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/garethgeorge/backrest/internal/oplog/indexutil"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Card,
|
||||
Col,
|
||||
Collapse,
|
||||
FormInstance,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useShowModal } from "../components/ModalManager";
|
||||
@@ -329,7 +330,7 @@ export const AddPlanModal = ({
|
||||
</Tooltip>
|
||||
|
||||
{/* Plan.retention */}
|
||||
<RetentionPolicyView policy={template?.retention} />
|
||||
<RetentionPolicyView policy={template?.retention} form={form} />
|
||||
|
||||
|
||||
{/* Plan.hooks */}
|
||||
@@ -364,17 +365,23 @@ export const AddPlanModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
|
||||
const RetentionPolicyView = ({ form, policy }: { policy?: RetentionPolicy, form: FormInstance }) => {
|
||||
enum PolicyType {
|
||||
TimeBased,
|
||||
CountBased,
|
||||
None,
|
||||
}
|
||||
|
||||
policy = policy || new RetentionPolicy();
|
||||
|
||||
const [policyType, setPolicyType] = useState<PolicyType>(
|
||||
policy.keepLastN ? PolicyType.CountBased : PolicyType.TimeBased
|
||||
);
|
||||
let defaultPolicyType = PolicyType.None;
|
||||
if (policy.keepLastN) {
|
||||
defaultPolicyType = PolicyType.CountBased;
|
||||
} else if (policy) {
|
||||
defaultPolicyType = PolicyType.TimeBased;
|
||||
}
|
||||
|
||||
const [policyType, setPolicyType] = useState<PolicyType>(defaultPolicyType);
|
||||
|
||||
let elem = null;
|
||||
switch (policyType) {
|
||||
@@ -464,6 +471,8 @@ const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
|
||||
</Form.Item>
|
||||
);
|
||||
break;
|
||||
case PolicyType.None:
|
||||
elem = <p>All backups are retained e.g. for append-only repos.</p>
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -474,6 +483,9 @@ const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
|
||||
value={policyType}
|
||||
onChange={(e) => {
|
||||
setPolicyType(e.target.value);
|
||||
if (e.target.value === PolicyType.None) {
|
||||
form.resetFields(["retention"]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={PolicyType.CountBased}>
|
||||
@@ -486,6 +498,11 @@ const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
|
||||
By Time Period
|
||||
</Tooltip>
|
||||
</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>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
@@ -119,10 +119,9 @@ export const App: React.FC = () => {
|
||||
<small style={{ color: "rgba(255,255,255,0.3)", fontSize: "0.6em" }}>
|
||||
{config && config.host ? "Host: " + config.host : undefined}
|
||||
</small>
|
||||
</h1>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ position: "absolute", right: "10px" }}
|
||||
style={{ marginLeft: "10px" }}
|
||||
onClick={() => {
|
||||
setAuthToken("");
|
||||
window.location.reload();
|
||||
@@ -130,6 +129,8 @@ export const App: React.FC = () => {
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</h1>
|
||||
|
||||
</Header>
|
||||
<Layout>
|
||||
<Sider width={300} style={{ background: colorBgContainer }}>
|
||||
|
||||
@@ -75,6 +75,8 @@ export const SettingsModal = () => {
|
||||
showModal(null);
|
||||
};
|
||||
|
||||
const users = config.auth?.users || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@@ -101,17 +103,17 @@ export const SettingsModal = () => {
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
>
|
||||
{config.auth?.users?.length === 0 ? (
|
||||
{users.length > 0 ? null : (
|
||||
<>
|
||||
<strong>Initial backrest setup! </strong>
|
||||
<p>
|
||||
Backrest has detected that you do not have any users configured, please add at least one user to secure the web interface.
|
||||
</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>
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
<Form.Item label="Users" required={true}>
|
||||
<Form.List
|
||||
name={["auth", "users"]}
|
||||
@@ -135,7 +137,7 @@ export const SettingsModal = () => {
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
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" />
|
||||
</Form.Item>
|
||||
@@ -143,7 +145,7 @@ export const SettingsModal = () => {
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
name={[field.name, "passwordBcrypt"]}
|
||||
rules={[{ required: true }]}
|
||||
rules={[{ required: true, message: "Password is required" }]}
|
||||
>
|
||||
<Input.Password placeholder="Password" onFocus={() => {
|
||||
form.setFieldValue(["auth", "users", index, "needsBcrypt"], true);
|
||||
|
||||
Reference in New Issue
Block a user