From 8992e4e75f4e902cd06086f3d60fb9f304bef737 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Tue, 2 Jul 2024 20:03:38 -0700 Subject: [PATCH] fix: improve handling of restore operations - restore operations are split into a new flow - added support displaying restore operation percentage and other details in tree view --- README.md | 22 +++-- gen/go/v1/operations.pb.go | 88 +++++++++---------- internal/api/backresthandler.go | 7 +- .../orchestrator/tasks/taskcollectgarbage.go | 41 +++------ internal/orchestrator/tasks/taskrestore.go | 4 +- proto/v1/operations.proto | 2 +- webui/gen/ts/v1/operations_pb.ts | 6 +- webui/src/components/OperationRow.tsx | 4 +- webui/src/components/OperationTree.tsx | 20 ++++- webui/src/components/SnapshotBrowser.tsx | 27 +++--- webui/src/state/oplog.ts | 43 +++++---- 11 files changed, 142 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index b19616c5..671ec8a2 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Backrest itself is built in Golang (matching restic's implementation) and is shi # Installation -Backrest is packaged as a single executable. It can be run directly on Linux, macOS, and Windows. [restic](https://github.com/restic/restic) will be downloaded and installed in the data directory on first run. +Backrest is packaged as a single executable. It can be run directly on Linux, macOS, and Windows. [restic](https://github.com/restic/restic) will be downloaded and installed on first run. Download options @@ -62,7 +62,7 @@ Download options 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 version of restic matches the version Backrest is tested against. 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 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. ## Running with Docker Compose @@ -234,12 +234,24 @@ To run the binary on login, create a shortcut to the binary and place it in the # Configuration -## Environment Variables +## Environment Variables (Unix) | Variable | Description | Default | | ------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| `BACKREST_PORT` | Port to bind to | 9898 | +| `BACKREST_PORT` | Port to bind to | 127.0.0.1:9898 (or 0.0.0.0:9898 for the docker images) | | `BACKREST_CONFIG` | Path to config file | `$HOME/.config/backrest/config.json`
(or, if `$XDG_CONFIG_HOME` is set, `$XDG_CONFIG_HOME/backrest/config.json`) | | `BACKREST_DATA` | Path to the data directory | `$HOME/.local/share/backrest`
(or, if `$XDG_DATA_HOME` is set, `$XDG_DATA_HOME/backrest`) | -| `BACKREST_RESTIC_COMMAND` | Path to restic binary | Defaults to a Backrest managed version of restic | +| `BACKREST_RESTIC_COMMAND` | Path to restic binary | Defaults to a Backrest managed version of restic at `$XDG_DATA_HOME/backrest/restic-x.x.x` | | `XDG_CACHE_HOME` | Path to the cache directory | | + +## Environment Variables (Windows) + +## Environment Variables (Linux) + +| Variable | Description | Default | +| ------------------------- | --------------------------- | ------------------------------------------------------------------------------------------ | +| `BACKREST_PORT` | Port to bind to | 127.0.0.1:9898 | +| `BACKREST_CONFIG` | Path to config file | `%appdata%\backrest` | +| `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 | | diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index 047fe637..172b80b2 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -777,9 +777,9 @@ type OperationRestore struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // path in the snapshot to restore. - Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` // location to restore it to. - Status *RestoreProgressEntry `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` // status of the restore. + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // path in the snapshot to restore. + Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` // location to restore it to. + LastStatus *RestoreProgressEntry `protobuf:"bytes,3,opt,name=last_status,json=lastStatus,proto3" json:"last_status,omitempty"` // status of the restore. } func (x *OperationRestore) Reset() { @@ -828,9 +828,9 @@ func (x *OperationRestore) GetTarget() string { return "" } -func (x *OperationRestore) GetStatus() *RestoreProgressEntry { +func (x *OperationRestore) GetLastStatus() *RestoreProgressEntry { if x != nil { - return x.Status + return x.LastStatus } return nil } @@ -1050,47 +1050,47 @@ var file_v1_operations_proto_rawDesc = []byte{ 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, 0x70, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 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, 0x30, 0x0a, 0x06, 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, 0x06, 0x73, 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, 0x7d, 0x0a, 0x10, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 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, + 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, 0x7d, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 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 ( @@ -1147,7 +1147,7 @@ var file_v1_operations_proto_depIdxs = []int32{ 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.status:type_name -> v1.RestoreProgressEntry + 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 diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 6f42800b..d47aefb6 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -400,12 +400,7 @@ func (s *BackrestHandler) Restore(ctx context.Context, req *connect.Request[v1.R } at := time.Now() - - flowID, err := tasks.FlowIDForSnapshotID(s.oplog, req.Msg.SnapshotId) - if err != nil { - return nil, fmt.Errorf("failed to get flow ID for snapshot %q: %w", req.Msg.SnapshotId, err) - } - s.orchestrator.ScheduleTask(tasks.NewOneoffRestoreTask(req.Msg.RepoId, req.Msg.PlanId, flowID, at, req.Msg.SnapshotId, req.Msg.Path, req.Msg.Target), tasks.TaskPriorityInteractive+tasks.TaskPriorityDefault) + s.orchestrator.ScheduleTask(tasks.NewOneoffRestoreTask(req.Msg.RepoId, req.Msg.PlanId, 0 /* flowID */, at, req.Msg.SnapshotId, req.Msg.Path, req.Msg.Target), tasks.TaskPriorityInteractive+tasks.TaskPriorityDefault) return connect.NewResponse(&emptypb.Empty{}), nil } diff --git a/internal/orchestrator/tasks/taskcollectgarbage.go b/internal/orchestrator/tasks/taskcollectgarbage.go index 573bf9ef..56d7af77 100644 --- a/internal/orchestrator/tasks/taskcollectgarbage.go +++ b/internal/orchestrator/tasks/taskcollectgarbage.go @@ -13,16 +13,20 @@ import ( const ( gcStartupDelay = 60 * time.Second gcInterval = 24 * time.Hour - // keep operations that are eligible for gc for 30 days OR up to a limit of 100 for any one plan. - // an operation is eligible for gc if: - // - it has no snapshot associated with it - // - it has a forgotten snapshot associated with it - gcHistoryAge = 30 * 24 * time.Hour - gcHistoryMaxCount = 1000 - // keep stats operations for 1 year (they're small and useful for long term trends) - gcHistoryStatsAge = 365 * 24 * time.Hour ) +// gcAgeForOperation returns the age at which an operation is eligible for garbage collection. +func gcAgeForOperation(op *v1.Operation) time.Duration { + switch op.Op.(type) { + // stats, check, and prune operations are kept for a year + case *v1.Operation_OperationStats, *v1.Operation_OperationCheck, *v1.Operation_OperationPrune: + return 365 * 24 * time.Hour + // all other operations are kept for 30 days + default: + return 30 * 24 * time.Hour + } +} + type CollectGarbageTask struct { BaseTask firstRun bool @@ -83,11 +87,8 @@ func (t *CollectGarbageTask) gcOperations(oplog *oplog.OpLog) error { forgot, ok := snapshotForgottenForFlow[op.FlowId] if !ok { // no snapshot associated with this flow; check if it's old enough to be gc'd - maxAgeForType := gcHistoryAge.Milliseconds() - if _, isStats := op.Op.(*v1.Operation_OperationStats); isStats { - maxAgeForType = gcHistoryStatsAge.Milliseconds() - } - if curTime-op.UnixTimeStartMs > maxAgeForType { + maxAgeForOperation := gcAgeForOperation(op) + if curTime-op.UnixTimeStartMs > maxAgeForOperation.Milliseconds() { forgetIDs = append(forgetIDs, op.Id) } } else if forgot { @@ -107,17 +108,3 @@ func (t *CollectGarbageTask) gcOperations(oplog *oplog.OpLog) error { zap.Any("operations_removed", len(forgetIDs))) return nil } - -func (t *CollectGarbageTask) Cancel(withStatus v1.OperationStatus) error { - return nil -} - -func (t *CollectGarbageTask) OperationId() int64 { - return 0 -} - -type gcOpInfo struct { - id int64 // operation ID - timestamp int64 // unix time milliseconds - isStats bool // true if this is a stats operation -} diff --git a/internal/orchestrator/tasks/taskrestore.go b/internal/orchestrator/tasks/taskrestore.go index 9be2c58f..478eb89f 100644 --- a/internal/orchestrator/tasks/taskrestore.go +++ b/internal/orchestrator/tasks/taskrestore.go @@ -77,7 +77,7 @@ func restoreHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner, zap.S().Infof("restore progress: %v", entry) - restoreOp.Status = entry + restoreOp.LastStatus = entry sendWg.Add(1) go func() { @@ -91,7 +91,7 @@ func restoreHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner, if err != nil { return fmt.Errorf("restore failed: %w", err) } - restoreOp.Status = summary + restoreOp.LastStatus = summary return nil } diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index f99f7f18..aaa84dda 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -100,7 +100,7 @@ message OperationCheck { message OperationRestore { string path = 1; // path in the snapshot to restore. string target = 2; // location to restore it to. - RestoreProgressEntry status = 3; // status of the restore. + RestoreProgressEntry last_status = 3; // status of the restore. } // OperationStats tracks a stats operation. diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts index 9547fb65..0b8782dd 100644 --- a/webui/gen/ts/v1/operations_pb.ts +++ b/webui/gen/ts/v1/operations_pb.ts @@ -620,9 +620,9 @@ export class OperationRestore extends Message { /** * status of the restore. * - * @generated from field: v1.RestoreProgressEntry status = 3; + * @generated from field: v1.RestoreProgressEntry last_status = 3; */ - status?: RestoreProgressEntry; + lastStatus?: RestoreProgressEntry; constructor(data?: PartialMessage) { super(); @@ -634,7 +634,7 @@ export class OperationRestore extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "path", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "target", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 3, name: "status", kind: "message", T: RestoreProgressEntry }, + { no: 3, name: "last_status", kind: "message", T: RestoreProgressEntry }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): OperationRestore { diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index b9c60039..e41aa639 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -259,7 +259,7 @@ export const OperationRow = ({ } else if (operation.op.case === "operationRestore") { const restore = operation.op.value; const progress = Math.round((details.percentage || 0) * 10) / 10; - const st = restore.status! || {}; + const st = restore.lastStatus! || {}; body = ( <> @@ -288,6 +288,8 @@ export const OperationRow = ({ ) : null} +
+ Snapshot ID: {normalizeSnapshotId(operation.snapshotId!)} Bytes Done/Total diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 7cd49598..64cd5ac3 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -3,6 +3,7 @@ import { BackupInfo, BackupInfoCollector, colorForStatus, + detailsForOperation, displayTypeToString, getOperations, getTypeForDisplay, @@ -159,15 +160,26 @@ export const OperationTree = ({ ); } else if (b.backupLastStatus.entry.case === "status") { const s = b.backupLastStatus.entry.value; - const percent = Math.floor( - (Number(s.bytesDone) / Number(s.totalBytes)) * 100 - ); + const percent = Number(s.bytesDone / s.totalBytes) * 100; details.push( - `${percent}% processed ${formatBytes( + `${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); + } + if (op.snapshotId) { + details.push(`ID: ${normalizeSnapshotId(op.snapshotId)}`); + } } if (b.snapshotInfo) { details.push(`ID: ${normalizeSnapshotId(b.snapshotInfo.id)}`); diff --git a/webui/src/components/SnapshotBrowser.tsx b/webui/src/components/SnapshotBrowser.tsx index 2ca6982e..19e64cf6 100644 --- a/webui/src/components/SnapshotBrowser.tsx +++ b/webui/src/components/SnapshotBrowser.tsx @@ -242,6 +242,17 @@ const RestoreModal = ({ const [form] = Form.useForm(); const showModal = useShowModal(); + const defaultPath = useMemo(() => { + if (path === pathSeparator) { + return ""; + } + return path + "-backrest-restore-" + normalizeSnapshotId(snapshotId); + }, [path]); + + useEffect(() => { + form.setFieldsValue({ target: defaultPath }); + }, [defaultPath]); + const handleCancel = () => { showModal(null); }; @@ -265,13 +276,6 @@ const RestoreModal = ({ } }; - const defaultPath = useMemo(() => { - if (path === pathSeparator) { - return ""; - } - return path + "-backrest-restore-" + normalizeSnapshotId(snapshotId); - }, [path]); - let targetPath = Form.useWatch("target", form); useEffect(() => { if (!targetPath) { @@ -279,17 +283,18 @@ const RestoreModal = ({ } (async () => { try { - if (targetPath.endsWith(pathSeparator)) { - targetPath = targetPath.slice(0, -1); + let p = targetPath; + if (p.endsWith(pathSeparator)) { + p = p.slice(0, -1); } - const dirname = basename(targetPath); + const dirname = basename(p); const files = await backrestService.pathAutocomplete( new StringValue({ value: dirname }) ); for (const file of files.values) { - if (dirname + file === targetPath) { + if (dirname + file === p) { form.setFields([ { name: "target", diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index 678f7d45..c06fe17c 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -5,7 +5,7 @@ import { OperationStatus, } from "../../gen/ts/v1/operations_pb"; import { GetOperationsRequest, OpSelector } from "../../gen/ts/v1/service_pb"; -import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic_pb"; +import { BackupProgressEntry, ResticSnapshot, RestoreProgressEntry } from "../../gen/ts/v1/restic_pb"; import _ from "lodash"; import { formatDuration, formatTime } from "../lib/formatting"; import { backrestService } from "../api"; @@ -71,13 +71,16 @@ export const getStatusForSelector = async (sel: OpSelector) => { // 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 = ops - .reverse() - .filter((op) => op.status !== OperationStatus.STATUS_PENDING); + ops.sort((a, b) => { + return Number(b.unixTimeStartMs - a.unixTimeStartMs); + }); if (ops.length === 0) { return OperationStatus.STATUS_SUCCESS; } - const flowId = ops[0].flowId; + const flowId = ops.find((op) => op.status !== OperationStatus.STATUS_PENDING)?.flowId; + if (!flowId) { + return OperationStatus.STATUS_SUCCESS; + } for (const op of ops) { if (op.status === OperationStatus.STATUS_PENDING) { continue; @@ -120,6 +123,7 @@ export interface BackupInfo { planId?: string; snapshotId?: string; backupLastStatus?: BackupProgressEntry; + restoreLastStatus?: RestoreProgressEntry; snapshotInfo?: ResticSnapshot; forgotten: boolean; } @@ -224,17 +228,22 @@ export class BackupInfoCollector { statusIdx--; } - let backupLastStatus = undefined; - let snapshotInfo = undefined; - let forgotten = false; - let snapshotId = ""; + let backupLastStatus: BackupProgressEntry | undefined = undefined; + let snapshotInfo: ResticSnapshot | undefined = undefined; + let forgotten: boolean = false; + let snapshotId: string = ""; for (const op of operations) { - if (op.op.case === "operationBackup") { - backupLastStatus = op.op.value.lastStatus; - } else if (op.op.case === "operationIndexSnapshot") { - snapshotInfo = op.op.value.snapshot; - forgotten = op.op.value.forgot || false; - snapshotId = op.op.value.snapshot?.id || ""; + 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; } } @@ -498,9 +507,7 @@ export const detailsForOperation = ( } } else if (op.op.case === "operationRestore") { const restore = op.op.value; - if (restore.status) { - percentage = (restore.status.percentDone || 1) * 100; - } + percentage = (restore.lastStatus?.percentDone || 0) * 100; } break; default: