mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-05-06 04:50:35 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user