diff --git a/gen/go/types/value.pb.go b/gen/go/types/value.pb.go index 3c92080..b9a3c01 100644 --- a/gen/go/types/value.pb.go +++ b/gen/go/types/value.pb.go @@ -114,6 +114,53 @@ func (x *StringList) GetValues() []string { return nil } +type Int64Value struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value int64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *Int64Value) Reset() { + *x = Int64Value{} + if protoimpl.UnsafeEnabled { + mi := &file_types_value_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Int64Value) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Int64Value) ProtoMessage() {} + +func (x *Int64Value) ProtoReflect() protoreflect.Message { + mi := &file_types_value_proto_msgTypes[2] + 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 Int64Value.ProtoReflect.Descriptor instead. +func (*Int64Value) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{2} +} + +func (x *Int64Value) GetValue() int64 { + if x != nil { + return x.Value + } + return 0 +} + var File_types_value_proto protoreflect.FileDescriptor var file_types_value_proto_rawDesc = []byte{ @@ -123,10 +170,13 @@ var file_types_value_proto_rawDesc = []byte{ 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x24, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x73, 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, 0x6f, 0x72, 0x61, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, - 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x22, 0x0a, 0x0a, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 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, 0x6f, 0x72, 0x61, 0x2f, 0x67, 0x65, 0x6e, + 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -141,10 +191,11 @@ func file_types_value_proto_rawDescGZIP() []byte { return file_types_value_proto_rawDescData } -var file_types_value_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_types_value_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_types_value_proto_goTypes = []interface{}{ (*StringValue)(nil), // 0: types.StringValue (*StringList)(nil), // 1: types.StringList + (*Int64Value)(nil), // 2: types.Int64Value } var file_types_value_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -184,6 +235,18 @@ func file_types_value_proto_init() { return nil } } + file_types_value_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Int64Value); 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{ @@ -191,7 +254,7 @@ func file_types_value_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_types_value_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index de4dd98..b5292a9 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -534,7 +534,7 @@ var file_v1_service_proto_rawDesc = []byte{ 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, 0xcc, 0x08, 0x0a, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0x9c, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x61, 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, @@ -597,7 +597,12 @@ var file_v1_service_proto_rawDesc = []byte{ 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, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x5b, + 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x4e, + 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 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, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 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, @@ -634,10 +639,11 @@ var file_v1_service_proto_goTypes = []interface{}{ (*Config)(nil), // 7: v1.Config (*Repo)(nil), // 8: v1.Repo (*types.StringValue)(nil), // 9: types.StringValue - (*OperationEvent)(nil), // 10: v1.OperationEvent - (*OperationList)(nil), // 11: v1.OperationList - (*ResticSnapshotList)(nil), // 12: v1.ResticSnapshotList - (*types.StringList)(nil), // 13: types.StringList + (*types.Int64Value)(nil), // 10: types.Int64Value + (*OperationEvent)(nil), // 11: v1.OperationEvent + (*OperationList)(nil), // 12: v1.OperationList + (*ResticSnapshotList)(nil), // 13: v1.ResticSnapshotList + (*types.StringList)(nil), // 14: types.StringList } var file_v1_service_proto_depIdxs = []int32{ 5, // 0: v1.ListSnapshotFilesResponse.entries:type_name -> v1.LsEntry @@ -653,22 +659,24 @@ var file_v1_service_proto_depIdxs = []int32{ 9, // 10: v1.Restora.Forget:input_type -> types.StringValue 2, // 11: v1.Restora.Restore:input_type -> v1.RestoreSnapshotRequest 9, // 12: v1.Restora.Unlock:input_type -> types.StringValue - 9, // 13: v1.Restora.PathAutocomplete:input_type -> types.StringValue - 7, // 14: v1.Restora.GetConfig:output_type -> v1.Config - 7, // 15: v1.Restora.SetConfig:output_type -> v1.Config - 7, // 16: v1.Restora.AddRepo:output_type -> v1.Config - 10, // 17: v1.Restora.GetOperationEvents:output_type -> v1.OperationEvent - 11, // 18: v1.Restora.GetOperations:output_type -> v1.OperationList - 12, // 19: v1.Restora.ListSnapshots:output_type -> v1.ResticSnapshotList - 4, // 20: v1.Restora.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse - 6, // 21: v1.Restora.Backup:output_type -> google.protobuf.Empty - 6, // 22: v1.Restora.Prune:output_type -> google.protobuf.Empty - 6, // 23: v1.Restora.Forget:output_type -> google.protobuf.Empty - 6, // 24: v1.Restora.Restore:output_type -> google.protobuf.Empty - 6, // 25: v1.Restora.Unlock:output_type -> google.protobuf.Empty - 13, // 26: v1.Restora.PathAutocomplete:output_type -> types.StringList - 14, // [14:27] is the sub-list for method output_type - 1, // [1:14] is the sub-list for method input_type + 10, // 13: v1.Restora.Cancel:input_type -> types.Int64Value + 9, // 14: v1.Restora.PathAutocomplete:input_type -> types.StringValue + 7, // 15: v1.Restora.GetConfig:output_type -> v1.Config + 7, // 16: v1.Restora.SetConfig:output_type -> v1.Config + 7, // 17: v1.Restora.AddRepo:output_type -> v1.Config + 11, // 18: v1.Restora.GetOperationEvents:output_type -> v1.OperationEvent + 12, // 19: v1.Restora.GetOperations:output_type -> v1.OperationList + 13, // 20: v1.Restora.ListSnapshots:output_type -> v1.ResticSnapshotList + 4, // 21: v1.Restora.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse + 6, // 22: v1.Restora.Backup:output_type -> google.protobuf.Empty + 6, // 23: v1.Restora.Prune:output_type -> google.protobuf.Empty + 6, // 24: v1.Restora.Forget:output_type -> google.protobuf.Empty + 6, // 25: v1.Restora.Restore:output_type -> google.protobuf.Empty + 6, // 26: v1.Restora.Unlock:output_type -> google.protobuf.Empty + 6, // 27: v1.Restora.Cancel:output_type -> google.protobuf.Empty + 14, // 28: v1.Restora.PathAutocomplete:output_type -> types.StringList + 15, // [15:29] is the sub-list for method output_type + 1, // [1:15] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension extendee 0, // [0:1] is the sub-list for field type_name diff --git a/gen/go/v1/service.pb.gw.go b/gen/go/v1/service.pb.gw.go index 961e4a9..a04de56 100644 --- a/gen/go/v1/service.pb.gw.go +++ b/gen/go/v1/service.pb.gw.go @@ -408,6 +408,40 @@ func local_request_Restora_Unlock_0(ctx context.Context, marshaler runtime.Marsh } +func request_Restora_Cancel_0(ctx context.Context, marshaler runtime.Marshaler, client RestoraClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq types.Int64Value + 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.Cancel(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Restora_Cancel_0(ctx context.Context, marshaler runtime.Marshaler, server RestoraServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq types.Int64Value + 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.Cancel(ctx, &protoReq) + return msg, metadata, err + +} + func request_Restora_PathAutocomplete_0(ctx context.Context, marshaler runtime.Marshaler, client RestoraClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq types.StringValue var metadata runtime.ServerMetadata @@ -730,6 +764,31 @@ func RegisterRestoraHandlerServer(ctx context.Context, mux *runtime.ServeMux, se }) + mux.Handle("POST", pattern_Restora_Cancel_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.Restora/Cancel", runtime.WithHTTPPathPattern("/v1/cmd/cancel")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Restora_Cancel_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_Restora_Cancel_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_Restora_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1060,6 +1119,28 @@ func RegisterRestoraHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl }) + mux.Handle("POST", pattern_Restora_Cancel_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.Restora/Cancel", runtime.WithHTTPPathPattern("/v1/cmd/cancel")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Restora_Cancel_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Restora_Cancel_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_Restora_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1110,6 +1191,8 @@ var ( pattern_Restora_Unlock_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "cmd", "unlock"}, "")) + pattern_Restora_Cancel_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "cmd", "cancel"}, "")) + pattern_Restora_PathAutocomplete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "autocomplete", "path"}, "")) ) @@ -1138,5 +1221,7 @@ var ( forward_Restora_Unlock_0 = runtime.ForwardResponseMessage + forward_Restora_Cancel_0 = runtime.ForwardResponseMessage + forward_Restora_PathAutocomplete_0 = runtime.ForwardResponseMessage ) diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index ff3f70a..4f989db 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -33,6 +33,7 @@ const ( Restora_Forget_FullMethodName = "/v1.Restora/Forget" Restora_Restore_FullMethodName = "/v1.Restora/Restore" Restora_Unlock_FullMethodName = "/v1.Restora/Unlock" + Restora_Cancel_FullMethodName = "/v1.Restora/Cancel" Restora_PathAutocomplete_FullMethodName = "/v1.Restora/PathAutocomplete" ) @@ -57,6 +58,8 @@ type RestoraClient interface { Restore(ctx context.Context, in *RestoreSnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // Unlock synchronously attempts to unlock the repo. Will block if other operations are in progress. Unlock(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) + // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. + Cancel(ctx context.Context, in *types.Int64Value, 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) } @@ -200,6 +203,15 @@ func (c *restoraClient) Unlock(ctx context.Context, in *types.StringValue, opts return out, nil } +func (c *restoraClient) Cancel(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Restora_Cancel_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *restoraClient) PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) { out := new(types.StringList) err := c.cc.Invoke(ctx, Restora_PathAutocomplete_FullMethodName, in, out, opts...) @@ -230,6 +242,8 @@ type RestoraServer interface { Restore(context.Context, *RestoreSnapshotRequest) (*emptypb.Empty, error) // Unlock synchronously attempts to unlock the repo. Will block if other operations are in progress. Unlock(context.Context, *types.StringValue) (*emptypb.Empty, error) + // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. + Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error) // PathAutocomplete provides path autocompletion options for a given filesystem path. PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) mustEmbedUnimplementedRestoraServer() @@ -275,6 +289,9 @@ func (UnimplementedRestoraServer) Restore(context.Context, *RestoreSnapshotReque func (UnimplementedRestoraServer) Unlock(context.Context, *types.StringValue) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Unlock not implemented") } +func (UnimplementedRestoraServer) Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Cancel not implemented") +} func (UnimplementedRestoraServer) PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) { return nil, status.Errorf(codes.Unimplemented, "method PathAutocomplete not implemented") } @@ -510,6 +527,24 @@ func _Restora_Unlock_Handler(srv interface{}, ctx context.Context, dec func(inte return interceptor(ctx, in, info, handler) } +func _Restora_Cancel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(types.Int64Value) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RestoraServer).Cancel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Restora_Cancel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RestoraServer).Cancel(ctx, req.(*types.Int64Value)) + } + return interceptor(ctx, in, info, handler) +} + func _Restora_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 { @@ -579,6 +614,10 @@ var Restora_ServiceDesc = grpc.ServiceDesc{ MethodName: "Unlock", Handler: _Restora_Unlock_Handler, }, + { + MethodName: "Cancel", + Handler: _Restora_Cancel_Handler, + }, { MethodName: "PathAutocomplete", Handler: _Restora_PathAutocomplete_Handler, diff --git a/internal/api/server.go b/internal/api/server.go index 569065c..63e64f0 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -314,6 +314,14 @@ func (s *Server) Unlock(ctx context.Context, req *types.StringValue) (*emptypb.E return &emptypb.Empty{}, nil } +func (s *Server) Cancel(ctx context.Context, req *types.Int64Value) (*emptypb.Empty, error) { + if err := s.orchestrator.CancelOperation(req.Value, v1.OperationStatus_STATUS_USER_CANCELLED); err != nil { + return nil, fmt.Errorf("failed to cancel operation %d: %w", req.Value, err) + } + + 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/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 6057e23..ba8341f 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -6,6 +6,7 @@ import ( "fmt" "slices" "sync" + "sync/atomic" "time" v1 "github.com/garethgeorge/restora/gen/go/v1" @@ -19,6 +20,7 @@ import ( var ErrRepoNotFound = errors.New("repo not found") var ErrRepoInitializationFailed = errors.New("repo initialization failed") var ErrPlanNotFound = errors.New("plan not found") +var ErrTaskNotFound = errors.New("task not found") const ( TaskPriorityDefault = iota @@ -38,6 +40,8 @@ type Orchestrator struct { // now for the purpose of testing; used by Run() to get the current time. now func() time.Time + + runningTask atomic.Pointer[taskExecutionInfo] } func NewOrchestrator(resticBin string, cfg *v1.Config, oplog *oplog.OpLog) (*Orchestrator, error) { @@ -163,6 +167,41 @@ func (o *Orchestrator) GetPlan(planId string) (*v1.Plan, error) { return nil, ErrPlanNotFound } +func (o *Orchestrator) CancelOperation(operationId int64, status v1.OperationStatus) error { + o.mu.Lock() + defer o.mu.Unlock() + + // note: if the task is running the requested status will not be set. + if running := o.runningTask.Load(); running != nil && running.operationId == operationId { + running.cancel() + } + + tasks := o.taskQueue.Reset() + remaining := make([]scheduledTask, 0, len(tasks)) + + for _, t := range tasks { + if t.task.OperationId() == operationId { + if err := t.task.Cancel(status); err != nil { + return fmt.Errorf("cancel task %q: %w", t.task.Name(), err) + } + + // check if the task has a next after it's current 'runAt' time, if it does then we will schedule the next run. + if nextTime := t.task.Next(t.runAt); nextTime != nil { + remaining = append(remaining, scheduledTask{ + task: t.task, + runAt: *nextTime, + }) + } + } else { + remaining = append(remaining, *t) + } + } + + o.taskQueue.Push(remaining...) + + return nil +} + // Run is the main orchestration loop. Cancel the context to stop the loop. func (o *Orchestrator) Run(mainCtx context.Context) { zap.L().Info("starting orchestrator loop") @@ -179,12 +218,24 @@ func (o *Orchestrator) Run(mainCtx context.Context) { } zap.L().Info("running task", zap.String("task", t.task.Name())) - if err := t.task.Run(mainCtx); err != nil { + + taskCtx, cancel := context.WithCancel(mainCtx) + + if swapped := o.runningTask.CompareAndSwap(nil, &taskExecutionInfo{ + operationId: t.task.OperationId(), + cancel: cancel, + }); !swapped { + zap.L().Fatal("failed to start task, another task is already running. Was Run() called twice?") + } + + if err := t.task.Run(taskCtx); err != nil { zap.L().Error("task failed", zap.String("task", t.task.Name()), zap.Error(err)) } else { zap.L().Info("task finished", zap.String("task", t.task.Name())) } + o.runningTask.Store(nil) + if nextTime := t.task.Next(o.curTime()); nextTime != nil { o.taskQueue.Push(scheduledTask{ task: t.task, @@ -268,3 +319,8 @@ func (rp *resticRepoPool) GetRepo(repoId string) (repo *RepoOrchestrator, err er rp.repos[repoId] = repo return repo, nil } + +type taskExecutionInfo struct { + operationId int64 + cancel func() +} diff --git a/internal/orchestrator/scheduledtaskheap.go b/internal/orchestrator/scheduledtaskheap.go index 5322fbd..8854b24 100644 --- a/internal/orchestrator/scheduledtaskheap.go +++ b/internal/orchestrator/scheduledtaskheap.go @@ -24,15 +24,17 @@ func (t *taskQueue) curTime() time.Time { return time.Now() } -func (t *taskQueue) Push(task scheduledTask) { +func (t *taskQueue) Push(tasks ...scheduledTask) { t.mu.Lock() defer t.mu.Unlock() - if task.task == nil { - panic("task cannot be nil") + for _, task := range tasks { + if task.task == nil { + panic("task cannot be nil") + } + heap.Push(&t.heap, &task) } - heap.Push(&t.heap, &task) if t.notify != nil { t.notify <- struct{}{} } @@ -57,12 +59,12 @@ func (t *taskQueue) Dequeue(ctx context.Context) *scheduledTask { t.dequeueMu.Lock() defer t.dequeueMu.Unlock() + t.mu.Lock() t.notify = make(chan struct{}, 1) defer func() { t.notify = nil }() - t.mu.Lock() for { first, ok := t.heap.Peek().(*scheduledTask) if !ok { // no tasks in heap. diff --git a/internal/orchestrator/tasks.go b/internal/orchestrator/tasks.go index 7c8f1b7..ee93af7 100644 --- a/internal/orchestrator/tasks.go +++ b/internal/orchestrator/tasks.go @@ -16,69 +16,64 @@ 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. - Cancel(withStatus v1.OperationStatus) error // cancel the task's execution with the given status (either STATUS_USER_CANCELLED or STATUS_SYSTEM_CANCELLED). + Cancel(withStatus v1.OperationStatus) error // informat the task that it's scheduled execution will be skipped (either STATUS_USER_CANCELLED or STATUS_SYSTEM_CANCELLED). OperationId() int64 // the id of the operation associated with this task (if any). } type TaskWithOperation struct { - orch *Orchestrator - op atomic.Pointer[v1.Operation] - cancelled chan struct{} + orch *Orchestrator + op *v1.Operation + running atomic.Bool } func (t *TaskWithOperation) OperationId() int64 { - return t.op.Load().GetId() + if t.op == nil { + return 0 + } + return t.op.Id } func (t *TaskWithOperation) setOperation(op *v1.Operation) error { - if t.op.Load() != nil { + if t.op != nil { return errors.New("task already has an operation") } if err := t.orch.OpLog.Add(op); err != nil { return fmt.Errorf("task failed to add operation to oplog: %v", err) } - t.op.Store(op) - t.cancelled = make(chan struct{}, 1) + t.op = op return nil } func (t *TaskWithOperation) runWithOpAndContext(ctx context.Context, do func(ctx context.Context, op *v1.Operation) error) error { - op := t.op.Load() - if op == nil { + if t.op == nil { return errors.New("task has no operation, a call to setOperation first is required.") } - go func() { - t.op.Store(nil) - }() + if t.running.Load() { + return errors.New("task is already running") + } - ctx, cancel := context.WithCancel(ctx) - defer cancel() + t.running.Store(true) + defer t.running.Store(false) - go func() { - select { - case <-ctx.Done(): - case <-t.cancelled: - cancel() - } - }() - - return WithOperation(t.orch.OpLog, op, func() error { - return do(ctx, op) + return WithOperation(t.orch.OpLog, t.op, func() error { + return do(ctx, t.op) }) } // Cancel marks a task as cancelled. Note that, unintuitively, it is actually an error to call cancel on a running task. func (t *TaskWithOperation) Cancel(withStatus v1.OperationStatus) error { - close(t.cancelled) - op := t.op.Load() - if op == nil { + if t.running.Load() { + return errors.New("cannot cancel a running task") // should never happen. + } + if t.op == nil { return nil } - op.Status = withStatus - op.UnixTimeEndMs = curTimeMillis() - if err := t.orch.OpLog.Update(op); err != nil { - return fmt.Errorf("failed to update operation %v in oplog: %w", op.Id, err) + t.op.Status = withStatus + t.op.UnixTimeEndMs = curTimeMillis() + if err := t.orch.OpLog.Update(t.op); err != nil { + return fmt.Errorf("failed to update operation %v in oplog: %w", t.op.Id, err) } + t.op = nil return nil } diff --git a/pkg/restic/error.go b/pkg/restic/error.go index 30ad12d..6577945 100644 --- a/pkg/restic/error.go +++ b/pkg/restic/error.go @@ -16,7 +16,7 @@ type CmdError struct { func (e *CmdError) Error() string { m := fmt.Sprintf("command %q failed: %s", e.Command, e.Err.Error()) if e.Output != "" { - m += "\nDetails: \n" + e.Output + m += "\nProcess STDOUT: \n" + e.Output } return m } @@ -35,6 +35,7 @@ func newCmdError(cmd *exec.Cmd, output string, err error) *CmdError { cerr := &CmdError{ Command: cmd.String(), Err: err, + Output: output, } if len(output) >= outputBufferLimit { @@ -48,5 +49,6 @@ func newCmdErrorPreformatted(cmd *exec.Cmd, output string, err error) *CmdError return &CmdError{ Command: cmd.String(), Err: err, + Output: output, } } diff --git a/proto/types/value.proto b/proto/types/value.proto index 457e9a0..8bd87a3 100644 --- a/proto/types/value.proto +++ b/proto/types/value.proto @@ -10,4 +10,8 @@ message StringValue { message StringList { repeated string values = 1; -} \ No newline at end of file +} + +message Int64Value { + int64 value = 1; +} diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 9e53fc4..e6a8b39 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -99,6 +99,14 @@ service Restora { }; } + // Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed. + rpc Cancel(types.Int64Value) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/v1/cmd/cancel" + body: "*" + }; + } + // PathAutocomplete provides path autocompletion options for a given filesystem path. rpc PathAutocomplete (types.StringValue) returns (types.StringList) { option (google.api.http) = { diff --git a/webui/gen/ts/types/value.pb.ts b/webui/gen/ts/types/value.pb.ts index 97348c1..a35d1c4 100644 --- a/webui/gen/ts/types/value.pb.ts +++ b/webui/gen/ts/types/value.pb.ts @@ -9,4 +9,8 @@ export type StringValue = { export type StringList = { values?: string[] +} + +export type Int64Value = { + value?: string } \ No newline at end of file diff --git a/webui/gen/ts/v1/service.pb.ts b/webui/gen/ts/v1/service.pb.ts index a451ecf..d22ee7c 100644 --- a/webui/gen/ts/v1/service.pb.ts +++ b/webui/gen/ts/v1/service.pb.ts @@ -92,6 +92,9 @@ export class Restora { static Unlock(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise { return fm.fetchReq(`/v1/cmd/unlock`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) } + static Cancel(req: TypesValue.Int64Value, initReq?: fm.InitReq): Promise { + return fm.fetchReq(`/v1/cmd/cancel`, {...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)}) } diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index 4fecd48..0bf5317 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -5,6 +5,7 @@ import { OperationStatus, } from "../../gen/ts/v1/operations.pb"; import { + Button, Card, Col, Collapse, @@ -43,8 +44,9 @@ import { normalizeSnapshotId, } from "../lib/formatting"; import _ from "lodash"; -import { GetOperationsRequest } from "../../gen/ts/v1/service.pb"; +import { GetOperationsRequest, Restora } from "../../gen/ts/v1/service.pb"; import { useAlertApi } from "./Alerts"; +import { MessageInstance } from "antd/es/message/interface"; export const OperationList = ({ req, @@ -121,13 +123,13 @@ export const OperationList = ({ renderItem={(backup) => { const ops = backup.operations; if (ops.length === 1) { - return ; + return ; } return ( {ops.map((op) => ( - + ))} ); @@ -143,7 +145,8 @@ export const OperationList = ({ export const OperationRow = ({ operation, -}: React.PropsWithoutRef<{ operation: EOperation }>) => { + alertApi, +}: React.PropsWithoutRef<{ operation: EOperation, alertApi?: MessageInstance }>) => { const details = detailsForOperation(operation); const displayType = getTypeForDisplay(operation); let avatar: React.ReactNode; @@ -176,13 +179,24 @@ export const OperationRow = ({ } const opName = displayTypeToString(getTypeForDisplay(operation)); - const title = ( + let title = ( <> {formatTime(operation.unixTimeStartMs!)} - {opName}{" "} {details.displayState} ); + if (operation.status === OperationStatus.STATUS_PENDING || operation.status == OperationStatus.STATUS_INPROGRESS) { + title = <> + {title} + + + } + let body: React.ReactNode | undefined; if ( diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index b2f8f8e..4b61203 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -93,12 +93,13 @@ export const OperationTree = ({ let oplist: React.ReactNode | null = null; if (selectedBackupId) { const backup = backups.find((b) => b.id === selectedBackupId); + if (!backup) { oplist = ; } else { oplist = ( <> -

Backup at {formatTime(backup.displayTime)}

+

Backup on {formatTime(backup.displayTime)}

);