diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index e113a3fc..f41cc6d0 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -125,6 +125,53 @@ func (OperationStatus) EnumDescriptor() ([]byte, []int) { return file_v1_operations_proto_rawDescGZIP(), []int{1} } +type OperationList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Operations []*Operation `protobuf:"bytes,1,rep,name=operations,proto3" json:"operations,omitempty"` +} + +func (x *OperationList) Reset() { + *x = OperationList{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_operations_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OperationList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationList) ProtoMessage() {} + +func (x *OperationList) ProtoReflect() protoreflect.Message { + mi := &file_v1_operations_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationList.ProtoReflect.Descriptor instead. +func (*OperationList) Descriptor() ([]byte, []int) { + return file_v1_operations_proto_rawDescGZIP(), []int{0} +} + +func (x *OperationList) GetOperations() []*Operation { + if x != nil { + return x.Operations + } + return nil +} + type Operation struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -146,7 +193,7 @@ type Operation struct { func (x *Operation) Reset() { *x = Operation{} if protoimpl.UnsafeEnabled { - mi := &file_v1_operations_proto_msgTypes[0] + mi := &file_v1_operations_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -159,7 +206,7 @@ func (x *Operation) String() string { func (*Operation) ProtoMessage() {} func (x *Operation) ProtoReflect() protoreflect.Message { - mi := &file_v1_operations_proto_msgTypes[0] + mi := &file_v1_operations_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -172,7 +219,7 @@ func (x *Operation) ProtoReflect() protoreflect.Message { // Deprecated: Use Operation.ProtoReflect.Descriptor instead. func (*Operation) Descriptor() ([]byte, []int) { - return file_v1_operations_proto_rawDescGZIP(), []int{0} + return file_v1_operations_proto_rawDescGZIP(), []int{1} } func (x *Operation) GetId() int64 { @@ -261,7 +308,7 @@ type OperationEvent struct { func (x *OperationEvent) Reset() { *x = OperationEvent{} if protoimpl.UnsafeEnabled { - mi := &file_v1_operations_proto_msgTypes[1] + mi := &file_v1_operations_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -274,7 +321,7 @@ func (x *OperationEvent) String() string { func (*OperationEvent) ProtoMessage() {} func (x *OperationEvent) ProtoReflect() protoreflect.Message { - mi := &file_v1_operations_proto_msgTypes[1] + mi := &file_v1_operations_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -287,7 +334,7 @@ func (x *OperationEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use OperationEvent.ProtoReflect.Descriptor instead. func (*OperationEvent) Descriptor() ([]byte, []int) { - return file_v1_operations_proto_rawDescGZIP(), []int{1} + return file_v1_operations_proto_rawDescGZIP(), []int{2} } func (x *OperationEvent) GetType() OperationEventType { @@ -315,7 +362,7 @@ type OperationBackup struct { func (x *OperationBackup) Reset() { *x = OperationBackup{} if protoimpl.UnsafeEnabled { - mi := &file_v1_operations_proto_msgTypes[2] + mi := &file_v1_operations_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -328,7 +375,7 @@ func (x *OperationBackup) String() string { func (*OperationBackup) ProtoMessage() {} func (x *OperationBackup) ProtoReflect() protoreflect.Message { - mi := &file_v1_operations_proto_msgTypes[2] + mi := &file_v1_operations_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -341,7 +388,7 @@ func (x *OperationBackup) ProtoReflect() protoreflect.Message { // Deprecated: Use OperationBackup.ProtoReflect.Descriptor instead. func (*OperationBackup) Descriptor() ([]byte, []int) { - return file_v1_operations_proto_rawDescGZIP(), []int{2} + return file_v1_operations_proto_rawDescGZIP(), []int{3} } func (x *OperationBackup) GetLastStatus() *BackupProgressEntry { @@ -356,7 +403,11 @@ var File_v1_operations_proto protoreflect.FileDescriptor var file_v1_operations_proto_rawDesc = []byte{ 0x0a, 0x13, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x31, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x72, 0x65, - 0x73, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc1, 0x02, 0x0a, 0x09, 0x4f, + 0x73, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3e, 0x0a, 0x0d, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x0a, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xc1, 0x02, 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, @@ -420,26 +471,28 @@ func file_v1_operations_proto_rawDescGZIP() []byte { } var file_v1_operations_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_v1_operations_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_v1_operations_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_v1_operations_proto_goTypes = []interface{}{ (OperationEventType)(0), // 0: v1.OperationEventType (OperationStatus)(0), // 1: v1.OperationStatus - (*Operation)(nil), // 2: v1.Operation - (*OperationEvent)(nil), // 3: v1.OperationEvent - (*OperationBackup)(nil), // 4: v1.OperationBackup - (*BackupProgressEntry)(nil), // 5: v1.BackupProgressEntry + (*OperationList)(nil), // 2: v1.OperationList + (*Operation)(nil), // 3: v1.Operation + (*OperationEvent)(nil), // 4: v1.OperationEvent + (*OperationBackup)(nil), // 5: v1.OperationBackup + (*BackupProgressEntry)(nil), // 6: v1.BackupProgressEntry } var file_v1_operations_proto_depIdxs = []int32{ - 1, // 0: v1.Operation.status:type_name -> v1.OperationStatus - 4, // 1: v1.Operation.operation_backup:type_name -> v1.OperationBackup - 0, // 2: v1.OperationEvent.type:type_name -> v1.OperationEventType - 2, // 3: v1.OperationEvent.operation:type_name -> v1.Operation - 5, // 4: v1.OperationBackup.last_status:type_name -> v1.BackupProgressEntry - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 3, // 0: v1.OperationList.operations:type_name -> v1.Operation + 1, // 1: v1.Operation.status:type_name -> v1.OperationStatus + 5, // 2: v1.Operation.operation_backup:type_name -> v1.OperationBackup + 0, // 3: v1.OperationEvent.type:type_name -> v1.OperationEventType + 3, // 4: v1.OperationEvent.operation:type_name -> v1.Operation + 6, // 5: v1.OperationBackup.last_status:type_name -> v1.BackupProgressEntry + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_v1_operations_proto_init() } @@ -450,7 +503,7 @@ func file_v1_operations_proto_init() { file_v1_restic_proto_init() if !protoimpl.UnsafeEnabled { file_v1_operations_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Operation); i { + switch v := v.(*OperationList); i { case 0: return &v.state case 1: @@ -462,7 +515,7 @@ func file_v1_operations_proto_init() { } } file_v1_operations_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OperationEvent); i { + switch v := v.(*Operation); i { case 0: return &v.state case 1: @@ -474,6 +527,18 @@ func file_v1_operations_proto_init() { } } file_v1_operations_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OperationEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v1_operations_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OperationBackup); i { case 0: return &v.state @@ -486,7 +551,7 @@ func file_v1_operations_proto_init() { } } } - file_v1_operations_proto_msgTypes[0].OneofWrappers = []interface{}{ + file_v1_operations_proto_msgTypes[1].OneofWrappers = []interface{}{ (*Operation_OperationBackup)(nil), } type x struct{} @@ -495,7 +560,7 @@ func file_v1_operations_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_operations_proto_rawDesc, NumEnums: 2, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index 43a192da..25e7f91f 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -78,6 +78,69 @@ func (x *ListSnapshotsRequest) GetPlanId() string { return "" } +type GetOperationsRequest struct { + state protoimpl.MessageState + 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 +} + +func (x *GetOperationsRequest) Reset() { + *x = GetOperationsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetOperationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOperationsRequest) ProtoMessage() {} + +func (x *GetOperationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOperationsRequest.ProtoReflect.Descriptor instead. +func (*GetOperationsRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{1} +} + +func (x *GetOperationsRequest) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *GetOperationsRequest) GetPlanId() string { + if x != nil { + return x.PlanId + } + return "" +} + +func (x *GetOperationsRequest) GetLastN() int64 { + if x != nil { + return x.LastN + } + return 0 +} + var File_v1_service_proto protoreflect.FileDescriptor var file_v1_service_proto_rawDesc = []byte{ @@ -95,41 +158,57 @@ 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, 0x32, 0xe5, 0x03, 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, 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, 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, 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, 0x32, 0x8f, 0x05, 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, 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 ( @@ -144,32 +223,38 @@ func file_v1_service_proto_rawDescGZIP() []byte { return file_v1_service_proto_rawDescData } -var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_v1_service_proto_goTypes = []interface{}{ (*ListSnapshotsRequest)(nil), // 0: v1.ListSnapshotsRequest - (*emptypb.Empty)(nil), // 1: google.protobuf.Empty - (*Config)(nil), // 2: v1.Config - (*Repo)(nil), // 3: v1.Repo - (*types.StringValue)(nil), // 4: types.StringValue - (*OperationEvent)(nil), // 5: v1.OperationEvent - (*ResticSnapshotList)(nil), // 6: v1.ResticSnapshotList - (*types.StringList)(nil), // 7: types.StringList + (*GetOperationsRequest)(nil), // 1: v1.GetOperationsRequest + (*emptypb.Empty)(nil), // 2: google.protobuf.Empty + (*Config)(nil), // 3: v1.Config + (*Repo)(nil), // 4: v1.Repo + (*types.StringValue)(nil), // 5: types.StringValue + (*OperationEvent)(nil), // 6: v1.OperationEvent + (*OperationList)(nil), // 7: v1.OperationList + (*ResticSnapshotList)(nil), // 8: v1.ResticSnapshotList + (*types.StringList)(nil), // 9: types.StringList } var file_v1_service_proto_depIdxs = []int32{ - 1, // 0: v1.ResticUI.GetConfig:input_type -> google.protobuf.Empty - 2, // 1: v1.ResticUI.SetConfig:input_type -> v1.Config - 3, // 2: v1.ResticUI.AddRepo:input_type -> v1.Repo - 1, // 3: v1.ResticUI.GetOperationEvents:input_type -> google.protobuf.Empty - 0, // 4: v1.ResticUI.ListSnapshots:input_type -> v1.ListSnapshotsRequest - 4, // 5: v1.ResticUI.PathAutocomplete:input_type -> types.StringValue - 2, // 6: v1.ResticUI.GetConfig:output_type -> v1.Config - 2, // 7: v1.ResticUI.SetConfig:output_type -> v1.Config - 2, // 8: v1.ResticUI.AddRepo:output_type -> v1.Config - 5, // 9: v1.ResticUI.GetOperationEvents:output_type -> v1.OperationEvent - 6, // 10: v1.ResticUI.ListSnapshots:output_type -> v1.ResticSnapshotList - 7, // 11: v1.ResticUI.PathAutocomplete:output_type -> types.StringList - 6, // [6:12] is the sub-list for method output_type - 0, // [0:6] is the sub-list for method input_type + 2, // 0: v1.ResticUI.GetConfig:input_type -> google.protobuf.Empty + 3, // 1: v1.ResticUI.SetConfig:input_type -> v1.Config + 4, // 2: v1.ResticUI.AddRepo:input_type -> v1.Repo + 2, // 3: v1.ResticUI.GetOperationEvents:input_type -> google.protobuf.Empty + 1, // 4: v1.ResticUI.GetOperations:input_type -> v1.GetOperationsRequest + 0, // 5: v1.ResticUI.ListSnapshots:input_type -> v1.ListSnapshotsRequest + 5, // 6: v1.ResticUI.Backup:input_type -> types.StringValue + 5, // 7: v1.ResticUI.PathAutocomplete:input_type -> types.StringValue + 3, // 8: v1.ResticUI.GetConfig:output_type -> v1.Config + 3, // 9: v1.ResticUI.SetConfig:output_type -> v1.Config + 3, // 10: v1.ResticUI.AddRepo:output_type -> v1.Config + 6, // 11: v1.ResticUI.GetOperationEvents:output_type -> v1.OperationEvent + 7, // 12: v1.ResticUI.GetOperations:output_type -> v1.OperationList + 8, // 13: v1.ResticUI.ListSnapshots:output_type -> v1.ResticSnapshotList + 2, // 14: v1.ResticUI.Backup:output_type -> google.protobuf.Empty + 9, // 15: v1.ResticUI.PathAutocomplete:output_type -> types.StringList + 8, // [8:16] is the sub-list for method output_type + 0, // [0:8] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -196,6 +281,18 @@ func file_v1_service_proto_init() { return nil } } + file_v1_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetOperationsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -203,7 +300,7 @@ func file_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_service_proto_rawDesc, NumEnums: 0, - NumMessages: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/go/v1/service.pb.gw.go b/gen/go/v1/service.pb.gw.go index 4f9f8b30..9df2ef9e 100644 --- a/gen/go/v1/service.pb.gw.go +++ b/gen/go/v1/service.pb.gw.go @@ -136,6 +136,40 @@ func request_ResticUI_GetOperationEvents_0(ctx context.Context, marshaler runtim } +func request_ResticUI_GetOperations_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOperationsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetOperations(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_ResticUI_GetOperations_0(ctx context.Context, marshaler runtime.Marshaler, server ResticUIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOperationsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetOperations(ctx, &protoReq) + return msg, metadata, err + +} + func request_ResticUI_ListSnapshots_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ListSnapshotsRequest var metadata runtime.ServerMetadata @@ -170,6 +204,40 @@ func local_request_ResticUI_ListSnapshots_0(ctx context.Context, marshaler runti } +func request_ResticUI_Backup_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq types.StringValue + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Backup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_ResticUI_Backup_0(ctx context.Context, marshaler runtime.Marshaler, server ResticUIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq types.StringValue + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.Backup(ctx, &protoReq) + return msg, metadata, err + +} + func request_ResticUI_PathAutocomplete_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq types.StringValue var metadata runtime.ServerMetadata @@ -292,6 +360,31 @@ func RegisterResticUIHandlerServer(ctx context.Context, mux *runtime.ServeMux, s return }) + mux.Handle("POST", pattern_ResticUI_GetOperations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/v1.ResticUI/GetOperations", runtime.WithHTTPPathPattern("/v1/operations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ResticUI_GetOperations_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ResticUI_GetOperations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_ResticUI_ListSnapshots_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -317,6 +410,31 @@ func RegisterResticUIHandlerServer(ctx context.Context, mux *runtime.ServeMux, s }) + mux.Handle("POST", pattern_ResticUI_Backup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/v1.ResticUI/Backup", runtime.WithHTTPPathPattern("/v1/cmd/backup")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ResticUI_Backup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ResticUI_Backup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_ResticUI_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -471,6 +589,28 @@ func RegisterResticUIHandlerClient(ctx context.Context, mux *runtime.ServeMux, c }) + mux.Handle("POST", pattern_ResticUI_GetOperations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/v1.ResticUI/GetOperations", runtime.WithHTTPPathPattern("/v1/operations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ResticUI_GetOperations_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ResticUI_GetOperations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_ResticUI_ListSnapshots_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -493,6 +633,28 @@ func RegisterResticUIHandlerClient(ctx context.Context, mux *runtime.ServeMux, c }) + mux.Handle("POST", pattern_ResticUI_Backup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/v1.ResticUI/Backup", runtime.WithHTTPPathPattern("/v1/cmd/backup")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ResticUI_Backup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ResticUI_Backup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_ResticUI_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -527,8 +689,12 @@ var ( pattern_ResticUI_GetOperationEvents_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "events", "operations"}, "")) + pattern_ResticUI_GetOperations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "operations"}, "")) + pattern_ResticUI_ListSnapshots_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "snapshots"}, "")) + pattern_ResticUI_Backup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "cmd", "backup"}, "")) + pattern_ResticUI_PathAutocomplete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "autocomplete", "path"}, "")) ) @@ -541,7 +707,11 @@ var ( forward_ResticUI_GetOperationEvents_0 = runtime.ForwardResponseStream + forward_ResticUI_GetOperations_0 = runtime.ForwardResponseMessage + forward_ResticUI_ListSnapshots_0 = runtime.ForwardResponseMessage + forward_ResticUI_Backup_0 = runtime.ForwardResponseMessage + forward_ResticUI_PathAutocomplete_0 = runtime.ForwardResponseMessage ) diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index 7f423360..4daf3f43 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -25,7 +25,9 @@ const ( ResticUI_SetConfig_FullMethodName = "/v1.ResticUI/SetConfig" ResticUI_AddRepo_FullMethodName = "/v1.ResticUI/AddRepo" ResticUI_GetOperationEvents_FullMethodName = "/v1.ResticUI/GetOperationEvents" + ResticUI_GetOperations_FullMethodName = "/v1.ResticUI/GetOperations" ResticUI_ListSnapshots_FullMethodName = "/v1.ResticUI/ListSnapshots" + ResticUI_Backup_FullMethodName = "/v1.ResticUI/Backup" ResticUI_PathAutocomplete_FullMethodName = "/v1.ResticUI/PathAutocomplete" ) @@ -37,7 +39,11 @@ type ResticUIClient interface { SetConfig(ctx context.Context, in *Config, opts ...grpc.CallOption) (*Config, error) AddRepo(ctx context.Context, in *Repo, opts ...grpc.CallOption) (*Config, error) GetOperationEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ResticUI_GetOperationEventsClient, error) + GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*OperationList, error) ListSnapshots(ctx context.Context, in *ListSnapshotsRequest, opts ...grpc.CallOption) (*ResticSnapshotList, error) + // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. + Backup(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) + // PathAutocomplete provides path autocompletion options for a given filesystem path. PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) } @@ -108,6 +114,15 @@ func (x *resticUIGetOperationEventsClient) Recv() (*OperationEvent, error) { return m, nil } +func (c *resticUIClient) GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*OperationList, error) { + out := new(OperationList) + err := c.cc.Invoke(ctx, ResticUI_GetOperations_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *resticUIClient) ListSnapshots(ctx context.Context, in *ListSnapshotsRequest, opts ...grpc.CallOption) (*ResticSnapshotList, error) { out := new(ResticSnapshotList) err := c.cc.Invoke(ctx, ResticUI_ListSnapshots_FullMethodName, in, out, opts...) @@ -117,6 +132,15 @@ func (c *resticUIClient) ListSnapshots(ctx context.Context, in *ListSnapshotsReq return out, nil } +func (c *resticUIClient) Backup(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ResticUI_Backup_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *resticUIClient) PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) { out := new(types.StringList) err := c.cc.Invoke(ctx, ResticUI_PathAutocomplete_FullMethodName, in, out, opts...) @@ -134,7 +158,11 @@ type ResticUIServer interface { SetConfig(context.Context, *Config) (*Config, error) AddRepo(context.Context, *Repo) (*Config, error) GetOperationEvents(*emptypb.Empty, ResticUI_GetOperationEventsServer) error + GetOperations(context.Context, *GetOperationsRequest) (*OperationList, error) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ResticSnapshotList, error) + // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. + Backup(context.Context, *types.StringValue) (*emptypb.Empty, error) + // PathAutocomplete provides path autocompletion options for a given filesystem path. PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) mustEmbedUnimplementedResticUIServer() } @@ -155,9 +183,15 @@ func (UnimplementedResticUIServer) AddRepo(context.Context, *Repo) (*Config, err func (UnimplementedResticUIServer) GetOperationEvents(*emptypb.Empty, ResticUI_GetOperationEventsServer) error { return status.Errorf(codes.Unimplemented, "method GetOperationEvents not implemented") } +func (UnimplementedResticUIServer) GetOperations(context.Context, *GetOperationsRequest) (*OperationList, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetOperations not implemented") +} func (UnimplementedResticUIServer) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ResticSnapshotList, error) { return nil, status.Errorf(codes.Unimplemented, "method ListSnapshots not implemented") } +func (UnimplementedResticUIServer) Backup(context.Context, *types.StringValue) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Backup not implemented") +} func (UnimplementedResticUIServer) PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) { return nil, status.Errorf(codes.Unimplemented, "method PathAutocomplete not implemented") } @@ -249,6 +283,24 @@ func (x *resticUIGetOperationEventsServer) Send(m *OperationEvent) error { return x.ServerStream.SendMsg(m) } +func _ResticUI_GetOperations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetOperationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResticUIServer).GetOperations(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResticUI_GetOperations_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResticUIServer).GetOperations(ctx, req.(*GetOperationsRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ResticUI_ListSnapshots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListSnapshotsRequest) if err := dec(in); err != nil { @@ -267,6 +319,24 @@ func _ResticUI_ListSnapshots_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _ResticUI_Backup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(types.StringValue) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResticUIServer).Backup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResticUI_Backup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResticUIServer).Backup(ctx, req.(*types.StringValue)) + } + return interceptor(ctx, in, info, handler) +} + func _ResticUI_PathAutocomplete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(types.StringValue) if err := dec(in); err != nil { @@ -304,10 +374,18 @@ var ResticUI_ServiceDesc = grpc.ServiceDesc{ MethodName: "AddRepo", Handler: _ResticUI_AddRepo_Handler, }, + { + MethodName: "GetOperations", + Handler: _ResticUI_GetOperations_Handler, + }, { MethodName: "ListSnapshots", Handler: _ResticUI_ListSnapshots_Handler, }, + { + MethodName: "Backup", + Handler: _ResticUI_Backup_Handler, + }, { MethodName: "PathAutocomplete", Handler: _ResticUI_PathAutocomplete_Handler, diff --git a/internal/api/server.go b/internal/api/server.go index 238aea1c..68a8fcc8 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "time" "github.com/garethgeorge/resticui/gen/go/types" v1 "github.com/garethgeorge/resticui/gen/go/v1" @@ -28,6 +29,7 @@ var _ v1.ResticUIServer = &Server{} func NewServer(orchestrator *orchestrator.Orchestrator, oplog *oplog.OpLog) *Server { s := &Server{ orchestrator: orchestrator, + oplog: oplog, } return s @@ -54,6 +56,12 @@ func (s *Server) SetConfig(ctx context.Context, c *v1.Config) (*v1.Config, error if err := config.Default.Update(c); err != nil { return nil, fmt.Errorf("failed to update config: %w", err) } + + newConfig, err := config.Default.Get() + if err != nil { + return nil, fmt.Errorf("failed to get newly set config: %w", err) + } + s.orchestrator.ApplyConfig(newConfig) return config.Default.Get() } @@ -122,7 +130,7 @@ func (s *Server) ListSnapshots(ctx context.Context, query *v1.ListSnapshotsReque }, nil } -// GetEvents implements GET /v1/events +// GetOperationEvents implements GET /v1/events/operations func (s *Server) GetOperationEvents(_ *emptypb.Empty, stream v1.ResticUI_GetOperationEventsServer) error { errorChan := make(chan error) defer close(errorChan) @@ -158,6 +166,40 @@ func (s *Server) GetOperationEvents(_ *emptypb.Empty, stream v1.ResticUI_GetOper } } +func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest) (*v1.OperationList, error) { + filter := oplog.FilterKeepAll() + + if req.LastN != 0 { + filter = oplog.FilterLastN(req.LastN) + } + + var err error + var ops []*v1.Operation + if req.RepoId != "" && req.PlanId != "" { + return nil, errors.New("cannot specify both repoId and planId") + } else if req.PlanId != "" { + ops, err = s.oplog.GetByPlan(req.PlanId, filter) + } else if req.RepoId != "" { + ops, err = s.oplog.GetByRepo(req.RepoId, filter) + } + if err != nil { + return nil, fmt.Errorf("failed to get operations: %w", err) + } + + return &v1.OperationList{ + Operations: ops, + }, nil +} + +func (s* Server) Backup(ctx context.Context, req *types.StringValue) (*emptypb.Empty, error) { + plan, err := s.orchestrator.GetPlan(req.Value) + if err != nil { + return nil, fmt.Errorf("failed to get plan %q: %w", req.Value, err) + } + s.orchestrator.EnqueueTask(orchestrator.NewOneofBackupTask(s.orchestrator, plan, time.Now())) + return &emptypb.Empty{}, nil +} + func (s *Server) PathAutocomplete(ctx context.Context, path *types.StringValue) (*types.StringList, error) { ents, err := os.ReadDir(path.Value) if errors.Is(err, os.ErrNotExist) { diff --git a/internal/oplog/serializationutil.go b/internal/oplog/serializationutil.go index c97483f7..ee8cfc26 100644 --- a/internal/oplog/serializationutil.go +++ b/internal/oplog/serializationutil.go @@ -18,3 +18,12 @@ func stob(v string) []byte { b = append(b, []byte(v)...) return b } + +func btos(b []byte) (string, int64) { + length := btoi(b[:8]) + return string(b[8:8+length]), 8+length +} + +func normalizeSnapshotId(id string) string { + return id[:8] +} \ No newline at end of file diff --git a/internal/oplog/serializationutil_test.go b/internal/oplog/serializationutil_test.go index 13c19d47..2c7cab01 100644 --- a/internal/oplog/serializationutil_test.go +++ b/internal/oplog/serializationutil_test.go @@ -10,4 +10,14 @@ func TestItoa(t *testing.T) { t.Errorf("itob/btoi failed for %d", num) } } -} \ No newline at end of file +} + +func TestStob(t *testing.T) { + strs := []string{"", "a", "ab", "abc", "abcd", "abcde", "abcdef"} + for _, str := range strs { + b := stob(str) + if val, _ := btos(b); val != str { + t.Errorf("stob/btos failed for %s", str) + } + } +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 68dcbb5f..865723a7 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -26,8 +26,9 @@ type Orchestrator struct { oplog *oplog.OpLog repoPool *resticRepoPool - // configUpdates chan makes config changes available to Run() - configUpdates chan *v1.Config + + configUpdates chan *v1.Config // configUpdates chan makes config changes available to Run() + externTasks chan Task // externTasks is a channel that externally added tasks can be added to, they will be consumed by Run() } func NewOrchestrator(configProvider config.ConfigStore, oplog *oplog.OpLog) (*Orchestrator, error) { @@ -40,6 +41,7 @@ func NewOrchestrator(configProvider config.ConfigStore, oplog *oplog.OpLog) (*Or config: cfg, oplog: oplog, repoPool: newResticRepoPool(&config.MemoryStore{Config: cfg}), + externTasks: make(chan Task, 2), }, nil } @@ -99,84 +101,99 @@ func (o *Orchestrator) Run(mainCtx context.Context) error { o.configUpdates = make(chan *v1.Config) o.mu.Unlock() - for { - lock := sync.Mutex{} - ctx, cancel := context.WithCancel(mainCtx) - - var wg sync.WaitGroup + for mainCtx.Err() == nil { + o.mu.Lock() + config := o.config + o.mu.Unlock() + o.runVersion(mainCtx, config) + zap.L().Info("Restarting orchestrator loop") + } + zap.L().Info("Exited orchestrator loop, context cancelled.") - var execTask func(t task, runAt time.Time) - execTask = func(t task, runAt time.Time) { - curTime := time.Now() - timer := time.NewTimer(runAt.Sub(curTime)) - zap.L().Debug("Scheduling task", zap.String("task", t.Name()), zap.String("runAt", runAt.Format(time.RFC3339))) + return nil +} - wg.Add(1) - go func() { - defer wg.Done() - select { - case <-ctx.Done(): - timer.Stop() - zap.L().Debug("Not running task, orchestrator context is cancelled.", zap.String("task", t.Name())) - return - case <-timer.C: - lock.Lock() - defer lock.Unlock() - zap.L().Debug("Running task", zap.String("task", t.Name())) - - // Task execution runs with mainCtx meaning config changes do not interrupt it, but cancelling the orchestration loop will. - if err := t.Run(mainCtx); err != nil { - zap.L().Error("Task failed", zap.String("task", t.Name()), zap.Error(err)) - } else { - zap.L().Debug("Task finished", zap.String("task", t.Name())) - } +// runImmutable is a helper function for Run() that runs the orchestration loop with a single version of the config. +func (o *Orchestrator) runVersion(mainCtx context.Context, config *v1.Config) { + lock := sync.Mutex{} + ctx, cancel := context.WithCancel(mainCtx) + + var wg sync.WaitGroup - if ctx.Err() != nil { - zap.L().Debug("Not attempting to reschedule task, orchestrator context is cancelled.", zap.String("task", t.Name())) - return - } + var execTask func(t Task) + execTask = func(t Task) { + curTime := time.Now() - next := t.Next(time.Now()) - if next != nil { - execTask(t, *next) - } else { - zap.L().Debug("Task has no next run, not rescheduling.", zap.String("task", t.Name())) - } + runAt := t.Next(curTime) + if runAt == nil { + zap.L().Debug("Task has no next run, not scheduling.", zap.String("task", t.Name())) + return + } + + timer := time.NewTimer(runAt.Sub(curTime)) + zap.L().Debug("Scheduling task", zap.String("task", t.Name()), zap.String("runAt", runAt.Format(time.RFC3339))) + + wg.Add(1) + go func() { + defer wg.Done() + select { + case <-ctx.Done(): + timer.Stop() + zap.L().Debug("Cancelling scheduled (but not running) task, orchestrator context is cancelled.", zap.String("task", t.Name())) + return + case <-timer.C: + lock.Lock() + defer lock.Unlock() + zap.L().Debug("Running task", zap.String("task", t.Name())) + + // Task execution runs with mainCtx meaning config changes do not interrupt it, but cancelling the orchestration loop will. + if err := t.Run(mainCtx); err != nil { + zap.L().Error("Task failed", zap.String("task", t.Name()), zap.Error(err)) + } else { + zap.L().Debug("Task finished", zap.String("task", t.Name())) } - }() + + if ctx.Err() != nil { + zap.L().Debug("Not attempting to reschedule task, orchestrator context is cancelled.", zap.String("task", t.Name())) + return + } + + execTask(t) + } + }() + } + + // Schedule all backup tasks. + for _, plan := range config.Plans { + t, err := NewScheduledBackupTask(o, plan) + if err != nil { + zap.L().Error("Failed to create backup task for plan", zap.String("plan", plan.Id), zap.Error(err)) } - // Schedule all backup tasks. - for _, plan := range o.config.Plans { - t, err := newBackupTask(o, plan) - if err != nil { - zap.L().Error("Failed to create backup task for plan", zap.String("plan", plan.Id), zap.Error(err)) - } + execTask(t) + } - next := t.Next(time.Now()) - if next != nil { - execTask(t, *next) - } else { - zap.L().Debug("Task has no next run, not scheduling.", zap.String("task", t.Name())) - } - } - - // wait for either an error or the context to be cancelled, then wait for all tasks. + // wait for either an error or the context to be cancelled, then wait for all tasks. + for { select { + case t := <-o.externTasks: + execTask(t) case <-ctx.Done(): cancel() wg.Wait() - return nil + return case <-o.configUpdates: zap.L().Info("Orchestrator received config change, waiting for in-progress operations") cancel() wg.Wait() - zap.L().Info("Restarting orchestrator loop") - continue + return } } } +func (o *Orchestrator) EnqueueTask(t Task) { + o.externTasks <- t +} // resticRepoPool caches restic repos. type resticRepoPool struct { diff --git a/internal/orchestrator/tasks.go b/internal/orchestrator/tasks.go index 17115e4f..bf71c080 100644 --- a/internal/orchestrator/tasks.go +++ b/internal/orchestrator/tasks.go @@ -14,72 +14,108 @@ import ( ) -type task interface { +type Task interface { Name() string // huamn readable name for this task. Next(now time.Time) *time.Time // when this task would like to be run. Run(ctx context.Context) error // run the task. } -type backupTask struct { +// BackupTask is a scheduled backup operation. +type ScheduledBackupTask struct { orchestrator *Orchestrator // owning orchestrator plan *v1.Plan schedule *cronexpr.Schedule } -var _ task = &backupTask{} +var _ Task = &ScheduledBackupTask{} -func newBackupTask(orchestrator *Orchestrator, plan *v1.Plan) (*backupTask, error) { +func NewScheduledBackupTask(orchestrator *Orchestrator, plan *v1.Plan) (*ScheduledBackupTask, error) { sched, err := cronexpr.Parse(plan.Cron) if err != nil { return nil, fmt.Errorf("failed to parse schedule %q: %w", plan.Cron, err) } - return &backupTask{ + return &ScheduledBackupTask{ orchestrator: orchestrator, plan: plan, schedule: sched, }, nil } -func (t *backupTask) Name() string { - return fmt.Sprintf("execute backup plan %v", t.plan.Id) +func (t *ScheduledBackupTask) Name() string { + return fmt.Sprintf("backup for plan %q", t.plan.Id) } -func (t *backupTask) Next(now time.Time) *time.Time { +func (t *ScheduledBackupTask) Next(now time.Time) *time.Time { next := t.schedule.Next(now) return &next } -func (t *backupTask) Run(ctx context.Context) error { +func (t *ScheduledBackupTask) Run(ctx context.Context) error { + return backupHelper(ctx, t.orchestrator, t.plan) +} + +// OnetimeBackupTask is a single backup operation. +type OnetimeBackupTask struct { + orchestrator *Orchestrator + plan *v1.Plan + time *time.Time +} + +func NewOneofBackupTask(orchestrator *Orchestrator, plan *v1.Plan, at time.Time) *OnetimeBackupTask { + return &OnetimeBackupTask{ + orchestrator: orchestrator, + plan: plan, + time: &at, + } +} + +func (t *OnetimeBackupTask) Name() string { + return fmt.Sprintf("onetime backup for plan %q", t.plan.Id) +} + +func (t *OnetimeBackupTask) Next(now time.Time) *time.Time { + ret := t.time + t.time = nil + return ret +} + +func (t *OnetimeBackupTask) Run(ctx context.Context) error { + return backupHelper(ctx, t.orchestrator, t.plan) +} + +// backupHelper does a backup. +func backupHelper(ctx context.Context, orchestrator *Orchestrator, plan *v1.Plan) error { backupOp := &v1.Operation_OperationBackup{ OperationBackup: &v1.OperationBackup{}, } op := &v1.Operation{ - PlanId: t.plan.Id, - RepoId: t.plan.Repo, + PlanId: plan.Id, + RepoId: plan.Repo, UnixTimeStartMs: time.Now().Unix(), Status: v1.OperationStatus_STATUS_INPROGRESS, Op: backupOp, } - return WithOperation(t.orchestrator.oplog, op, func() error { - zap.L().Info("Starting backup", zap.String("plan", t.plan.Id)) - repo, err := t.orchestrator.GetRepo(t.plan.Repo) + return WithOperation(orchestrator.oplog, op, func() error { + zap.L().Info("Starting backup", zap.String("plan", plan.Id)) + repo, err := orchestrator.GetRepo(plan.Repo) if err != nil { - return fmt.Errorf("failed to get repo %q: %w", t.plan.Repo, err) + return fmt.Errorf("failed to get repo %q: %w", plan.Repo, err) } - if _, err := repo.Backup(ctx, t.plan, func(entry *restic.BackupProgressEntry) { + if _, err := repo.Backup(ctx, plan, func(entry *restic.BackupProgressEntry) { backupOp.OperationBackup.LastStatus = entry.ToProto() - if err := t.orchestrator.oplog.Update(op); err != nil { + if err := orchestrator.oplog.Update(op); err != nil { zap.S().Errorf("failed to update oplog with progress for backup: %v", err) } - zap.L().Info("Backup progress", zap.Float64("progress", entry.PercentDone)) + zap.L().Debug("Backup progress", zap.Float64("progress", entry.PercentDone)) }); err != nil { - return fmt.Errorf("failed to backup repo %q: %w", t.plan.Repo, err) + return fmt.Errorf("failed to backup repo %q: %w", plan.Repo, err) } + zap.L().Info("Backup complete", zap.String("plan", plan.Id)) return nil }) } diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index 05db451f..d53dc4cc 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -6,6 +6,10 @@ option go_package = "github.com/garethgeorge/resticui/go/proto/v1"; import "v1/restic.proto"; +message OperationList { + repeated Operation operations = 1; +} + message Operation { int64 id = 1; string repo_id = 2; // repo id if associated with a repo (always true) diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 6c1ced11..0c39b041 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -38,6 +38,13 @@ service ResticUI { }; } + rpc GetOperations (GetOperationsRequest) returns (OperationList) { + option (google.api.http) = { + post: "/v1/operations" + body: "*" + }; + } + rpc ListSnapshots(ListSnapshotsRequest) returns (ResticSnapshotList) { option (google.api.http) = { post: "/v1/snapshots" @@ -45,6 +52,15 @@ service ResticUI { }; } + // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. + rpc Backup(types.StringValue) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/v1/cmd/backup" + body: "*" + }; + } + + // PathAutocomplete provides path autocompletion options for a given filesystem path. rpc PathAutocomplete (types.StringValue) returns (types.StringList) { option (google.api.http) = { post: "/v1/autocomplete/path" @@ -56,4 +72,10 @@ service ResticUI { message ListSnapshotsRequest { string repo_id = 1; string plan_id = 2; -} \ No newline at end of file +} + +message GetOperationsRequest { + string repo_id = 1; + string plan_id = 2; + int64 last_n = 3; // limit to the last n operations +} diff --git a/webui/gen/ts/v1/operations.pb.ts b/webui/gen/ts/v1/operations.pb.ts index a6abeee4..c2b4a33f 100644 --- a/webui/gen/ts/v1/operations.pb.ts +++ b/webui/gen/ts/v1/operations.pb.ts @@ -29,6 +29,10 @@ export enum OperationStatus { STATUS_ERROR = "STATUS_ERROR", } +export type OperationList = { + operations?: Operation[] +} + type BaseOperation = { id?: string diff --git a/webui/gen/ts/v1/service.pb.ts b/webui/gen/ts/v1/service.pb.ts index d6d3a173..5d2422da 100644 --- a/webui/gen/ts/v1/service.pb.ts +++ b/webui/gen/ts/v1/service.pb.ts @@ -15,23 +15,90 @@ export type ListSnapshotsRequest = { planId?: string } +export type GetOperationsRequest = { + repoId?: string; + planId?: string; + lastN?: string; +}; + export class ResticUI { - static GetConfig(req: GoogleProtobufEmpty.Empty, initReq?: fm.InitReq): Promise { - return fm.fetchReq(`/v1/config?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"}) + static GetConfig( + req: GoogleProtobufEmpty.Empty, + initReq?: fm.InitReq + ): Promise { + return fm.fetchReq( + `/v1/config?${fm.renderURLSearchParams(req, [])}`, + { ...initReq, method: "GET" } + ); } - static SetConfig(req: V1Config.Config, initReq?: fm.InitReq): Promise { - return fm.fetchReq(`/v1/config`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) + static SetConfig( + req: V1Config.Config, + initReq?: fm.InitReq + ): Promise { + return fm.fetchReq(`/v1/config`, { + ...initReq, + method: "POST", + body: JSON.stringify(req, fm.replacer), + }); } - static AddRepo(req: V1Config.Repo, initReq?: fm.InitReq): Promise { - return fm.fetchReq(`/v1/config/repo`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) + static AddRepo( + req: V1Config.Repo, + initReq?: fm.InitReq + ): Promise { + return fm.fetchReq(`/v1/config/repo`, { + ...initReq, + method: "POST", + body: JSON.stringify(req, fm.replacer), + }); } - static GetOperationEvents(req: GoogleProtobufEmpty.Empty, entityNotifier?: fm.NotifyStreamEntityArrival, initReq?: fm.InitReq): Promise { - return fm.fetchStreamingRequest(`/v1/events/operations?${fm.renderURLSearchParams(req, [])}`, entityNotifier, {...initReq, method: "GET"}) + static GetOperationEvents( + req: GoogleProtobufEmpty.Empty, + entityNotifier?: fm.NotifyStreamEntityArrival, + initReq?: fm.InitReq + ): Promise { + return fm.fetchStreamingRequest< + GoogleProtobufEmpty.Empty, + V1Operations.OperationEvent + >( + `/v1/events/operations?${fm.renderURLSearchParams(req, [])}`, + entityNotifier, + { ...initReq, method: "GET" } + ); } - static ListSnapshots(req: ListSnapshotsRequest, initReq?: fm.InitReq): Promise { - return fm.fetchReq(`/v1/snapshots`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) + static GetOperations( + req: GetOperationsRequest, + initReq?: fm.InitReq + ): Promise { + return fm.fetchReq( + `/v1/operations`, + { ...initReq, method: "POST", body: JSON.stringify(req, fm.replacer) } + ); } - static PathAutocomplete(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise { - return fm.fetchReq(`/v1/autocomplete/path`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) + static ListSnapshots( + req: ListSnapshotsRequest, + initReq?: fm.InitReq + ): Promise { + return fm.fetchReq( + `/v1/snapshots`, + { ...initReq, method: "POST", body: JSON.stringify(req, fm.replacer) } + ); + } + static Backup( + req: TypesValue.StringValue, + initReq?: fm.InitReq + ): Promise { + return fm.fetchReq( + `/v1/cmd/backup`, + { ...initReq, method: "POST", body: JSON.stringify(req, fm.replacer) } + ); + } + static PathAutocomplete( + req: TypesValue.StringValue, + initReq?: fm.InitReq + ): Promise { + return fm.fetchReq( + `/v1/autocomplete/path`, + { ...initReq, method: "POST", body: JSON.stringify(req, fm.replacer) } + ); } } \ No newline at end of file diff --git a/webui/package-lock.json b/webui/package-lock.json index f7a7a215..123e93ff 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -22,6 +22,10 @@ "react-js-cron": "^5.0.1", "recoil": "^0.7.7", "typescript": "^5.2.2" + }, + "devDependencies": { + "events": "^3.3.0", + "stream-browserify": "^3.0.0" } }, "node_modules/@ant-design/colors": { @@ -2806,6 +2810,15 @@ "node": ">=0.8.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2950,6 +2963,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4161,6 +4180,20 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/recoil": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", @@ -4282,6 +4315,25 @@ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", @@ -4430,6 +4482,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, "node_modules/utility-types": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", diff --git a/webui/package.json b/webui/package.json index 4f5dfa0b..8e0380b4 100644 --- a/webui/package.json +++ b/webui/package.json @@ -23,5 +23,9 @@ "react-js-cron": "^5.0.1", "recoil": "^0.7.7", "typescript": "^5.2.2" + }, + "devDependencies": { + "events": "^3.3.0", + "stream-browserify": "^3.0.0" } } diff --git a/webui/src/components/GettingStartedGuide.tsx b/webui/src/components/GettingStartedGuide.tsx new file mode 100644 index 00000000..2ddb4b88 --- /dev/null +++ b/webui/src/components/GettingStartedGuide.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +export const GettingStartedGuide = () => { + return ( + <> +

Getting Started

+
    +
  • + Repos map directly to restic repositories, start by configuring your + backup locations. +
  • +
  • + Plans are where you configure directories to backup, and backup + scheduling. Multiple plans can backup to a single restic repository. +
  • +
  • + See{" "} + + the restic docs on preparing a new repository + {" "} + for details about available repository types and how they can be + configured. +
  • +
+ + ); +}; diff --git a/webui/src/components/MainContentArea.tsx b/webui/src/components/MainContentArea.tsx new file mode 100644 index 00000000..c95f48b9 --- /dev/null +++ b/webui/src/components/MainContentArea.tsx @@ -0,0 +1,56 @@ +import { Breadcrumb, Layout, Spin, theme } from "antd"; +import { BreadcrumbItemType } from "antd/es/breadcrumb/Breadcrumb"; +import { Content } from "antd/es/layout/layout"; +import React, { useContext } from "react"; +import { createContext } from "react"; +import { atom, useRecoilValue, useSetRecoilState } from "recoil"; + +interface Breadcrumb { + title: string; + onClick?: () => void; +} + +const contentPanel = atom({ + key: "ui.content", + default: null, +}); + +const breadcrumbs = atom({ + key: "ui.breadcrumbs", + default: [], +}); + +export const useSetContent = () => { + const setContent = useSetRecoilState(contentPanel); + const setBreadcrumbs = useSetRecoilState(breadcrumbs); + + return (content: React.ReactNode | null, breadcrumbs: Breadcrumb[]) => { + setContent(content); + setBreadcrumbs(breadcrumbs); + }; +}; + +export const MainContentArea = () => { + const { + token: { colorBgContainer }, + } = theme.useToken(); + + const content = useRecoilValue(contentPanel); + const crumbs = useRecoilValue(breadcrumbs); + + return ( + + + + {content} + + + ); +}; diff --git a/webui/src/state/config.ts b/webui/src/state/config.ts index c669538e..9b8f0d21 100644 --- a/webui/src/state/config.ts +++ b/webui/src/state/config.ts @@ -2,13 +2,13 @@ import { atom, useSetRecoilState } from "recoil"; import { Config, Repo } from "../../gen/ts/v1/config.pb"; import { ResticUI } from "../../gen/ts/v1/service.pb"; -export const configState = atom({ +export const configState = atom({ key: "config", - default: null as Config | null, + default: {}, }); export const fetchConfig = async (): Promise => { - return await ResticUI.GetConfig({}, { pathPrefix: "/api/" }); + return await ResticUI.GetConfig({}, { pathPrefix: "/api" }); }; export const addRepo = async (repo: Repo): Promise => { diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts new file mode 100644 index 00000000..1aa612e3 --- /dev/null +++ b/webui/src/state/oplog.ts @@ -0,0 +1,24 @@ +import { atom } from "recoil"; +import { + Operation, + OperationEvent, + OperationEventType, + OperationStatus, +} from "../../gen/ts/v1/operations.pb"; +import { ResticUI } from "../../gen/ts/v1/service.pb"; +import { EventEmitter } from "events"; + +export const operationEmitter = new EventEmitter(); + +// Start fetching and emitting operations. +(async () => { + await ResticUI.GetOperationEvents( + {}, + (event: OperationEvent) => { + operationEmitter.emit("operation", event); + }, + { + pathPrefix: "/api", + } + ); +})(); diff --git a/webui/src/views/AddPlanModel.tsx b/webui/src/views/AddPlanModel.tsx index 21d6cad0..bccdaa15 100644 --- a/webui/src/views/AddPlanModel.tsx +++ b/webui/src/views/AddPlanModel.tsx @@ -114,6 +114,7 @@ export const AddPlanModal = ({ <> diff --git a/webui/src/views/AddRepoModel.tsx b/webui/src/views/AddRepoModel.tsx index 3e94d73f..dab93a2f 100644 --- a/webui/src/views/AddRepoModel.tsx +++ b/webui/src/views/AddRepoModel.tsx @@ -134,6 +134,7 @@ export const AddRepoModel = ({ <> diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index 784b9aec..dc9ce55f 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -14,6 +14,9 @@ import { AlertContextProvider, useAlertApi } from "../components/Alerts"; import { useShowModal } from "../components/ModalManager"; import { AddPlanModal } from "./AddPlanModel"; import { AddRepoModel } from "./AddRepoModel"; +import { MainContentArea, useSetContent } from "../components/MainContentArea"; +import { GettingStartedGuide } from "../components/GettingStartedGuide"; +import { PlanView } from "./PlanView"; const { Header, Content, Sider } = Layout; @@ -22,6 +25,7 @@ export const App: React.FC = () => { token: { colorBgContainer, colorTextLightSolid }, } = theme.useToken(); + const setContent = useSetContent(); const [config, setConfig] = useRecoilState(configState); const alertApi = useAlertApi()!; const showModal = useShowModal(); @@ -29,6 +33,8 @@ export const App: React.FC = () => { useEffect(() => { showModal(); + setContent(, [{ title: "Getting Started" }]); + fetchConfig() .then((config) => { setConfig(config); @@ -64,22 +70,7 @@ export const App: React.FC = () => { items={items} /> - - - - Content - - + ); @@ -87,6 +78,7 @@ export const App: React.FC = () => { const getSidenavItems = (config: Config | null): MenuProps["items"] => { const showModal = useShowModal(); + const setContent = useSetContent(); if (!config) return []; @@ -108,7 +100,10 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { icon: , label: plan.id, onClick: () => { - showModal(); + setContent(, [ + { title: "Plans" }, + { title: plan.id || "" }, + ]); }, }; }), diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx new file mode 100644 index 00000000..0859e132 --- /dev/null +++ b/webui/src/views/PlanView.tsx @@ -0,0 +1,123 @@ +import React, { useEffect, useState } from "react"; +import { Plan } from "../../gen/ts/v1/config.pb"; +import { Button, Flex } from "antd"; +import { SettingOutlined } from "@ant-design/icons"; +import { AddPlanModal } from "./AddPlanModel"; +import { useShowModal } from "../components/ModalManager"; +import { useRecoilValue } from "recoil"; +import { configState } from "../state/config"; +import { useAlertApi } from "../components/Alerts"; +import { ResticUI } from "../../gen/ts/v1/service.pb"; +import { + Operation, + OperationEvent, + OperationEventType, +} from "../../gen/ts/v1/operations.pb"; +import { operationEmitter } from "../state/oplog"; + +export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { + const showModal = useShowModal(); + const alertsApi = useAlertApi()!; + const [operations, setOperations] = useState([]); + + useEffect(() => { + (async () => { + try { + const ops = await ResticUI.GetOperations( + { planId: plan.id }, + { pathPrefix: "/api" } + ); + if (!ops.operations) throw new Error("No operations returned"); + setOperations(ops.operations); + } catch (e: any) { + alertsApi.error("Failed to fetch operations: " + e.message); + } + })(); + + const listener = (opEvent: OperationEvent) => { + setOperations((operations) => { + if (opEvent.type === OperationEventType.EVENT_CREATED) { + operations.push(opEvent.operation!); + } else if (opEvent.type === OperationEventType.EVENT_UPDATED) { + // We iterate from the back since the most recent operations are at the end and + // only recent ops receive updates. + for (let i = operations.length - 1; i >= 0; i--) { + if (operations[i].id === opEvent.operation?.id) { + operations[i] = opEvent.operation!; + break; + } + } + } + return operations; + }); + }; + + operationEmitter.on("operation", listener); + + return () => { + operationEmitter.removeListener("operation", listener); + }; + }, [plan.id]); + + // Gracefully handle deletions by checking if the plan is still in the config. + const config = useRecoilValue(configState); + let planInConfig = config.plans?.find((p) => p.id === plan.id); + if (!planInConfig) { + return

Plan was deleted.

; + } + plan = planInConfig; + + const handleBackupNow = async () => { + try { + ResticUI.Backup({ value: plan.id }, { pathPrefix: "/api" }); + alertsApi.success("Backup scheduled."); + } catch (e: any) { + alertsApi.error("Failed to schedule backup: " + e.message); + } + }; + + const handlePruneNow = () => { + alertsApi.warning("Not implemented yet :("); + }; + + return ( + <> + +

{plan.id}

+ + +
+ + + ); +}; + +const OperationsPanel = ({ operations }: { operations: Operation[] }) => { + return ( + <> +

Operations List

+ {operations.map((op) => { + return ( +
+

{op.id}

+
+ ); + })} + + ); +};