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: