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
This commit is contained in:
garethgeorge
2024-07-02 20:03:38 -07:00
parent 64aa4f269e
commit 620caed7e3
11 changed files with 142 additions and 122 deletions
+17 -5
View File
@@ -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`<br>(or, if `$XDG_CONFIG_HOME` is set, `$XDG_CONFIG_HOME/backrest/config.json`) |
| `BACKREST_DATA` | Path to the data directory | `$HOME/.local/share/backrest`<br>(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 | |
+44 -44
View File
@@ -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
+1 -6
View File
@@ -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
}
@@ -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
}
+2 -2
View File
@@ -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
}
+1 -1
View File
@@ -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.
+3 -3
View File
@@ -620,9 +620,9 @@ export class OperationRestore extends Message<OperationRestore> {
/**
* 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<OperationRestore>) {
super();
@@ -634,7 +634,7 @@ export class OperationRestore extends Message<OperationRestore> {
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<BinaryReadOptions>): OperationRestore {
+3 -1
View File
@@ -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 = ({
</Button>
</>
) : null}
<br />
Snapshot ID: {normalizeSnapshotId(operation.snapshotId!)}
<Row gutter={16}>
<Col span={12}>
<Typography.Text strong>Bytes Done/Total</Typography.Text>
+16 -4
View File
@@ -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)}`);
+16 -11
View File
@@ -242,6 +242,17 @@ const RestoreModal = ({
const [form] = Form.useForm<RestoreSnapshotRequest>();
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",
+25 -18
View File
@@ -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: