mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-09-26 01:45:43 +00:00
Merge pull request #9 from garethgeorge/optree
feat: implement tree view for backup operations
This commit is contained in:
+31
-20
@@ -78,6 +78,7 @@ const (
|
||||
OperationStatus_STATUS_INPROGRESS OperationStatus = 2
|
||||
OperationStatus_STATUS_SUCCESS OperationStatus = 3
|
||||
OperationStatus_STATUS_ERROR OperationStatus = 4
|
||||
OperationStatus_STATUS_CANCELLED OperationStatus = 5
|
||||
)
|
||||
|
||||
// Enum value maps for OperationStatus.
|
||||
@@ -88,6 +89,7 @@ var (
|
||||
2: "STATUS_INPROGRESS",
|
||||
3: "STATUS_SUCCESS",
|
||||
4: "STATUS_ERROR",
|
||||
5: "STATUS_CANCELLED",
|
||||
}
|
||||
OperationStatus_value = map[string]int32{
|
||||
"STATUS_UNKNOWN": 0,
|
||||
@@ -95,6 +97,7 @@ var (
|
||||
"STATUS_INPROGRESS": 2,
|
||||
"STATUS_SUCCESS": 3,
|
||||
"STATUS_ERROR": 4,
|
||||
"STATUS_CANCELLED": 5,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -177,14 +180,21 @@ type Operation struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
RepoId string `protobuf:"bytes,2,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` // repo id if associated with a repo (always true)
|
||||
PlanId string `protobuf:"bytes,3,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` // plan id if associated with a plan (always true)
|
||||
SnapshotId string `protobuf:"bytes,8,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` // snapshot id if associated with a snapshot.
|
||||
Status OperationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=v1.OperationStatus" json:"status,omitempty"`
|
||||
UnixTimeStartMs int64 `protobuf:"varint,5,opt,name=unix_time_start_ms,json=unixTimeStartMs,proto3" json:"unix_time_start_ms,omitempty"`
|
||||
UnixTimeEndMs int64 `protobuf:"varint,6,opt,name=unix_time_end_ms,json=unixTimeEndMs,proto3" json:"unix_time_end_ms,omitempty"`
|
||||
DisplayMessage string `protobuf:"bytes,7,opt,name=display_message,json=displayMessage,proto3" json:"display_message,omitempty"` // human readable context message (if any)
|
||||
// required, primary ID of the operation.
|
||||
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
// required, repo id if associated with a repo
|
||||
RepoId string `protobuf:"bytes,2,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"`
|
||||
// required, plan id if associated with a plan
|
||||
PlanId string `protobuf:"bytes,3,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"`
|
||||
// optional snapshot id if associated with a snapshot.
|
||||
SnapshotId string `protobuf:"bytes,8,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
|
||||
Status OperationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=v1.OperationStatus" json:"status,omitempty"`
|
||||
// required, unix time in milliseconds of the operation's creation (ID is derived from this)
|
||||
UnixTimeStartMs int64 `protobuf:"varint,5,opt,name=unix_time_start_ms,json=unixTimeStartMs,proto3" json:"unix_time_start_ms,omitempty"`
|
||||
// optional, unix time in milliseconds of the operation's completion
|
||||
UnixTimeEndMs int64 `protobuf:"varint,6,opt,name=unix_time_end_ms,json=unixTimeEndMs,proto3" json:"unix_time_end_ms,omitempty"`
|
||||
// optional, human readable context message, typically an error message.
|
||||
DisplayMessage string `protobuf:"bytes,7,opt,name=display_message,json=displayMessage,proto3" json:"display_message,omitempty"`
|
||||
// Types that are assignable to Op:
|
||||
//
|
||||
// *Operation_OperationBackup
|
||||
@@ -526,18 +536,19 @@ var file_v1_operations_proto_rawDesc = []byte{
|
||||
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, 0x2a, 0x76, 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, 0x10,
|
||||
0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04,
|
||||
0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67,
|
||||
0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74,
|
||||
0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31,
|
||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x8c, 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,
|
||||
0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10,
|
||||
0x04, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43,
|
||||
0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75,
|
||||
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72,
|
||||
0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
+106
-87
@@ -83,9 +83,11 @@ type GetOperationsRequest struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"`
|
||||
PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"`
|
||||
LastN int64 `protobuf:"varint,3,opt,name=last_n,json=lastN,proto3" json:"last_n,omitempty"` // limit to the last n operations
|
||||
RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"`
|
||||
PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"`
|
||||
SnapshotId string `protobuf:"bytes,4,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
|
||||
Ids []int64 `protobuf:"varint,5,rep,packed,name=ids,proto3" json:"ids,omitempty"`
|
||||
LastN int64 `protobuf:"varint,3,opt,name=last_n,json=lastN,proto3" json:"last_n,omitempty"` // limit to the last n operations
|
||||
}
|
||||
|
||||
func (x *GetOperationsRequest) Reset() {
|
||||
@@ -134,6 +136,20 @@ func (x *GetOperationsRequest) GetPlanId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetOperationsRequest) GetSnapshotId() string {
|
||||
if x != nil {
|
||||
return x.SnapshotId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetOperationsRequest) GetIds() []int64 {
|
||||
if x != nil {
|
||||
return x.Ids
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetOperationsRequest) GetLastN() int64 {
|
||||
if x != nil {
|
||||
return x.LastN
|
||||
@@ -395,90 +411,93 @@ var file_v1_service_proto_rawDesc = []byte{
|
||||
0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07,
|
||||
0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70,
|
||||
0x6c, 0x61, 0x6e, 0x49, 0x64, 0x22, 0x5f, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a,
|
||||
0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
|
||||
0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x12,
|
||||
0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e,
|
||||
0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73,
|
||||
0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68,
|
||||
0x22, 0x56, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74,
|
||||
0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a,
|
||||
0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74,
|
||||
0x68, 0x12, 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03,
|
||||
0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
|
||||
0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x01, 0x0a, 0x07, 0x4c, 0x73, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75,
|
||||
0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65,
|
||||
0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x74, 0x69,
|
||||
0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d,
|
||||
0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0x81,
|
||||
0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12, 0x43, 0x0a, 0x09, 0x47,
|
||||
0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||
0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x12, 0x82, 0xd3,
|
||||
0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0a, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43,
|
||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a,
|
||||
0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, 0x0a, 0x07,
|
||||
0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70,
|
||||
0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x1a, 0x82,
|
||||
0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x61, 0x0a, 0x12, 0x47, 0x65, 0x74,
|
||||
0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12,
|
||||
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65,
|
||||
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x1d, 0x82, 0xd3, 0xe4,
|
||||
0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f,
|
||||
0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, 0x12, 0x57, 0x0a, 0x0d,
|
||||
0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65,
|
||||
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93,
|
||||
0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61,
|
||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
|
||||
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70,
|
||||
0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12,
|
||||
0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||
0x74, 0x73, 0x12, 0x70, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68,
|
||||
0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73,
|
||||
0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53,
|
||||
0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01, 0x2a, 0x22,
|
||||
0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x2f, 0x66,
|
||||
0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x12,
|
||||
0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93,
|
||||
0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x62,
|
||||
0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74,
|
||||
0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65,
|
||||
0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e,
|
||||
0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74,
|
||||
0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x76, 0x31,
|
||||
0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2f, 0x70, 0x61,
|
||||
0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65,
|
||||
0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
|
||||
0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x6c, 0x61, 0x6e, 0x49, 0x64, 0x22, 0x92, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65,
|
||||
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17,
|
||||
0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64,
|
||||
0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49,
|
||||
0x64, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x03, 0x52, 0x03,
|
||||
0x69, 0x64, 0x73, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69,
|
||||
0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12,
|
||||
0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x70, 0x61, 0x74, 0x68, 0x22, 0x56, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70,
|
||||
0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73,
|
||||
0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e,
|
||||
0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x01, 0x0a,
|
||||
0x07, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x70, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65,
|
||||
0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65,
|
||||
0x12, 0x14, 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18,
|
||||
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69,
|
||||
0x6d, 0x65, 0x32, 0x81, 0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12,
|
||||
0x43, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67,
|
||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
||||
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
|
||||
0x67, 0x12, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02,
|
||||
0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x12, 0x3b, 0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x52, 0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
|
||||
0x67, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76,
|
||||
0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x61, 0x0a,
|
||||
0x12, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65,
|
||||
0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22,
|
||||
0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65,
|
||||
0x6e, 0x74, 0x73, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01,
|
||||
0x12, 0x57, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19,
|
||||
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6f,
|
||||
0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, 0x0d, 0x4c, 0x69, 0x73,
|
||||
0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63,
|
||||
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x18, 0x82, 0xd3,
|
||||
0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61,
|
||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x70, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e,
|
||||
0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
|
||||
0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18,
|
||||
0x3a, 0x01, 0x2a, 0x22, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||
0x74, 0x73, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b,
|
||||
0x75, 0x70, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e,
|
||||
0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19,
|
||||
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63,
|
||||
0x6d, 0x64, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74,
|
||||
0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e,
|
||||
0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67,
|
||||
0x4c, 0x69, 0x73, 0x74, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22,
|
||||
0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74,
|
||||
0x65, 0x2f, 0x70, 0x61, 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
|
||||
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67,
|
||||
0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
+12
-1
@@ -96,7 +96,7 @@ func (s *Server) AddRepo(ctx context.Context, repo *v1.Repo) (*v1.Config, error)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ListSnapshots implements GET /v1/snapshots/{repo.id}/{plan.id?}
|
||||
// ListSnapshots implements POST /v1/snapshots
|
||||
func (s *Server) ListSnapshots(ctx context.Context, query *v1.ListSnapshotsRequest) (*v1.ResticSnapshotList, error) {
|
||||
repo, err := s.orchestrator.GetRepo(query.RepoId)
|
||||
if err != nil {
|
||||
@@ -200,6 +200,17 @@ func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest
|
||||
ops, err = s.oplog.GetByPlan(req.PlanId, collector)
|
||||
} else if req.RepoId != "" {
|
||||
ops, err = s.oplog.GetByRepo(req.RepoId, collector)
|
||||
} else if req.SnapshotId != "" {
|
||||
ops, err = s.oplog.GetBySnapshotId(req.SnapshotId, collector)
|
||||
} else if len(req.Ids) > 0 {
|
||||
ops = make([]*v1.Operation, 0, len(req.Ids))
|
||||
for i, id := range req.Ids {
|
||||
op, err := s.oplog.Get(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get operation %d: %w", i, err)
|
||||
}
|
||||
ops = append(ops, op)
|
||||
}
|
||||
} else {
|
||||
ops, err = s.oplog.GetAll()
|
||||
}
|
||||
|
||||
+13
-5
@@ -195,17 +195,25 @@ func (o *OpLog) getOperationHelper(b *bolt.Bucket, id int64) (*v1.Operation, err
|
||||
return &op, nil
|
||||
}
|
||||
|
||||
func (o *OpLog) nextOperationId(b *bolt.Bucket, unixTimeMs int64) (int64, error) {
|
||||
seq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("next sequence: %w", err)
|
||||
}
|
||||
return int64(unixTimeMs<<20) | int64(seq&((1<<20)-1)), nil
|
||||
}
|
||||
|
||||
func (o *OpLog) addOperationHelper(tx *bolt.Tx, op *v1.Operation) error {
|
||||
b := tx.Bucket(OpLogBucket)
|
||||
if op.Id == 0 {
|
||||
seq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting next sequence: %w", err)
|
||||
}
|
||||
if op.UnixTimeStartMs == 0 {
|
||||
return fmt.Errorf("operation must have a start time")
|
||||
}
|
||||
op.Id = op.UnixTimeStartMs<<20 | int64(seq&(1<<20-1))
|
||||
var err error
|
||||
op.Id, err = o.nextOperationId(b, op.UnixTimeStartMs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create next operation ID: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
op.SnapshotId = NormalizeSnapshotId(op.SnapshotId)
|
||||
|
||||
@@ -20,6 +20,7 @@ type RepoOrchestrator struct {
|
||||
repo *restic.Repo
|
||||
}
|
||||
|
||||
// newRepoOrchestrator accepts a config and a repo that is configured with the properties of that config object.
|
||||
func newRepoOrchestrator(repoConfig *v1.Repo, repo *restic.Repo) *RepoOrchestrator {
|
||||
return &RepoOrchestrator{
|
||||
repoConfig: repoConfig,
|
||||
@@ -47,6 +48,11 @@ func (r *RepoOrchestrator) SnapshotsForPlan(ctx context.Context, plan *v1.Plan)
|
||||
|
||||
func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCallback func(event *restic.BackupProgressEntry)) (*restic.BackupProgressEntry, error) {
|
||||
zap.L().Debug("repo orchestrator starting backup", zap.String("repo", r.repoConfig.Id))
|
||||
|
||||
if err := r.repo.Init(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to init repo: %w", err)
|
||||
}
|
||||
|
||||
snapshots, err := r.SnapshotsForPlan(ctx, plan)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get snapshots for plan: %w", err)
|
||||
|
||||
@@ -18,15 +18,15 @@ func TestBackup(t *testing.T) {
|
||||
|
||||
// create a new repo with cache disabled for testing
|
||||
r := &v1.Repo{
|
||||
Id: "test",
|
||||
Uri: repo,
|
||||
Id: "test",
|
||||
Uri: repo,
|
||||
Password: "test",
|
||||
Flags: []string{"--no-cache"},
|
||||
Flags: []string{"--no-cache"},
|
||||
}
|
||||
|
||||
|
||||
plan := &v1.Plan{
|
||||
Id: "test",
|
||||
Repo: "test",
|
||||
Id: "test",
|
||||
Repo: "test",
|
||||
Paths: []string{testData},
|
||||
}
|
||||
|
||||
@@ -48,34 +48,34 @@ func TestBackup(t *testing.T) {
|
||||
|
||||
func TestSnapshotParenting(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
repo := t.TempDir()
|
||||
testData := test.CreateTestData(t)
|
||||
|
||||
// create a new repo with cache disabled for testing
|
||||
r := &v1.Repo{
|
||||
Id: "test",
|
||||
Uri: repo,
|
||||
Id: "test",
|
||||
Uri: repo,
|
||||
Password: "test",
|
||||
Flags: []string{"--no-cache"},
|
||||
Flags: []string{"--no-cache"},
|
||||
}
|
||||
|
||||
plans := []*v1.Plan{
|
||||
{
|
||||
Id: "test",
|
||||
Repo: "test",
|
||||
Id: "test",
|
||||
Repo: "test",
|
||||
Paths: []string{testData},
|
||||
},
|
||||
{
|
||||
Id: "test2",
|
||||
Repo: "test",
|
||||
Id: "test2",
|
||||
Repo: "test",
|
||||
Paths: []string{testData},
|
||||
},
|
||||
}
|
||||
|
||||
orchestrator := newRepoOrchestrator(r, restic.NewRepo(r, restic.WithFlags("--no-cache")))
|
||||
|
||||
for i := 0; i < 4; i ++{
|
||||
for i := 0; i < 4; i++ {
|
||||
for _, plan := range plans {
|
||||
summary, err := orchestrator.Backup(context.Background(), plan, nil)
|
||||
if err != nil {
|
||||
@@ -92,7 +92,7 @@ func TestSnapshotParenting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for _, plan := range plans {
|
||||
snapshots, err := orchestrator.SnapshotsForPlan(context.Background(), plan)
|
||||
if err != nil {
|
||||
@@ -105,7 +105,7 @@ func TestSnapshotParenting(t *testing.T) {
|
||||
}
|
||||
|
||||
for i := 1; i < len(snapshots); i++ {
|
||||
prev := snapshots[i - 1]
|
||||
prev := snapshots[i-1]
|
||||
curr := snapshots[i]
|
||||
|
||||
if prev.ToProto().UnixTimeMs >= curr.ToProto().UnixTimeMs {
|
||||
@@ -129,4 +129,4 @@ func TestSnapshotParenting(t *testing.T) {
|
||||
if len(snapshots) != 8 {
|
||||
t.Errorf("expected 8 snapshots, got %d", len(snapshots))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,21 +47,6 @@ func (t *ScheduledBackupTask) Name() string {
|
||||
}
|
||||
|
||||
func (t *ScheduledBackupTask) Next(now time.Time) *time.Time {
|
||||
if ops, err := t.orchestrator.OpLog.GetByPlan(t.plan.Id, indexutil.CollectLastN(10)); err == nil {
|
||||
var lastBackupOp *v1.Operation
|
||||
for _, op := range ops {
|
||||
if _, ok := op.Op.(*v1.Operation_OperationBackup); ok {
|
||||
lastBackupOp = op
|
||||
}
|
||||
}
|
||||
|
||||
if lastBackupOp != nil {
|
||||
now = time.Unix(0, lastBackupOp.UnixTimeEndMs*int64(time.Millisecond))
|
||||
}
|
||||
} else {
|
||||
zap.S().Errorf("error getting last operation for plan %q when computing backup schedule: %v", t.plan.Id, err)
|
||||
}
|
||||
|
||||
next := t.schedule.Next(now)
|
||||
return &next
|
||||
}
|
||||
@@ -116,7 +101,7 @@ func backupHelper(ctx context.Context, orchestrator *Orchestrator, plan *v1.Plan
|
||||
startTime := time.Now()
|
||||
|
||||
err := WithOperation(orchestrator.OpLog, op, func() error {
|
||||
zap.L().Info("Starting backup", zap.String("plan", plan.Id))
|
||||
zap.L().Info("Starting backup", zap.String("plan", plan.Id), zap.Int64("opId", op.Id))
|
||||
repo, err := orchestrator.GetRepo(plan.Repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't get repo %q: %w", plan.Repo, err)
|
||||
@@ -124,7 +109,7 @@ func backupHelper(ctx context.Context, orchestrator *Orchestrator, plan *v1.Plan
|
||||
|
||||
lastSent := time.Now() // debounce progress updates, these can endup being very frequent.
|
||||
summary, err := repo.Backup(ctx, plan, func(entry *restic.BackupProgressEntry) {
|
||||
if time.Since(lastSent) < 200*time.Millisecond {
|
||||
if time.Since(lastSent) < 250*time.Millisecond {
|
||||
return
|
||||
}
|
||||
lastSent = time.Now()
|
||||
@@ -230,7 +215,7 @@ func WithOperation(oplog *oplog.OpLog, op *v1.Operation, do func() error) error
|
||||
if op.Status == v1.OperationStatus_STATUS_INPROGRESS {
|
||||
op.Status = v1.OperationStatus_STATUS_SUCCESS
|
||||
}
|
||||
if e := oplog.Update(op); err != nil {
|
||||
if e := oplog.Update(op); e != nil {
|
||||
return multierror.Append(err, fmt.Errorf("failed to update operation in oplog: %w", e))
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -77,7 +77,6 @@ func (r *Repo) init(ctx context.Context) error {
|
||||
func (r *Repo) Init(ctx context.Context) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.initialized = false
|
||||
return r.init(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,18 +11,20 @@ message OperationList {
|
||||
}
|
||||
|
||||
message Operation {
|
||||
// required, primary ID of the operation.
|
||||
int64 id = 1;
|
||||
// repo id if associated with a repo (always true)
|
||||
// required, repo id if associated with a repo
|
||||
string repo_id = 2;
|
||||
// plan id if associated with a plan (always true)
|
||||
// required, plan id if associated with a plan
|
||||
string plan_id = 3;
|
||||
// snapshot id if associated with a snapshot.
|
||||
// optional snapshot id if associated with a snapshot.
|
||||
string snapshot_id = 8;
|
||||
OperationStatus status = 4;
|
||||
// unix time in milliseconds of the operation's creation (ID is derived from this)
|
||||
// required, unix time in milliseconds of the operation's creation (ID is derived from this)
|
||||
int64 unix_time_start_ms = 5;
|
||||
// optional, unix time in milliseconds of the operation's completion
|
||||
int64 unix_time_end_ms = 6;
|
||||
// human readable context message, typically an error message.
|
||||
// optional, human readable context message, typically an error message.
|
||||
string display_message = 7;
|
||||
|
||||
oneof op {
|
||||
@@ -50,6 +52,7 @@ enum OperationStatus {
|
||||
STATUS_INPROGRESS = 2;
|
||||
STATUS_SUCCESS = 3;
|
||||
STATUS_ERROR = 4;
|
||||
STATUS_CANCELLED = 5;
|
||||
}
|
||||
|
||||
message OperationBackup {
|
||||
|
||||
@@ -84,6 +84,8 @@ message ListSnapshotsRequest {
|
||||
message GetOperationsRequest {
|
||||
string repo_id = 1;
|
||||
string plan_id = 2;
|
||||
string snapshot_id = 4;
|
||||
repeated int64 ids = 5;
|
||||
int64 last_n = 3; // limit to the last n operations
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export enum OperationStatus {
|
||||
STATUS_INPROGRESS = "STATUS_INPROGRESS",
|
||||
STATUS_SUCCESS = "STATUS_SUCCESS",
|
||||
STATUS_ERROR = "STATUS_ERROR",
|
||||
STATUS_CANCELLED = "STATUS_CANCELLED",
|
||||
}
|
||||
|
||||
export type OperationList = {
|
||||
|
||||
@@ -18,6 +18,8 @@ export type ListSnapshotsRequest = {
|
||||
export type GetOperationsRequest = {
|
||||
repoId?: string
|
||||
planId?: string
|
||||
snapshotId?: string
|
||||
ids?: string[]
|
||||
lastN?: string
|
||||
}
|
||||
|
||||
|
||||
Generated
+116
@@ -887,6 +887,17 @@
|
||||
"react-refresh": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"@parcel/transformer-sass": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/transformer-sass/-/transformer-sass-2.10.3.tgz",
|
||||
"integrity": "sha512-Q8pTsMO+YuczBjmW8NdFcUjYpCdvY6EzYhPYw4eTyvldalGkaUVs1mJoKmWDHIDsIpdiaARxTpXP946B9cgE3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@parcel/plugin": "2.10.3",
|
||||
"@parcel/source-map": "^2.1.1",
|
||||
"sass": "^1.38.0"
|
||||
}
|
||||
},
|
||||
"@parcel/transformer-svg": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.10.3.tgz",
|
||||
@@ -1224,6 +1235,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.202",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
|
||||
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "20.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz",
|
||||
@@ -1328,6 +1344,16 @@
|
||||
"throttle-debounce": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -1356,6 +1382,12 @@
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
},
|
||||
"boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
@@ -1408,6 +1440,22 @@
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.3.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
}
|
||||
},
|
||||
"chrome-trace-event": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
|
||||
@@ -1606,11 +1654,27 @@
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"get-port": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz",
|
||||
"integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw=="
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "13.23.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
|
||||
@@ -1655,6 +1719,12 @@
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz",
|
||||
"integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==",
|
||||
"dev": true
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -1675,6 +1745,15 @@
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -1830,6 +1909,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -1916,6 +2000,12 @@
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
||||
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
@@ -2460,6 +2550,15 @@
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"recoil": {
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
|
||||
@@ -2488,6 +2587,17 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.69.5",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz",
|
||||
"integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
@@ -2517,6 +2627,12 @@
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true
|
||||
},
|
||||
"srcset": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz",
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"antd": "^5.11.1",
|
||||
"buffer": "^6.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"parcel": "^2.10.2",
|
||||
"process": "^0.11.10",
|
||||
"react": "^18.2.0",
|
||||
@@ -25,6 +27,7 @@
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/transformer-sass": "^2.10.3",
|
||||
"events": "^3.3.0",
|
||||
"stream-browserify": "^3.0.0"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Operation, OperationStatus } from "../../gen/ts/v1/operations.pb";
|
||||
import {
|
||||
Card,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
List,
|
||||
Progress,
|
||||
Row,
|
||||
Spin,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {
|
||||
@@ -16,7 +17,12 @@ import {
|
||||
SaveOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb";
|
||||
import { EOperation } from "../state/oplog";
|
||||
import {
|
||||
EOperation,
|
||||
buildOperationListListener,
|
||||
subscribeToOperations,
|
||||
unsubscribeFromOperations,
|
||||
} from "../state/oplog";
|
||||
import { SnapshotBrowser } from "./SnapshotBrowser";
|
||||
import {
|
||||
formatBytes,
|
||||
@@ -24,11 +30,24 @@ import {
|
||||
formatTime,
|
||||
normalizeSnapshotId,
|
||||
} from "../lib/formatting";
|
||||
import _ from "lodash";
|
||||
import { GetOperationsRequest } from "../../gen/ts/v1/service.pb";
|
||||
|
||||
export const OperationList = ({
|
||||
operations,
|
||||
}: React.PropsWithoutRef<{ operations: EOperation[] }>) => {
|
||||
operations.sort((a, b) => b.parsedTime - a.parsedTime);
|
||||
req,
|
||||
}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => {
|
||||
const [operations, setOperations] = useState<EOperation[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const lis = buildOperationListListener(req, (event, operation, opList) => {
|
||||
setOperations(opList);
|
||||
});
|
||||
subscribeToOperations(lis);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromOperations(lis);
|
||||
};
|
||||
}, [JSON.stringify(req)]);
|
||||
|
||||
if (operations.length === 0) {
|
||||
return (
|
||||
@@ -39,27 +58,20 @@ export const OperationList = ({
|
||||
);
|
||||
}
|
||||
|
||||
const groupBy = (ops: EOperation[], keyFunc: (op: EOperation) => string) => {
|
||||
const groups: { [key: string]: EOperation[] } = {};
|
||||
|
||||
ops.forEach((op) => {
|
||||
const key = keyFunc(op);
|
||||
if (!(key in groups)) {
|
||||
groups[key] = [];
|
||||
}
|
||||
groups[key].push(op);
|
||||
});
|
||||
|
||||
return Object.values(groups);
|
||||
};
|
||||
|
||||
// groups items by snapshotID if one can be identified, otherwise by operation ID.
|
||||
const groupedItems = groupBy(operations, (op: EOperation) => {
|
||||
return getSnapshotId(op) || op.id!;
|
||||
});
|
||||
const groupedItems = _.values(
|
||||
_.groupBy(operations, (op: EOperation) => {
|
||||
return getSnapshotId(op) || op.id!;
|
||||
})
|
||||
);
|
||||
groupedItems.sort((a, b) => {
|
||||
return b[0].parsedTime - a[0].parsedTime;
|
||||
});
|
||||
groupedItems.forEach((group) => {
|
||||
group.sort((a, b) => {
|
||||
return b.parsedTime - a.parsedTime;
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<List
|
||||
@@ -68,13 +80,13 @@ export const OperationList = ({
|
||||
dataSource={groupedItems}
|
||||
renderItem={(group, index) => {
|
||||
if (group.length === 1) {
|
||||
return <OperationRow key={group[0].parsedId} operation={group[0]} />;
|
||||
return <OperationRow key={group[0].id!} operation={group[0]} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card size="small" style={{ margin: "5px" }}>
|
||||
{group.map((op) => (
|
||||
<OperationRow key={op.parsedId} operation={op} />
|
||||
<OperationRow key={op.id!} operation={op} />
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
@@ -82,7 +94,7 @@ export const OperationList = ({
|
||||
pagination={
|
||||
operations.length > 50
|
||||
? { position: "both", align: "center", defaultPageSize: 50 }
|
||||
: {}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
BackupInfo,
|
||||
BackupInfoCollector,
|
||||
EOperation,
|
||||
getOperations,
|
||||
subscribeToOperations,
|
||||
toEop,
|
||||
unsubscribeFromOperations,
|
||||
} from "../state/oplog";
|
||||
import { Col, Empty, Row, Tree } from "antd";
|
||||
import _ from "lodash";
|
||||
import { DataNode } from "antd/es/tree";
|
||||
import {
|
||||
formatBytes,
|
||||
formatDate,
|
||||
formatDuration,
|
||||
formatTime,
|
||||
normalizeSnapshotId,
|
||||
} from "../lib/formatting";
|
||||
import {
|
||||
ExclamationOutlined,
|
||||
QuestionOutlined,
|
||||
SaveOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { OperationEvent, OperationStatus } from "../../gen/ts/v1/operations.pb";
|
||||
import { useAlertApi } from "./Alerts";
|
||||
import { OperationList } from "./OperationList";
|
||||
import { GetOperationsRequest } from "../../gen/ts/v1/service.pb";
|
||||
|
||||
type OpTreeNode = DataNode & {
|
||||
backup?: BackupInfo;
|
||||
};
|
||||
|
||||
export const OperationTree = ({
|
||||
req,
|
||||
}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => {
|
||||
console.log("Loading operation tree with req: ", req);
|
||||
const alertApi = useAlertApi();
|
||||
const [backups, setBackups] = useState<BackupInfo[]>([]);
|
||||
const [selectedBackupId, setSelectedBackupId] = useState<string | null>(null);
|
||||
|
||||
// track backups for this operation tree view.
|
||||
useEffect(() => {
|
||||
setSelectedBackupId(null);
|
||||
const backupCollector = new BackupInfoCollector();
|
||||
const lis = (opEvent: OperationEvent) => {
|
||||
if (!!req.planId && opEvent.operation!.planId !== req.planId) {
|
||||
return;
|
||||
}
|
||||
if (!!req.repoId && opEvent.operation!.repoId !== req.repoId) {
|
||||
return;
|
||||
}
|
||||
backupCollector.addOperation(opEvent.type!, opEvent.operation!);
|
||||
};
|
||||
subscribeToOperations(lis);
|
||||
|
||||
backupCollector.subscribe(() => {
|
||||
const backups = backupCollector.getAll();
|
||||
backups.sort((a, b) => {
|
||||
return b.startTimeMs - a.startTimeMs;
|
||||
});
|
||||
setBackups(backups);
|
||||
});
|
||||
|
||||
getOperations(req)
|
||||
.then((ops) => {
|
||||
backupCollector.bulkAddOperations(ops);
|
||||
})
|
||||
.catch((e) => {
|
||||
alertApi!.error("Failed to fetch operations: " + e.message);
|
||||
});
|
||||
return () => {
|
||||
unsubscribeFromOperations(lis);
|
||||
};
|
||||
}, [req]);
|
||||
|
||||
if (backups.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<QuestionOutlined /> No operations yet.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const treeData = buildTreeYear(backups);
|
||||
|
||||
let oplist: React.ReactNode | null = null;
|
||||
if (selectedBackupId) {
|
||||
const backup = backups.find((b) => b.id === selectedBackupId);
|
||||
if (!backup) {
|
||||
oplist = <Empty description="Backup not found." />;
|
||||
} else {
|
||||
const req: GetOperationsRequest = {
|
||||
ids: backup.operations.map((op) => op.id!),
|
||||
};
|
||||
oplist = (
|
||||
<>
|
||||
<h3>Backup at {formatTime(backup.displayTime)}</h3>
|
||||
<OperationList req={req} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<h3>Browse Backups</h3>
|
||||
<Tree<OpTreeNode>
|
||||
treeData={treeData}
|
||||
showIcon
|
||||
defaultExpandedKeys={[backups[0].id!]}
|
||||
onSelect={(keys, info) => {
|
||||
setSelectedBackupId(
|
||||
info.selectedNodes.length > 0
|
||||
? info.selectedNodes[0].backup!.id!
|
||||
: null
|
||||
);
|
||||
}}
|
||||
titleRender={(node: OpTreeNode): React.ReactNode => {
|
||||
if (node.title) {
|
||||
return <>{node.title}</>;
|
||||
}
|
||||
if (node.backup) {
|
||||
const b = node.backup;
|
||||
const details: string[] = [];
|
||||
|
||||
if (b.backupLastStatus) {
|
||||
if (b.backupLastStatus.summary) {
|
||||
const s = b.backupLastStatus.summary;
|
||||
details.push(
|
||||
`${formatBytes(s.totalBytesProcessed)} in ${formatDuration(
|
||||
s.totalDuration!
|
||||
)}`
|
||||
);
|
||||
} else if (b.backupLastStatus.status) {
|
||||
const s = b.backupLastStatus.status;
|
||||
const bytesDone = parseInt(s.bytesDone!);
|
||||
const bytesTotal = parseInt(s.totalBytes!);
|
||||
const percent = Math.floor((bytesDone / bytesTotal) * 100);
|
||||
details.push(
|
||||
`${percent}% processed ${formatBytes(
|
||||
bytesDone
|
||||
)} / ${formatBytes(bytesTotal)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (b.snapshotInfo) {
|
||||
details.push(`ID: ${normalizeSnapshotId(b.snapshotInfo.id!)}`);
|
||||
}
|
||||
|
||||
let detailsElem: React.ReactNode | null = null;
|
||||
if (details.length > 0) {
|
||||
detailsElem = (
|
||||
<span className="resticui backup-details">
|
||||
[{details.join(", ")}]
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
Backup {formatTime(b.displayTime)} {detailsElem}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span>ERROR: this element should not appear, this is a bug.</span>
|
||||
);
|
||||
}}
|
||||
></Tree>
|
||||
</Col>
|
||||
<Col span={12}>{oplist}</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
const buildTreeYear = (operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const grouped = _.groupBy(operations, (op) => {
|
||||
return op.displayTime.getFullYear();
|
||||
});
|
||||
|
||||
const entries: OpTreeNode[] = _.map(grouped, (value, key) => {
|
||||
return {
|
||||
key: "y" + key,
|
||||
title: "" + key,
|
||||
children: buildTreeMonth(value),
|
||||
};
|
||||
});
|
||||
entries.sort(sortByKey);
|
||||
return entries;
|
||||
};
|
||||
|
||||
const buildTreeMonth = (operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const grouped = _.groupBy(operations, (op) => {
|
||||
return `y${op.displayTime.getFullYear()}m${op.displayTime.getMonth()}`;
|
||||
});
|
||||
const entries: OpTreeNode[] = _.map(grouped, (value, key) => {
|
||||
return {
|
||||
key: key,
|
||||
title: value[0].displayTime.toLocaleString("default", {
|
||||
month: "long",
|
||||
}),
|
||||
children: buildTreeDay(value),
|
||||
};
|
||||
});
|
||||
entries.sort(sortByKey);
|
||||
return entries;
|
||||
};
|
||||
|
||||
const buildTreeDay = (operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const grouped = _.groupBy(operations, (op) => {
|
||||
return `y${op.displayTime.getFullYear()}m${op.displayTime.getMonth()}d${op.displayTime.getDate()}`;
|
||||
});
|
||||
|
||||
const entries = _.map(grouped, (value, key) => {
|
||||
return {
|
||||
key: "d" + key,
|
||||
title: formatDate(value[0].displayTime),
|
||||
children: buildTreeLeaf(value),
|
||||
};
|
||||
});
|
||||
entries.sort(sortByKey);
|
||||
return entries;
|
||||
};
|
||||
|
||||
const buildTreeLeaf = (operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const entries = _.map(operations, (b): OpTreeNode => {
|
||||
let iconColor = "grey";
|
||||
let icon: React.ReactNode | null = <QuestionOutlined />;
|
||||
|
||||
switch (b.status) {
|
||||
case OperationStatus.STATUS_PENDING:
|
||||
iconColor = "grey";
|
||||
break;
|
||||
case OperationStatus.STATUS_SUCCESS:
|
||||
iconColor = "green";
|
||||
break;
|
||||
case OperationStatus.STATUS_ERROR:
|
||||
iconColor = "red";
|
||||
break;
|
||||
case OperationStatus.STATUS_INPROGRESS:
|
||||
iconColor = "blue";
|
||||
break;
|
||||
case OperationStatus.STATUS_CANCELLED:
|
||||
iconColor = "orange";
|
||||
break;
|
||||
}
|
||||
|
||||
if (b.status === OperationStatus.STATUS_ERROR) {
|
||||
icon = <ExclamationOutlined style={{ color: iconColor }} />;
|
||||
} else {
|
||||
icon = <SaveOutlined style={{ color: iconColor }} />;
|
||||
}
|
||||
|
||||
return {
|
||||
key: b.id!,
|
||||
backup: b,
|
||||
icon: icon,
|
||||
};
|
||||
});
|
||||
entries.sort(sortByKey);
|
||||
return entries;
|
||||
};
|
||||
|
||||
const sortByKey = (a: OpTreeNode, b: OpTreeNode) => {
|
||||
if (a.key < b.key) {
|
||||
return 1;
|
||||
} else if (a.key > b.key) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
@@ -20,6 +20,9 @@ const replaceKeyInTree = (
|
||||
if (curNode.key === setKey) {
|
||||
return setValue;
|
||||
}
|
||||
if (setKey.indexOf(curNode.key as string) === -1) {
|
||||
return null;
|
||||
}
|
||||
if (!curNode.children) {
|
||||
return null;
|
||||
}
|
||||
@@ -83,8 +86,6 @@ export const SnapshotBrowser = ({
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Loading data for key: " + key);
|
||||
|
||||
const resp = await ResticUI.ListSnapshotFiles(
|
||||
{
|
||||
path: (key + "/") as string,
|
||||
@@ -103,33 +104,20 @@ export const SnapshotBrowser = ({
|
||||
}
|
||||
|
||||
if (!toUpdate) {
|
||||
console.log("No node to update found!");
|
||||
return;
|
||||
}
|
||||
|
||||
const toUpdateCopy = { ...toUpdate };
|
||||
toUpdateCopy.children = respToNodes(resp);
|
||||
|
||||
console.log(
|
||||
"Replacing key: " +
|
||||
key +
|
||||
" with: " +
|
||||
JSON.stringify(toUpdateCopy, null, 2)
|
||||
);
|
||||
console.log("In tree: " + JSON.stringify(treeData, null, 2));
|
||||
|
||||
const newTree = treeData.map((node) => {
|
||||
console.log("trying replace in tree...");
|
||||
const didUpdate = replaceKeyInTree(node, key as string, toUpdateCopy);
|
||||
if (didUpdate) {
|
||||
console.log("Replaced in tree successfully!");
|
||||
return didUpdate;
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
console.log("New tree: ", JSON.stringify(newTree, null, 2));
|
||||
|
||||
setTreeData(newTree);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const API_PREFIX = "/api";
|
||||
export const MAX_OPERATION_HISTORY = 10000;
|
||||
@@ -1,3 +0,0 @@
|
||||
body {
|
||||
margin: 0px;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>ResticUI</title>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<link rel="stylesheet" href="index.sass" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
body
|
||||
margin: 0px
|
||||
|
||||
.resticui
|
||||
&.backup-details
|
||||
color: grey
|
||||
font-family: monospace
|
||||
font-size: 0.7em
|
||||
@@ -16,14 +16,35 @@ export const formatBytes = (bytes?: number | string) => {
|
||||
};
|
||||
|
||||
const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000;
|
||||
export const formatTime = (time: number | string) => {
|
||||
// formatTime formats a time as YYYY-MM-DD at HH:MM AM/PM
|
||||
export const formatTime = (time: number | string | Date) => {
|
||||
if (typeof time === "string") {
|
||||
time = parseInt(time);
|
||||
} else if (time instanceof Date) {
|
||||
time = time.getTime();
|
||||
}
|
||||
const d = new Date();
|
||||
d.setTime(time - timezoneOffsetMs);
|
||||
const isoStr = d.toISOString();
|
||||
return `${isoStr.substring(0, 10)} ${d.getUTCHours()}h${d.getUTCMinutes()}m`;
|
||||
const hours = d.getUTCHours() % 12 == 0 ? 12 : d.getUTCHours() % 12;
|
||||
const minutes =
|
||||
d.getUTCMinutes() < 10 ? "0" + d.getUTCMinutes() : d.getUTCMinutes();
|
||||
return `${isoStr.substring(0, 10)} at ${hours}:${minutes} ${
|
||||
d.getUTCHours() > 12 ? "PM" : "AM"
|
||||
}`;
|
||||
};
|
||||
|
||||
// formatDate formats a time as YYYY-MM-DD
|
||||
export const formatDate = (time: number | string | Date) => {
|
||||
if (typeof time === "string") {
|
||||
time = parseInt(time);
|
||||
} else if (time instanceof Date) {
|
||||
time = time.getTime();
|
||||
}
|
||||
let d = new Date();
|
||||
d.setTime(time - timezoneOffsetMs);
|
||||
const isoStr = d.toISOString();
|
||||
return isoStr.substring(0, 10);
|
||||
};
|
||||
|
||||
export const formatDuration = (ms: number) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { atom, useSetRecoilState } from "recoil";
|
||||
import { Config, Repo } from "../../gen/ts/v1/config.pb";
|
||||
import { ResticUI } from "../../gen/ts/v1/service.pb";
|
||||
import { API_PREFIX } from "../constants";
|
||||
|
||||
export const configState = atom<Config>({
|
||||
key: "config",
|
||||
@@ -8,17 +9,17 @@ export const configState = atom<Config>({
|
||||
});
|
||||
|
||||
export const fetchConfig = async (): Promise<Config> => {
|
||||
return await ResticUI.GetConfig({}, { pathPrefix: "/api" });
|
||||
return await ResticUI.GetConfig({}, { pathPrefix: API_PREFIX });
|
||||
};
|
||||
|
||||
export const addRepo = async (repo: Repo): Promise<Config> => {
|
||||
return await ResticUI.AddRepo(repo, {
|
||||
pathPrefix: "/api",
|
||||
pathPrefix: API_PREFIX,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateConfig = async (config: Config): Promise<Config> => {
|
||||
return await ResticUI.SetConfig(config, {
|
||||
pathPrefix: "/api",
|
||||
pathPrefix: API_PREFIX,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
+158
-27
@@ -1,4 +1,3 @@
|
||||
import { atom } from "recoil";
|
||||
import {
|
||||
Operation,
|
||||
OperationEvent,
|
||||
@@ -6,12 +5,13 @@ import {
|
||||
OperationStatus,
|
||||
} from "../../gen/ts/v1/operations.pb";
|
||||
import { GetOperationsRequest, ResticUI } from "../../gen/ts/v1/service.pb";
|
||||
import { EventEmitter } from "events";
|
||||
import { useAlertApi } from "../components/Alerts";
|
||||
import { API_PREFIX } from "../constants";
|
||||
import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb";
|
||||
import _ from "lodash";
|
||||
|
||||
export type EOperation = Operation & {
|
||||
parsedId: number;
|
||||
parsedTime: number;
|
||||
parsedDate: Date;
|
||||
};
|
||||
|
||||
const subscribers: ((event: OperationEvent) => void)[] = [];
|
||||
@@ -28,7 +28,7 @@ const subscribers: ((event: OperationEvent) => void)[] = [];
|
||||
subscribers.forEach((subscriber) => subscriber(event));
|
||||
},
|
||||
{
|
||||
pathPrefix: "/api",
|
||||
pathPrefix: API_PREFIX,
|
||||
}
|
||||
);
|
||||
} catch (e: any) {
|
||||
@@ -40,21 +40,12 @@ const subscribers: ((event: OperationEvent) => void)[] = [];
|
||||
}
|
||||
})();
|
||||
|
||||
export const getOperations = async ({
|
||||
planId,
|
||||
repoId,
|
||||
lastN,
|
||||
}: GetOperationsRequest): Promise<EOperation[]> => {
|
||||
const opList = await ResticUI.GetOperations(
|
||||
{
|
||||
planId,
|
||||
repoId,
|
||||
lastN,
|
||||
},
|
||||
{
|
||||
pathPrefix: "/api",
|
||||
}
|
||||
);
|
||||
export const getOperations = async (
|
||||
req: GetOperationsRequest
|
||||
): Promise<EOperation[]> => {
|
||||
const opList = await ResticUI.GetOperations(req, {
|
||||
pathPrefix: API_PREFIX,
|
||||
});
|
||||
return (opList.operations || []).map(toEop);
|
||||
};
|
||||
|
||||
@@ -87,10 +78,10 @@ export const buildOperationListListener = (
|
||||
(async () => {
|
||||
let opsFromServer = await getOperations(req);
|
||||
operations = opsFromServer.filter(
|
||||
(o) => !operations.find((op) => op.parsedId === o.parsedId)
|
||||
(o) => !operations.find((op) => op.id === o.id)
|
||||
);
|
||||
operations.sort((a, b) => {
|
||||
return a.parsedId - b.parsedId;
|
||||
return a.parsedTime! - b.parsedTime!;
|
||||
});
|
||||
|
||||
callback(null, null, operations);
|
||||
@@ -105,6 +96,12 @@ export const buildOperationListListener = (
|
||||
if (!!req.repoId && op.repoId !== req.repoId) {
|
||||
return;
|
||||
}
|
||||
if (req.snapshotId && op.snapshotId !== req.snapshotId) {
|
||||
return;
|
||||
}
|
||||
if (req.ids && !req.ids.includes(op.id!)) {
|
||||
return;
|
||||
}
|
||||
if (type === OperationEventType.EVENT_UPDATED) {
|
||||
const index = operations.findIndex((o) => o.id === op.id);
|
||||
if (index > -1) {
|
||||
@@ -112,7 +109,7 @@ export const buildOperationListListener = (
|
||||
} else {
|
||||
operations.push(op);
|
||||
operations.sort((a, b) => {
|
||||
return parseInt(a.id!) - parseInt(b.id!);
|
||||
return a.parsedTime! - b.parsedTime!;
|
||||
});
|
||||
}
|
||||
} else if (type === OperationEventType.EVENT_CREATED) {
|
||||
@@ -124,12 +121,146 @@ export const buildOperationListListener = (
|
||||
};
|
||||
|
||||
export const toEop = (op: Operation): EOperation => {
|
||||
const time =
|
||||
op.operationIndexSnapshot?.snapshot?.unixTimeMs || op.unixTimeStartMs;
|
||||
const time = parseInt(op.unixTimeStartMs!);
|
||||
const date = new Date();
|
||||
date.setTime(time);
|
||||
|
||||
return {
|
||||
...op,
|
||||
parsedId: parseInt(op.id!),
|
||||
parsedTime: parseInt(time!),
|
||||
parsedTime: time,
|
||||
parsedDate: date,
|
||||
};
|
||||
};
|
||||
|
||||
export interface BackupInfo {
|
||||
id: string; // id of the first operation that generated this backup.
|
||||
displayTime: Date;
|
||||
startTimeMs: number;
|
||||
endTimeMs: number;
|
||||
status: OperationStatus;
|
||||
operations: Operation[];
|
||||
repoId?: string;
|
||||
planId?: string;
|
||||
snapshotId?: string;
|
||||
backupLastStatus?: BackupProgressEntry;
|
||||
snapshotInfo?: ResticSnapshot;
|
||||
}
|
||||
|
||||
// BackupInfoCollector maps multiple operations to single aggregate 'BackupInfo' objects.
|
||||
// A backup info object aggregates the backup status (if available), snapshot info (if available), and possibly the forget status (if available).
|
||||
export class BackupInfoCollector {
|
||||
private listeners: ((
|
||||
event: OperationEventType,
|
||||
info: BackupInfo[]
|
||||
) => void)[] = [];
|
||||
private backupByOpId: { [key: string]: BackupInfo } = {};
|
||||
private backupBySnapshotId: { [key: string]: BackupInfo } = {};
|
||||
|
||||
private mergeBackups(existing: BackupInfo, newInfo: BackupInfo) {
|
||||
existing.startTimeMs = Math.min(existing.startTimeMs, newInfo.startTimeMs);
|
||||
existing.endTimeMs = Math.max(existing.endTimeMs, newInfo.endTimeMs);
|
||||
existing.displayTime = new Date(existing.startTimeMs);
|
||||
if (newInfo.startTimeMs >= existing.startTimeMs) {
|
||||
existing.status = newInfo.status; // use the latest status
|
||||
}
|
||||
existing.operations = _.uniqBy(
|
||||
[...existing.operations, ...newInfo.operations],
|
||||
(o) => o.id!
|
||||
);
|
||||
if (newInfo.backupLastStatus) {
|
||||
existing.backupLastStatus = newInfo.backupLastStatus;
|
||||
}
|
||||
if (newInfo.snapshotInfo) {
|
||||
existing.snapshotInfo = newInfo.snapshotInfo;
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
private operationToBackup(op: Operation): BackupInfo {
|
||||
const startTimeMs = parseInt(op.unixTimeStartMs!);
|
||||
const endTimeMs = parseInt(op.unixTimeEndMs!);
|
||||
|
||||
const b: BackupInfo = {
|
||||
id: op.id!,
|
||||
startTimeMs,
|
||||
endTimeMs,
|
||||
status: op.status!,
|
||||
displayTime: new Date(startTimeMs),
|
||||
repoId: op.repoId,
|
||||
planId: op.planId,
|
||||
snapshotId: op.snapshotId,
|
||||
operations: [op],
|
||||
};
|
||||
|
||||
if (op.operationBackup) {
|
||||
const ob = op.operationBackup;
|
||||
b.backupLastStatus = ob.lastStatus;
|
||||
}
|
||||
if (op.operationIndexSnapshot) {
|
||||
const oi = op.operationIndexSnapshot;
|
||||
b.snapshotInfo = oi.snapshot;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private addHelper(op: Operation) {
|
||||
if (op.snapshotId) {
|
||||
delete this.backupByOpId[op.id!];
|
||||
|
||||
let newInfo = this.operationToBackup(op);
|
||||
const existing = this.backupBySnapshotId[op.snapshotId!];
|
||||
if (existing) {
|
||||
this.mergeBackups(existing, newInfo);
|
||||
return existing;
|
||||
} else {
|
||||
this.backupBySnapshotId[op.snapshotId] = newInfo;
|
||||
return newInfo;
|
||||
}
|
||||
} else {
|
||||
const newInfo = this.operationToBackup(op);
|
||||
this.backupByOpId[op.id!] = newInfo;
|
||||
return newInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public addOperation(event: OperationEventType, op: Operation): BackupInfo {
|
||||
const backupInfo = this.addHelper(op);
|
||||
this.listeners.forEach((l) => l(event, [backupInfo]));
|
||||
return backupInfo;
|
||||
}
|
||||
|
||||
public bulkAddOperations(ops: Operation[]): BackupInfo[] {
|
||||
const backupInfos = ops.map((op) => this.addHelper(op));
|
||||
this.listeners.forEach((l) =>
|
||||
l(OperationEventType.EVENT_UNKNOWN, backupInfos)
|
||||
);
|
||||
return backupInfos;
|
||||
}
|
||||
|
||||
public addOperationNoNotify(op: Operation) {
|
||||
this.addHelper(op);
|
||||
}
|
||||
|
||||
public getAll(): BackupInfo[] {
|
||||
const arr = [];
|
||||
arr.push(...Object.values(this.backupByOpId));
|
||||
arr.push(...Object.values(this.backupBySnapshotId));
|
||||
return arr;
|
||||
}
|
||||
|
||||
public subscribe(
|
||||
listener: (event: OperationEventType, info: BackupInfo[]) => void
|
||||
) {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
public unsubscribe(
|
||||
listener: (event: OperationEventType, info: BackupInfo[]) => void
|
||||
) {
|
||||
const index = this.listeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
this.listeners[index] = this.listeners[this.listeners.length - 1];
|
||||
this.listeners.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ export const AddRepoModel = ({
|
||||
showModal(null);
|
||||
alertsApi.success("Updated repo " + repo.uri);
|
||||
|
||||
// Update the snapshots for the repo
|
||||
// Update the snapshots for the repo to confirm the config works.
|
||||
// TODO: this operation is only used here, find a different RPC for this purpose.
|
||||
await ResticUI.ListSnapshots(
|
||||
{
|
||||
repoId: repo.id,
|
||||
|
||||
+58
-94
@@ -9,7 +9,7 @@ import {
|
||||
import type { MenuProps } from "antd";
|
||||
import { Button, Layout, List, Menu, Modal, Spin, theme } from "antd";
|
||||
import { configState, fetchConfig } from "../state/config";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { useRecoilState, useRecoilValue } from "recoil";
|
||||
import { Config, Plan } from "../../gen/ts/v1/config.pb";
|
||||
import { useAlertApi } from "../components/Alerts";
|
||||
import { useShowModal } from "../components/ModalManager";
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
toEop,
|
||||
unsubscribeFromOperations,
|
||||
} from "../state/oplog";
|
||||
import { formatTime } from "../lib/formatting";
|
||||
import { formatTime, normalizeSnapshotId } from "../lib/formatting";
|
||||
import { SnapshotBrowser } from "../components/SnapshotBrowser";
|
||||
import { OperationRow } from "../components/OperationList";
|
||||
import {
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
OperationEvent,
|
||||
OperationEventType,
|
||||
} from "../../gen/ts/v1/operations.pb";
|
||||
import { MessageInstance } from "antd/es/message/interface";
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
|
||||
@@ -57,7 +58,8 @@ export const App: React.FC = () => {
|
||||
.catch((err) => {
|
||||
alertApi.error(err.message, 0);
|
||||
alertApi.error(
|
||||
"Failed to fetch initial config, typically this means the UI could not connect to the backend"
|
||||
"Failed to fetch initial config, typically this means the UI could not connect to the backend",
|
||||
0
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
@@ -66,6 +68,7 @@ export const App: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Layout style={{ height: "auto" }}>
|
||||
<OperationNotificationGenerator />
|
||||
<Header style={{ display: "flex", alignItems: "center" }}>
|
||||
<h1>
|
||||
<a
|
||||
@@ -98,66 +101,12 @@ export const App: React.FC = () => {
|
||||
const getSidenavItems = (config: Config | null): MenuProps["items"] => {
|
||||
const showModal = useShowModal();
|
||||
const setContent = useSetContent();
|
||||
const [snapshotsByPlan, setSnapshotsByPlan] = useState<{
|
||||
[planId: string]: EOperation[];
|
||||
}>({});
|
||||
|
||||
const addSnapshots = (planId: string, ops: EOperation[]) => {
|
||||
const snapsByPlanCpy = { ...snapshotsByPlan };
|
||||
let snapsForPlanCpy = [...(snapsByPlanCpy[planId] || [])];
|
||||
for (const op of ops) {
|
||||
snapsForPlanCpy.push(toEop(op));
|
||||
}
|
||||
snapsForPlanCpy.sort((a, b) => {
|
||||
return a.parsedTime > b.parsedTime ? -1 : 1;
|
||||
});
|
||||
if (snapsForPlanCpy.length > 5) {
|
||||
snapsForPlanCpy = snapsForPlanCpy.slice(0, 5);
|
||||
}
|
||||
snapsByPlanCpy[planId] = snapsForPlanCpy;
|
||||
setSnapshotsByPlan(snapsByPlanCpy);
|
||||
};
|
||||
|
||||
// Track newly created snapshots in the set.
|
||||
useEffect(() => {
|
||||
const listener = (event: OperationEvent) => {
|
||||
if (event.type !== OperationEventType.EVENT_CREATED) return;
|
||||
const op = event.operation!;
|
||||
if (!op.planId) return;
|
||||
if (!op.operationIndexSnapshot) return;
|
||||
addSnapshots(op.planId!, [toEop(op)]);
|
||||
};
|
||||
|
||||
subscribeToOperations(listener);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromOperations(listener);
|
||||
};
|
||||
}, [snapshotsByPlan]);
|
||||
|
||||
if (!config) return [];
|
||||
|
||||
const configPlans = config.plans || [];
|
||||
const configRepos = config.repos || [];
|
||||
|
||||
const onSelectPlan = (plan: Plan) => {
|
||||
setContent(<PlanView plan={plan} />, [
|
||||
{ title: "Plans" },
|
||||
{ title: plan.id || "" },
|
||||
]);
|
||||
|
||||
if (!snapshotsByPlan[plan.id!]) {
|
||||
(async () => {
|
||||
const ops = await getOperations({ planId: plan.id!, lastN: "20" });
|
||||
// avoid races by checking again after the request
|
||||
if (!snapshotsByPlan[plan.id!]) {
|
||||
const snapshots = ops.filter((op) => !!op.operationIndexSnapshot);
|
||||
addSnapshots(plan.id!, snapshots);
|
||||
}
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
const plans: MenuProps["items"] = [
|
||||
{
|
||||
key: "add-plan",
|
||||
@@ -168,47 +117,16 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
|
||||
},
|
||||
},
|
||||
...configPlans.map((plan) => {
|
||||
const children: MenuProps["items"] = (
|
||||
snapshotsByPlan[plan.id!] || []
|
||||
).map((snapshot) => {
|
||||
return {
|
||||
key: "s-" + snapshot.id,
|
||||
icon: <PaperClipOutlined />,
|
||||
label: (
|
||||
<small>{"Operation " + formatTime(snapshot.parsedTime)}</small>
|
||||
),
|
||||
onClick: () => {
|
||||
showModal(
|
||||
<Modal
|
||||
title="View Snapshot"
|
||||
open={true}
|
||||
onCancel={() => showModal(null)}
|
||||
footer={[
|
||||
<Button
|
||||
key="done"
|
||||
onClick={() => showModal(null)}
|
||||
type="primary"
|
||||
>
|
||||
Done
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<List>
|
||||
<OperationRow operation={snapshot} />
|
||||
</List>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
key: "p-" + plan.id,
|
||||
icon: <CheckCircleOutlined style={{ color: "green" }} />,
|
||||
label: plan.id,
|
||||
children: children,
|
||||
onTitleClick: onSelectPlan.bind(null, plan), // if children
|
||||
onClick: onSelectPlan.bind(null, plan), // if no children
|
||||
onClick: () => {
|
||||
setContent(<PlanView plan={plan} />, [
|
||||
{ title: "Plans" },
|
||||
{ title: plan.id || "" },
|
||||
]);
|
||||
},
|
||||
};
|
||||
}),
|
||||
];
|
||||
@@ -249,3 +167,49 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const OperationNotificationGenerator = () => {
|
||||
const alertApi = useAlertApi()!;
|
||||
const setContent = useSetContent();
|
||||
const config = useRecoilValue(configState);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: factor notification generator into a separate file.
|
||||
const listener = (event: OperationEvent) => {
|
||||
if (event.type != OperationEventType.EVENT_CREATED) return;
|
||||
const planId = event.operation!.planId!;
|
||||
const repoId = event.operation!.repoId!;
|
||||
|
||||
const onClick = () => {
|
||||
const plan = config.plans!.find((p) => p.id == planId);
|
||||
if (!plan) return;
|
||||
setContent(<PlanView plan={plan} />, [
|
||||
{ title: "Plans" },
|
||||
{ title: planId || "" },
|
||||
]);
|
||||
};
|
||||
|
||||
if (event.operation?.operationBackup) {
|
||||
alertApi.info({
|
||||
content: `Backup started for plan ${planId}.`,
|
||||
onClick: onClick,
|
||||
});
|
||||
} else if (event.operation?.operationIndexSnapshot) {
|
||||
const indexOp = event.operation.operationIndexSnapshot;
|
||||
alertApi.info({
|
||||
content: `Indexed snapshot ${normalizeSnapshotId(
|
||||
indexOp.snapshot!.id!
|
||||
)} for plan ${planId}.`,
|
||||
onClick: onClick,
|
||||
});
|
||||
}
|
||||
};
|
||||
subscribeToOperations(listener);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromOperations(listener);
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Plan } from "../../gen/ts/v1/config.pb";
|
||||
import { Button, Flex } from "antd";
|
||||
import { Button, Flex, Tabs } from "antd";
|
||||
import { SettingOutlined } from "@ant-design/icons";
|
||||
import { AddPlanModal } from "./AddPlanModel";
|
||||
import { useShowModal } from "../components/ModalManager";
|
||||
@@ -15,25 +15,12 @@ import {
|
||||
unsubscribeFromOperations,
|
||||
} from "../state/oplog";
|
||||
import { OperationList } from "../components/OperationList";
|
||||
import { OperationTree } from "../components/OperationTree";
|
||||
import { MAX_OPERATION_HISTORY } from "../constants";
|
||||
|
||||
export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
const showModal = useShowModal();
|
||||
const alertsApi = useAlertApi()!;
|
||||
const [operations, setOperations] = useState<EOperation[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = buildOperationListListener(
|
||||
{ planId: plan.id, lastN: "1000" },
|
||||
(event, changedOp, operations) => {
|
||||
setOperations([...operations]);
|
||||
}
|
||||
);
|
||||
subscribeToOperations(listener);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromOperations(listener);
|
||||
};
|
||||
}, [plan.id]);
|
||||
|
||||
// Gracefully handle deletions by checking if the plan is still in the config.
|
||||
const config = useRecoilValue(configState);
|
||||
@@ -78,8 +65,34 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
Prune Now
|
||||
</Button>
|
||||
</Flex>
|
||||
<h2>Backup Action History ({operations.length} loaded)</h2>
|
||||
<OperationList operations={operations} />
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: "Tree View",
|
||||
children: (
|
||||
<>
|
||||
<OperationTree
|
||||
req={{ planId: plan.id!, lastN: "" + MAX_OPERATION_HISTORY }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Operation List",
|
||||
children: (
|
||||
<>
|
||||
<h2>Backup Action History</h2>
|
||||
<OperationList
|
||||
req={{ planId: plan.id!, lastN: "" + MAX_OPERATION_HISTORY }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user