feat: implement 'run command' button to execute arbitrary restic commands in a repo

This commit is contained in:
garethgeorge
2024-05-07 23:40:32 -07:00
parent c236b1f3f1
commit 74c8abc39a
21 changed files with 1635 additions and 426 deletions
+432 -295
View File
File diff suppressed because it is too large Load Diff
+230
View File
@@ -0,0 +1,230 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
// protoc (unknown)
// source: v1/hub.proto
package v1
import (
_ "github.com/garethgeorge/backrest/gen/go/types"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type GetInstancesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Instances []*Instance `protobuf:"bytes,1,rep,name=instances,proto3" json:"instances,omitempty"`
}
func (x *GetInstancesResponse) Reset() {
*x = GetInstancesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_v1_hub_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetInstancesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetInstancesResponse) ProtoMessage() {}
func (x *GetInstancesResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_hub_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 GetInstancesResponse.ProtoReflect.Descriptor instead.
func (*GetInstancesResponse) Descriptor() ([]byte, []int) {
return file_v1_hub_proto_rawDescGZIP(), []int{0}
}
func (x *GetInstancesResponse) GetInstances() []*Instance {
if x != nil {
return x.Instances
}
return nil
}
type Instance struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
}
func (x *Instance) Reset() {
*x = Instance{}
if protoimpl.UnsafeEnabled {
mi := &file_v1_hub_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Instance) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Instance) ProtoMessage() {}
func (x *Instance) ProtoReflect() protoreflect.Message {
mi := &file_v1_hub_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 Instance.ProtoReflect.Descriptor instead.
func (*Instance) Descriptor() ([]byte, []int) {
return file_v1_hub_proto_rawDescGZIP(), []int{1}
}
func (x *Instance) GetId() string {
if x != nil {
return x.Id
}
return ""
}
var File_v1_hub_proto protoreflect.FileDescriptor
var file_v1_hub_proto_rawDesc = []byte{
0x0a, 0x0c, 0x76, 0x31, 0x2f, 0x68, 0x75, 0x62, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02,
0x76, 0x31, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, 0x74, 0x79, 0x70, 0x65, 0x73,
0x2f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x42, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x49, 0x6e,
0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x2a, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65,
0x52, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x1a, 0x0a, 0x08, 0x49,
0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x32, 0x49, 0x0a, 0x03, 0x48, 0x75, 0x62, 0x12, 0x42,
0x0a, 0x0c, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 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, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49,
0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x00, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61,
0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_v1_hub_proto_rawDescOnce sync.Once
file_v1_hub_proto_rawDescData = file_v1_hub_proto_rawDesc
)
func file_v1_hub_proto_rawDescGZIP() []byte {
file_v1_hub_proto_rawDescOnce.Do(func() {
file_v1_hub_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_hub_proto_rawDescData)
})
return file_v1_hub_proto_rawDescData
}
var file_v1_hub_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_v1_hub_proto_goTypes = []interface{}{
(*GetInstancesResponse)(nil), // 0: v1.GetInstancesResponse
(*Instance)(nil), // 1: v1.Instance
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
}
var file_v1_hub_proto_depIdxs = []int32{
1, // 0: v1.GetInstancesResponse.instances:type_name -> v1.Instance
2, // 1: v1.Hub.GetInstances:input_type -> google.protobuf.Empty
0, // 2: v1.Hub.GetInstances:output_type -> v1.GetInstancesResponse
2, // [2:3] is the sub-list for method output_type
1, // [1:2] 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
}
func init() { file_v1_hub_proto_init() }
func file_v1_hub_proto_init() {
if File_v1_hub_proto != nil {
return
}
file_v1_config_proto_init()
file_v1_restic_proto_init()
file_v1_operations_proto_init()
if !protoimpl.UnsafeEnabled {
file_v1_hub_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetInstancesResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v1_hub_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Instance); 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{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_v1_hub_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_v1_hub_proto_goTypes,
DependencyIndexes: file_v1_hub_proto_depIdxs,
MessageInfos: file_v1_hub_proto_msgTypes,
}.Build()
File_v1_hub_proto = out.File
file_v1_hub_proto_rawDesc = nil
file_v1_hub_proto_goTypes = nil
file_v1_hub_proto_depIdxs = nil
}
+112
View File
@@ -0,0 +1,112 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc (unknown)
// source: v1/hub.proto
package v1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
const (
Hub_GetInstances_FullMethodName = "/v1.Hub/GetInstances"
)
// HubClient is the client API for Hub service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HubClient interface {
// GetInstances returns a list of all instances
GetInstances(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetInstancesResponse, error)
}
type hubClient struct {
cc grpc.ClientConnInterface
}
func NewHubClient(cc grpc.ClientConnInterface) HubClient {
return &hubClient{cc}
}
func (c *hubClient) GetInstances(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetInstancesResponse, error) {
out := new(GetInstancesResponse)
err := c.cc.Invoke(ctx, Hub_GetInstances_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// HubServer is the server API for Hub service.
// All implementations must embed UnimplementedHubServer
// for forward compatibility
type HubServer interface {
// GetInstances returns a list of all instances
GetInstances(context.Context, *emptypb.Empty) (*GetInstancesResponse, error)
mustEmbedUnimplementedHubServer()
}
// UnimplementedHubServer must be embedded to have forward compatible implementations.
type UnimplementedHubServer struct {
}
func (UnimplementedHubServer) GetInstances(context.Context, *emptypb.Empty) (*GetInstancesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetInstances not implemented")
}
func (UnimplementedHubServer) mustEmbedUnimplementedHubServer() {}
// UnsafeHubServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to HubServer will
// result in compilation errors.
type UnsafeHubServer interface {
mustEmbedUnimplementedHubServer()
}
func RegisterHubServer(s grpc.ServiceRegistrar, srv HubServer) {
s.RegisterService(&Hub_ServiceDesc, srv)
}
func _Hub_GetInstances_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HubServer).GetInstances(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Hub_GetInstances_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HubServer).GetInstances(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
// Hub_ServiceDesc is the grpc.ServiceDesc for Hub service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Hub_ServiceDesc = grpc.ServiceDesc{
ServiceName: "v1.Hub",
HandlerType: (*HubServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetInstances",
Handler: _Hub_GetInstances_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "v1/hub.proto",
}
+194 -116
View File
@@ -662,6 +662,61 @@ func (x *LsEntry) GetCtime() string {
return ""
}
type RunCommandRequest 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"`
Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"`
}
func (x *RunCommandRequest) Reset() {
*x = RunCommandRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_v1_service_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RunCommandRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RunCommandRequest) ProtoMessage() {}
func (x *RunCommandRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_service_proto_msgTypes[9]
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 RunCommandRequest.ProtoReflect.Descriptor instead.
func (*RunCommandRequest) Descriptor() ([]byte, []int) {
return file_v1_service_proto_rawDescGZIP(), []int{9}
}
func (x *RunCommandRequest) GetRepoId() string {
if x != nil {
return x.RepoId
}
return ""
}
func (x *RunCommandRequest) GetCommand() string {
if x != nil {
return x.Command
}
return ""
}
var File_v1_service_proto protoreflect.FileDescriptor
var file_v1_service_proto_rawDesc = []byte{
@@ -741,81 +796,89 @@ var file_v1_service_proto_rawDesc = []byte{
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, 0xe2, 0x08, 0x0a, 0x08, 0x42, 0x61,
0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x12, 0x31, 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, 0x00, 0x12, 0x25, 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, 0x00,
0x12, 0x21, 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, 0x00, 0x12, 0x44, 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, 0x00, 0x30, 0x01, 0x12, 0x3e, 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, 0x00, 0x12, 0x43, 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, 0x00, 0x12, 0x52,
0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69,
0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61,
0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73,
0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, 0x70, 0x73,
0x68, 0x6f, 0x74, 0x73, 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, 0x00, 0x12, 0x36, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x12, 0x2e, 0x74,
0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x46, 0x0a, 0x11, 0x52, 0x75, 0x6e,
0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 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, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61,
0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
0x64, 0x32, 0x9e, 0x09, 0x0a, 0x08, 0x42, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x12, 0x31,
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,
0x00, 0x12, 0x25, 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, 0x00, 0x12, 0x21, 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, 0x00, 0x12, 0x44, 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, 0x00, 0x30,
0x01, 0x12, 0x3e, 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,
0x00, 0x12, 0x43, 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, 0x00, 0x12, 0x52, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e,
0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31,
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0e, 0x49, 0x6e,
0x64, 0x65, 0x78, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 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, 0x00, 0x12, 0x35, 0x0a, 0x05, 0x50, 0x72,
0x75, 0x6e, 0x65, 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,
0x00, 0x12, 0x35, 0x0a, 0x06, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x76, 0x31,
0x2e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x00, 0x12, 0x3f, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74,
0x6f, 0x72, 0x65, 0x12, 0x1a, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x36, 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, 0x00, 0x12, 0x35, 0x0a, 0x05, 0x50, 0x72, 0x75, 0x6e, 0x65, 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, 0x00, 0x12, 0x36, 0x0a, 0x06, 0x55, 0x6e, 0x6c,
0x6f, 0x63, 0x6b, 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,
0x00, 0x12, 0x35, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 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, 0x00, 0x12, 0x35, 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, 0x00, 0x12,
0x32, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x12, 0x2e, 0x76, 0x31, 0x2e,
0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11,
0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f,
0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e,
0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73,
0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x41,
0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x17,
0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x35, 0x0a, 0x06, 0x46, 0x6f, 0x72,
0x67, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x00,
0x12, 0x3f, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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,
0x00, 0x12, 0x3b, 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, 0x00, 0x42, 0x2c,
0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72,
0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65,
0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x00, 0x12, 0x36, 0x0a, 0x06, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 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, 0x00, 0x12, 0x35, 0x0a, 0x05, 0x53, 0x74, 0x61,
0x74, 0x73, 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, 0x00,
0x12, 0x35, 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, 0x00, 0x12, 0x32, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f,
0x67, 0x73, 0x12, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42,
0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x0a, 0x52,
0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x31, 0x2e, 0x52,
0x75, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61,
0x6c, 0x75, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f,
0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65,
0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74,
0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x22, 0x00, 0x12, 0x41, 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f,
0x72, 0x79, 0x12, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73,
0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x00, 0x12, 0x3b, 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, 0x00, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61,
0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -830,7 +893,7 @@ func file_v1_service_proto_rawDescGZIP() []byte {
return file_v1_service_proto_rawDescData
}
var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_v1_service_proto_goTypes = []interface{}{
(*ClearHistoryRequest)(nil), // 0: v1.ClearHistoryRequest
(*ForgetRequest)(nil), // 1: v1.ForgetRequest
@@ -841,59 +904,62 @@ var file_v1_service_proto_goTypes = []interface{}{
(*ListSnapshotFilesResponse)(nil), // 6: v1.ListSnapshotFilesResponse
(*LogDataRequest)(nil), // 7: v1.LogDataRequest
(*LsEntry)(nil), // 8: v1.LsEntry
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
(*Config)(nil), // 10: v1.Config
(*Repo)(nil), // 11: v1.Repo
(*types.StringValue)(nil), // 12: types.StringValue
(*types.Int64Value)(nil), // 13: types.Int64Value
(*OperationEvent)(nil), // 14: v1.OperationEvent
(*OperationList)(nil), // 15: v1.OperationList
(*ResticSnapshotList)(nil), // 16: v1.ResticSnapshotList
(*types.BytesValue)(nil), // 17: types.BytesValue
(*types.StringList)(nil), // 18: types.StringList
(*RunCommandRequest)(nil), // 9: v1.RunCommandRequest
(*emptypb.Empty)(nil), // 10: google.protobuf.Empty
(*Config)(nil), // 11: v1.Config
(*Repo)(nil), // 12: v1.Repo
(*types.StringValue)(nil), // 13: types.StringValue
(*types.Int64Value)(nil), // 14: types.Int64Value
(*OperationEvent)(nil), // 15: v1.OperationEvent
(*OperationList)(nil), // 16: v1.OperationList
(*ResticSnapshotList)(nil), // 17: v1.ResticSnapshotList
(*types.BytesValue)(nil), // 18: types.BytesValue
(*types.StringList)(nil), // 19: types.StringList
}
var file_v1_service_proto_depIdxs = []int32{
8, // 0: v1.ListSnapshotFilesResponse.entries:type_name -> v1.LsEntry
9, // 1: v1.Backrest.GetConfig:input_type -> google.protobuf.Empty
10, // 2: v1.Backrest.SetConfig:input_type -> v1.Config
11, // 3: v1.Backrest.AddRepo:input_type -> v1.Repo
9, // 4: v1.Backrest.GetOperationEvents:input_type -> google.protobuf.Empty
10, // 1: v1.Backrest.GetConfig:input_type -> google.protobuf.Empty
11, // 2: v1.Backrest.SetConfig:input_type -> v1.Config
12, // 3: v1.Backrest.AddRepo:input_type -> v1.Repo
10, // 4: v1.Backrest.GetOperationEvents:input_type -> google.protobuf.Empty
3, // 5: v1.Backrest.GetOperations:input_type -> v1.GetOperationsRequest
2, // 6: v1.Backrest.ListSnapshots:input_type -> v1.ListSnapshotsRequest
5, // 7: v1.Backrest.ListSnapshotFiles:input_type -> v1.ListSnapshotFilesRequest
12, // 8: v1.Backrest.IndexSnapshots:input_type -> types.StringValue
12, // 9: v1.Backrest.Backup:input_type -> types.StringValue
12, // 10: v1.Backrest.Prune:input_type -> types.StringValue
13, // 8: v1.Backrest.IndexSnapshots:input_type -> types.StringValue
13, // 9: v1.Backrest.Backup:input_type -> types.StringValue
13, // 10: v1.Backrest.Prune:input_type -> types.StringValue
1, // 11: v1.Backrest.Forget:input_type -> v1.ForgetRequest
4, // 12: v1.Backrest.Restore:input_type -> v1.RestoreSnapshotRequest
12, // 13: v1.Backrest.Unlock:input_type -> types.StringValue
12, // 14: v1.Backrest.Stats:input_type -> types.StringValue
13, // 15: v1.Backrest.Cancel:input_type -> types.Int64Value
13, // 13: v1.Backrest.Unlock:input_type -> types.StringValue
13, // 14: v1.Backrest.Stats:input_type -> types.StringValue
14, // 15: v1.Backrest.Cancel:input_type -> types.Int64Value
7, // 16: v1.Backrest.GetLogs:input_type -> v1.LogDataRequest
13, // 17: v1.Backrest.GetDownloadURL:input_type -> types.Int64Value
0, // 18: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest
12, // 19: v1.Backrest.PathAutocomplete:input_type -> types.StringValue
10, // 20: v1.Backrest.GetConfig:output_type -> v1.Config
10, // 21: v1.Backrest.SetConfig:output_type -> v1.Config
10, // 22: v1.Backrest.AddRepo:output_type -> v1.Config
14, // 23: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent
15, // 24: v1.Backrest.GetOperations:output_type -> v1.OperationList
16, // 25: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList
6, // 26: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
9, // 27: v1.Backrest.IndexSnapshots:output_type -> google.protobuf.Empty
9, // 28: v1.Backrest.Backup:output_type -> google.protobuf.Empty
9, // 29: v1.Backrest.Prune:output_type -> google.protobuf.Empty
9, // 30: v1.Backrest.Forget:output_type -> google.protobuf.Empty
9, // 31: v1.Backrest.Restore:output_type -> google.protobuf.Empty
9, // 32: v1.Backrest.Unlock:output_type -> google.protobuf.Empty
9, // 33: v1.Backrest.Stats:output_type -> google.protobuf.Empty
9, // 34: v1.Backrest.Cancel:output_type -> google.protobuf.Empty
17, // 35: v1.Backrest.GetLogs:output_type -> types.BytesValue
12, // 36: v1.Backrest.GetDownloadURL:output_type -> types.StringValue
9, // 37: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty
18, // 38: v1.Backrest.PathAutocomplete:output_type -> types.StringList
20, // [20:39] is the sub-list for method output_type
1, // [1:20] is the sub-list for method input_type
9, // 17: v1.Backrest.RunCommand:input_type -> v1.RunCommandRequest
14, // 18: v1.Backrest.GetDownloadURL:input_type -> types.Int64Value
0, // 19: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest
13, // 20: v1.Backrest.PathAutocomplete:input_type -> types.StringValue
11, // 21: v1.Backrest.GetConfig:output_type -> v1.Config
11, // 22: v1.Backrest.SetConfig:output_type -> v1.Config
11, // 23: v1.Backrest.AddRepo:output_type -> v1.Config
15, // 24: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent
16, // 25: v1.Backrest.GetOperations:output_type -> v1.OperationList
17, // 26: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList
6, // 27: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
10, // 28: v1.Backrest.IndexSnapshots:output_type -> google.protobuf.Empty
10, // 29: v1.Backrest.Backup:output_type -> google.protobuf.Empty
10, // 30: v1.Backrest.Prune:output_type -> google.protobuf.Empty
10, // 31: v1.Backrest.Forget:output_type -> google.protobuf.Empty
10, // 32: v1.Backrest.Restore:output_type -> google.protobuf.Empty
10, // 33: v1.Backrest.Unlock:output_type -> google.protobuf.Empty
10, // 34: v1.Backrest.Stats:output_type -> google.protobuf.Empty
10, // 35: v1.Backrest.Cancel:output_type -> google.protobuf.Empty
18, // 36: v1.Backrest.GetLogs:output_type -> types.BytesValue
18, // 37: v1.Backrest.RunCommand:output_type -> types.BytesValue
13, // 38: v1.Backrest.GetDownloadURL:output_type -> types.StringValue
10, // 39: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty
19, // 40: v1.Backrest.PathAutocomplete:output_type -> types.StringList
21, // [21:41] is the sub-list for method output_type
1, // [1:21] 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
@@ -1016,6 +1082,18 @@ func file_v1_service_proto_init() {
return nil
}
}
file_v1_service_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RunCommandRequest); 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{
@@ -1023,7 +1101,7 @@ func file_v1_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_v1_service_proto_rawDesc,
NumEnums: 0,
NumMessages: 9,
NumMessages: 10,
NumExtensions: 0,
NumServices: 1,
},
+66
View File
@@ -37,6 +37,7 @@ const (
Backrest_Stats_FullMethodName = "/v1.Backrest/Stats"
Backrest_Cancel_FullMethodName = "/v1.Backrest/Cancel"
Backrest_GetLogs_FullMethodName = "/v1.Backrest/GetLogs"
Backrest_RunCommand_FullMethodName = "/v1.Backrest/RunCommand"
Backrest_GetDownloadURL_FullMethodName = "/v1.Backrest/GetDownloadURL"
Backrest_ClearHistory_FullMethodName = "/v1.Backrest/ClearHistory"
Backrest_PathAutocomplete_FullMethodName = "/v1.Backrest/PathAutocomplete"
@@ -71,6 +72,8 @@ type BackrestClient interface {
Cancel(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*emptypb.Empty, error)
// GetLogs returns the keyed large data for the given operation.
GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (*types.BytesValue, error)
// RunCommand executes a generic restic command on the repository.
RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (Backrest_RunCommandClient, error)
// GetDownloadURL returns a signed download URL given a forget operation ID.
GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error)
// Clears the history of operations
@@ -254,6 +257,38 @@ func (c *backrestClient) GetLogs(ctx context.Context, in *LogDataRequest, opts .
return out, nil
}
func (c *backrestClient) RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (Backrest_RunCommandClient, error) {
stream, err := c.cc.NewStream(ctx, &Backrest_ServiceDesc.Streams[1], Backrest_RunCommand_FullMethodName, opts...)
if err != nil {
return nil, err
}
x := &backrestRunCommandClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type Backrest_RunCommandClient interface {
Recv() (*types.BytesValue, error)
grpc.ClientStream
}
type backrestRunCommandClient struct {
grpc.ClientStream
}
func (x *backrestRunCommandClient) Recv() (*types.BytesValue, error) {
m := new(types.BytesValue)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *backrestClient) GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error) {
out := new(types.StringValue)
err := c.cc.Invoke(ctx, Backrest_GetDownloadURL_FullMethodName, in, out, opts...)
@@ -310,6 +345,8 @@ type BackrestServer interface {
Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error)
// GetLogs returns the keyed large data for the given operation.
GetLogs(context.Context, *LogDataRequest) (*types.BytesValue, error)
// RunCommand executes a generic restic command on the repository.
RunCommand(*RunCommandRequest, Backrest_RunCommandServer) error
// GetDownloadURL returns a signed download URL given a forget operation ID.
GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error)
// Clears the history of operations
@@ -371,6 +408,9 @@ func (UnimplementedBackrestServer) Cancel(context.Context, *types.Int64Value) (*
func (UnimplementedBackrestServer) GetLogs(context.Context, *LogDataRequest) (*types.BytesValue, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetLogs not implemented")
}
func (UnimplementedBackrestServer) RunCommand(*RunCommandRequest, Backrest_RunCommandServer) error {
return status.Errorf(codes.Unimplemented, "method RunCommand not implemented")
}
func (UnimplementedBackrestServer) GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDownloadURL not implemented")
}
@@ -684,6 +724,27 @@ func _Backrest_GetLogs_Handler(srv interface{}, ctx context.Context, dec func(in
return interceptor(ctx, in, info, handler)
}
func _Backrest_RunCommand_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(RunCommandRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(BackrestServer).RunCommand(m, &backrestRunCommandServer{stream})
}
type Backrest_RunCommandServer interface {
Send(*types.BytesValue) error
grpc.ServerStream
}
type backrestRunCommandServer struct {
grpc.ServerStream
}
func (x *backrestRunCommandServer) Send(m *types.BytesValue) error {
return x.ServerStream.SendMsg(m)
}
func _Backrest_GetDownloadURL_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 {
@@ -824,6 +885,11 @@ var Backrest_ServiceDesc = grpc.ServiceDesc{
Handler: _Backrest_GetOperationEvents_Handler,
ServerStreams: true,
},
{
StreamName: "RunCommand",
Handler: _Backrest_RunCommand_Handler,
ServerStreams: true,
},
},
Metadata: "v1/service.proto",
}
+115
View File
@@ -0,0 +1,115 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: v1/hub.proto
package v1connect
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
v1 "github.com/garethgeorge/backrest/gen/go/v1"
emptypb "google.golang.org/protobuf/types/known/emptypb"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// HubName is the fully-qualified name of the Hub service.
HubName = "v1.Hub"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// HubGetInstancesProcedure is the fully-qualified name of the Hub's GetInstances RPC.
HubGetInstancesProcedure = "/v1.Hub/GetInstances"
)
// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package.
var (
hubServiceDescriptor = v1.File_v1_hub_proto.Services().ByName("Hub")
hubGetInstancesMethodDescriptor = hubServiceDescriptor.Methods().ByName("GetInstances")
)
// HubClient is a client for the v1.Hub service.
type HubClient interface {
// GetInstances returns a list of all instances
GetInstances(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetInstancesResponse], error)
}
// NewHubClient constructs a client for the v1.Hub service. By default, it uses the Connect protocol
// with the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed requests. To
// use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or connect.WithGRPCWeb()
// options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewHubClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) HubClient {
baseURL = strings.TrimRight(baseURL, "/")
return &hubClient{
getInstances: connect.NewClient[emptypb.Empty, v1.GetInstancesResponse](
httpClient,
baseURL+HubGetInstancesProcedure,
connect.WithSchema(hubGetInstancesMethodDescriptor),
connect.WithClientOptions(opts...),
),
}
}
// hubClient implements HubClient.
type hubClient struct {
getInstances *connect.Client[emptypb.Empty, v1.GetInstancesResponse]
}
// GetInstances calls v1.Hub.GetInstances.
func (c *hubClient) GetInstances(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetInstancesResponse], error) {
return c.getInstances.CallUnary(ctx, req)
}
// HubHandler is an implementation of the v1.Hub service.
type HubHandler interface {
// GetInstances returns a list of all instances
GetInstances(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetInstancesResponse], error)
}
// NewHubHandler builds an HTTP handler from the service implementation. It returns the path on
// which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewHubHandler(svc HubHandler, opts ...connect.HandlerOption) (string, http.Handler) {
hubGetInstancesHandler := connect.NewUnaryHandler(
HubGetInstancesProcedure,
svc.GetInstances,
connect.WithSchema(hubGetInstancesMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
return "/v1.Hub/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case HubGetInstancesProcedure:
hubGetInstancesHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedHubHandler returns CodeUnimplemented from all methods.
type UnimplementedHubHandler struct{}
func (UnimplementedHubHandler) GetInstances(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetInstancesResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Hub.GetInstances is not implemented"))
}
+31
View File
@@ -69,6 +69,8 @@ const (
BackrestCancelProcedure = "/v1.Backrest/Cancel"
// BackrestGetLogsProcedure is the fully-qualified name of the Backrest's GetLogs RPC.
BackrestGetLogsProcedure = "/v1.Backrest/GetLogs"
// BackrestRunCommandProcedure is the fully-qualified name of the Backrest's RunCommand RPC.
BackrestRunCommandProcedure = "/v1.Backrest/RunCommand"
// BackrestGetDownloadURLProcedure is the fully-qualified name of the Backrest's GetDownloadURL RPC.
BackrestGetDownloadURLProcedure = "/v1.Backrest/GetDownloadURL"
// BackrestClearHistoryProcedure is the fully-qualified name of the Backrest's ClearHistory RPC.
@@ -97,6 +99,7 @@ var (
backrestStatsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Stats")
backrestCancelMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Cancel")
backrestGetLogsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetLogs")
backrestRunCommandMethodDescriptor = backrestServiceDescriptor.Methods().ByName("RunCommand")
backrestGetDownloadURLMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetDownloadURL")
backrestClearHistoryMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ClearHistory")
backrestPathAutocompleteMethodDescriptor = backrestServiceDescriptor.Methods().ByName("PathAutocomplete")
@@ -129,6 +132,8 @@ type BackrestClient interface {
Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error)
// GetLogs returns the keyed large data for the given operation.
GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error)
// RunCommand executes a generic restic command on the repository.
RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.ServerStreamForClient[types.BytesValue], error)
// GetDownloadURL returns a signed download URL given a forget operation ID.
GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error)
// Clears the history of operations
@@ -243,6 +248,12 @@ func NewBackrestClient(httpClient connect.HTTPClient, baseURL string, opts ...co
connect.WithSchema(backrestGetLogsMethodDescriptor),
connect.WithClientOptions(opts...),
),
runCommand: connect.NewClient[v1.RunCommandRequest, types.BytesValue](
httpClient,
baseURL+BackrestRunCommandProcedure,
connect.WithSchema(backrestRunCommandMethodDescriptor),
connect.WithClientOptions(opts...),
),
getDownloadURL: connect.NewClient[types.Int64Value, types.StringValue](
httpClient,
baseURL+BackrestGetDownloadURLProcedure,
@@ -282,6 +293,7 @@ type backrestClient struct {
stats *connect.Client[types.StringValue, emptypb.Empty]
cancel *connect.Client[types.Int64Value, emptypb.Empty]
getLogs *connect.Client[v1.LogDataRequest, types.BytesValue]
runCommand *connect.Client[v1.RunCommandRequest, types.BytesValue]
getDownloadURL *connect.Client[types.Int64Value, types.StringValue]
clearHistory *connect.Client[v1.ClearHistoryRequest, emptypb.Empty]
pathAutocomplete *connect.Client[types.StringValue, types.StringList]
@@ -367,6 +379,11 @@ func (c *backrestClient) GetLogs(ctx context.Context, req *connect.Request[v1.Lo
return c.getLogs.CallUnary(ctx, req)
}
// RunCommand calls v1.Backrest.RunCommand.
func (c *backrestClient) RunCommand(ctx context.Context, req *connect.Request[v1.RunCommandRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) {
return c.runCommand.CallServerStream(ctx, req)
}
// GetDownloadURL calls v1.Backrest.GetDownloadURL.
func (c *backrestClient) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) {
return c.getDownloadURL.CallUnary(ctx, req)
@@ -409,6 +426,8 @@ type BackrestHandler interface {
Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error)
// GetLogs returns the keyed large data for the given operation.
GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error)
// RunCommand executes a generic restic command on the repository.
RunCommand(context.Context, *connect.Request[v1.RunCommandRequest], *connect.ServerStream[types.BytesValue]) error
// GetDownloadURL returns a signed download URL given a forget operation ID.
GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error)
// Clears the history of operations
@@ -519,6 +538,12 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str
connect.WithSchema(backrestGetLogsMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
backrestRunCommandHandler := connect.NewServerStreamHandler(
BackrestRunCommandProcedure,
svc.RunCommand,
connect.WithSchema(backrestRunCommandMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
backrestGetDownloadURLHandler := connect.NewUnaryHandler(
BackrestGetDownloadURLProcedure,
svc.GetDownloadURL,
@@ -571,6 +596,8 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str
backrestCancelHandler.ServeHTTP(w, r)
case BackrestGetLogsProcedure:
backrestGetLogsHandler.ServeHTTP(w, r)
case BackrestRunCommandProcedure:
backrestRunCommandHandler.ServeHTTP(w, r)
case BackrestGetDownloadURLProcedure:
backrestGetDownloadURLHandler.ServeHTTP(w, r)
case BackrestClearHistoryProcedure:
@@ -650,6 +677,10 @@ func (UnimplementedBackrestHandler) GetLogs(context.Context, *connect.Request[v1
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetLogs is not implemented"))
}
func (UnimplementedBackrestHandler) RunCommand(context.Context, *connect.Request[v1.RunCommandRequest], *connect.ServerStream[types.BytesValue]) error {
return connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.RunCommand is not implemented"))
}
func (UnimplementedBackrestHandler) GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetDownloadURL is not implemented"))
}
+41
View File
@@ -411,6 +411,47 @@ func (s *BackrestHandler) Stats(ctx context.Context, req *connect.Request[types.
}
func (s *BackrestHandler) RunCommand(ctx context.Context, req *connect.Request[v1.RunCommandRequest], resp *connect.ServerStream[types.BytesValue]) error {
repo, err := s.orchestrator.GetRepoOrchestrator(req.Msg.RepoId)
if err != nil {
return fmt.Errorf("failed to get repo %q: %w", req.Msg.RepoId, err)
}
ctx, cancel := context.WithCancel(ctx)
errChan := make(chan error, 1)
var outputBuf []byte
go func() {
if err := repo.RunCommand(ctx, req.Msg.Command, func(output []byte) {
outputBuf = append(outputBuf, output...)
}); err != nil {
errChan <- err
}
cancel()
}()
for {
select {
case err := <-errChan:
if err := resp.Send(&types.BytesValue{Value: outputBuf}); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
return err
case <-ctx.Done():
if err := resp.Send(&types.BytesValue{Value: outputBuf}); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
return nil
case <-time.After(100 * time.Millisecond):
if err := resp.Send(&types.BytesValue{Value: outputBuf}); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
outputBuf = outputBuf[:0] // clear the buffer and continue
}
}
}
func (s *BackrestHandler) Cancel(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) {
if err := s.orchestrator.CancelOperation(req.Msg.Value, v1.OperationStatus_STATUS_USER_CANCELLED); err != nil {
return nil, err
+4 -4
View File
@@ -11,12 +11,12 @@ import (
// pipeResticLogsToWriter sets the restic logger to write to the provided writer.
// returns a new context with the logger set and a function to flush the logs.
func forwardResticLogs(ctx context.Context) (context.Context, func()) {
writer := logging.WriterFromContext(ctx)
if writer == nil {
return ctx, func() {}
}
capture := ioutil.NewOutputCapturer(64 * 1024)
return restic.ContextWithLogger(ctx, capture), func() {
writer := logging.WriterFromContext(ctx)
if writer == nil {
return
}
_, _ = writer.Write(capture.Bytes())
}
}
+31 -1
View File
@@ -18,7 +18,8 @@ import (
"go.uber.org/zap"
)
// RepoOrchestrator is responsible for managing a single repo.
// RepoOrchestrator implements higher level repository operations on top of
// the restic package. It can be thought of as a controller for a repo.
type RepoOrchestrator struct {
mu sync.Mutex
@@ -326,6 +327,8 @@ func (r *RepoOrchestrator) Stats(ctx context.Context) (*v1.RepoStats, error) {
func (r *RepoOrchestrator) AddTags(ctx context.Context, snapshotIDs []string, tags []string) error {
r.mu.Lock()
defer r.mu.Unlock()
ctx, flush := forwardResticLogs(ctx)
defer flush()
for idx, snapshotIDs := range chunkBy(snapshotIDs, 20) {
r.l.Debug("adding tag to snapshots", zap.Strings("snapshots", snapshotIDs), zap.Strings("tags", tags))
@@ -337,6 +340,24 @@ func (r *RepoOrchestrator) AddTags(ctx context.Context, snapshotIDs []string, ta
return nil
}
// RunCommand runs a command in the repo's environment. Output is buffered and sent to the onProgress callback in batches.
func (r *RepoOrchestrator) RunCommand(ctx context.Context, command string, onProgress func([]byte)) error {
r.mu.Lock()
defer r.mu.Unlock()
ctx, flush := forwardResticLogs(ctx)
defer flush()
r.l.Debug("running command", zap.String("command", command))
args, err := shlex.Split(command)
if err != nil {
return fmt.Errorf("parse command: %w", err)
}
ctx = restic.ContextWithLogger(ctx, &callbackWriter{callback: onProgress})
return r.repo.GenericCommand(ctx, args)
}
func (r *RepoOrchestrator) Config() *v1.Repo {
if r == nil {
return nil
@@ -356,3 +377,12 @@ func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {
}
return append(chunks, items)
}
type callbackWriter struct {
callback func([]byte)
}
func (w *callbackWriter) Write(p []byte) (n int, err error) {
w.callback(p)
return len(p), nil
}
+16 -7
View File
@@ -124,10 +124,10 @@ func (r *Repo) Backup(ctx context.Context, paths []string, progressCallback func
args = append(args, paths...)
cmd := r.commandWithContext(ctx, args, opts...)
fullOutput := ioutil.NewOutputCapturer(outputBufferLimit)
outputForErr := ioutil.NewOutputCapturer(outputBufferLimit)
buf := buffer.New(32 * 1024) // 32KB IO buffer for the realtime event parsing
reader, writer := nio.Pipe(buf)
r.pipeCmdOutputToWriter(cmd, fullOutput, writer)
r.pipeCmdOutputToWriter(cmd, outputForErr, writer)
var readErr error
var summary *BackupProgressEntry
@@ -158,7 +158,7 @@ func (r *Repo) Backup(ctx context.Context, paths []string, progressCallback func
}
}
}
return summary, newCmdErrorPreformatted(ctx, cmd, string(fullOutput.Bytes()), errors.Join(cmdErr, readErr))
return summary, newCmdErrorPreformatted(ctx, cmd, string(outputForErr.Bytes()), errors.Join(cmdErr, readErr))
}
return summary, nil
}
@@ -239,9 +239,10 @@ func (r *Repo) Prune(ctx context.Context, pruneOutput io.Writer, opts ...Generic
func (r *Repo) Restore(ctx context.Context, snapshot string, callback func(*RestoreProgressEntry), opts ...GenericOption) (*RestoreProgressEntry, error) {
cmd := r.commandWithContext(ctx, []string{"restore", "--json", snapshot}, opts...)
capture := ioutil.NewOutputCapturer(outputBufferLimit) // for error reporting.
reader, writer := io.Pipe()
r.pipeCmdOutputToWriter(cmd, writer, capture)
outputForErr := ioutil.NewOutputCapturer(outputBufferLimit) // for error reporting.
buf := buffer.New(32 * 1024) // 32KB IO buffer for the realtime event parsing
reader, writer := nio.Pipe(buf)
r.pipeCmdOutputToWriter(cmd, writer, outputForErr)
var readErr error
var summary *RestoreProgressEntry
@@ -272,7 +273,7 @@ func (r *Repo) Restore(ctx context.Context, snapshot string, callback func(*Rest
}
}
return summary, newCmdErrorPreformatted(ctx, cmd, string(capture.Bytes()), errors.Join(cmdErr, readErr))
return summary, newCmdErrorPreformatted(ctx, cmd, string(outputForErr.Bytes()), errors.Join(cmdErr, readErr))
}
return summary, nil
}
@@ -341,6 +342,14 @@ func (r *Repo) AddTags(ctx context.Context, snapshotIDs []string, tags []string,
return nil
}
func (r *Repo) GenericCommand(ctx context.Context, args []string, opts ...GenericOption) error {
cmd := r.commandWithContext(ctx, args, opts...)
if err := cmd.Run(); err != nil {
return err
}
return nil
}
type RetentionPolicy struct {
KeepLastN int // keep the last n snapshots.
KeepHourly int // keep the last n hourly snapshots.
+8
View File
@@ -53,6 +53,9 @@ service Backrest {
// GetLogs returns the keyed large data for the given operation.
rpc GetLogs(LogDataRequest) returns (types.BytesValue) {}
// RunCommand executes a generic restic command on the repository.
rpc RunCommand(RunCommandRequest) returns (stream types.BytesValue) {}
// GetDownloadURL returns a signed download URL given a forget operation ID.
rpc GetDownloadURL(types.Int64Value) returns (types.StringValue) {}
@@ -125,3 +128,8 @@ message LsEntry {
string atime = 9;
string ctime = 10;
}
message RunCommandRequest {
string repo_id = 1;
string command = 2;
}
+82
View File
@@ -6,6 +6,88 @@
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
/**
* @generated from message v1.HubConfig
*/
export class HubConfig extends Message<HubConfig> {
/**
* @generated from field: repeated v1.HubConfig.InstanceInfo instances = 1;
*/
instances: HubConfig_InstanceInfo[] = [];
constructor(data?: PartialMessage<HubConfig>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "v1.HubConfig";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "instances", kind: "message", T: HubConfig_InstanceInfo, repeated: true },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): HubConfig {
return new HubConfig().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): HubConfig {
return new HubConfig().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): HubConfig {
return new HubConfig().fromJsonString(jsonString, options);
}
static equals(a: HubConfig | PlainMessage<HubConfig> | undefined, b: HubConfig | PlainMessage<HubConfig> | undefined): boolean {
return proto3.util.equals(HubConfig, a, b);
}
}
/**
* @generated from message v1.HubConfig.InstanceInfo
*/
export class HubConfig_InstanceInfo extends Message<HubConfig_InstanceInfo> {
/**
* @generated from field: string id = 1;
*/
id = "";
/**
* secret used to authenticate with the hub.
*
* @generated from field: string secret = 2;
*/
secret = "";
constructor(data?: PartialMessage<HubConfig_InstanceInfo>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "v1.HubConfig.InstanceInfo";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "secret", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): HubConfig_InstanceInfo {
return new HubConfig_InstanceInfo().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): HubConfig_InstanceInfo {
return new HubConfig_InstanceInfo().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): HubConfig_InstanceInfo {
return new HubConfig_InstanceInfo().fromJsonString(jsonString, options);
}
static equals(a: HubConfig_InstanceInfo | PlainMessage<HubConfig_InstanceInfo> | undefined, b: HubConfig_InstanceInfo | PlainMessage<HubConfig_InstanceInfo> | undefined): boolean {
return proto3.util.equals(HubConfig_InstanceInfo, a, b);
}
}
/**
* Config is the top level config object for restic UI.
*
+28
View File
@@ -0,0 +1,28 @@
// @generated by protoc-gen-connect-es v1.3.0 with parameter "target=ts"
// @generated from file v1/hub.proto (package v1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { Empty, MethodKind } from "@bufbuild/protobuf";
import { GetInstancesResponse } from "./hub_pb.js";
/**
* @generated from service v1.Hub
*/
export const Hub = {
typeName: "v1.Hub",
methods: {
/**
* GetInstances returns a list of all instances
*
* @generated from rpc v1.Hub.GetInstances
*/
getInstances: {
name: "GetInstances",
I: Empty,
O: GetInstancesResponse,
kind: MethodKind.Unary,
},
}
} as const;
+82
View File
@@ -0,0 +1,82 @@
// @generated by protoc-gen-es v1.6.0 with parameter "target=ts"
// @generated from file v1/hub.proto (package v1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
/**
* @generated from message v1.GetInstancesResponse
*/
export class GetInstancesResponse extends Message<GetInstancesResponse> {
/**
* @generated from field: repeated v1.Instance instances = 1;
*/
instances: Instance[] = [];
constructor(data?: PartialMessage<GetInstancesResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "v1.GetInstancesResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "instances", kind: "message", T: Instance, repeated: true },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetInstancesResponse {
return new GetInstancesResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetInstancesResponse {
return new GetInstancesResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetInstancesResponse {
return new GetInstancesResponse().fromJsonString(jsonString, options);
}
static equals(a: GetInstancesResponse | PlainMessage<GetInstancesResponse> | undefined, b: GetInstancesResponse | PlainMessage<GetInstancesResponse> | undefined): boolean {
return proto3.util.equals(GetInstancesResponse, a, b);
}
}
/**
* @generated from message v1.Instance
*/
export class Instance extends Message<Instance> {
/**
* @generated from field: string id = 1;
*/
id = "";
constructor(data?: PartialMessage<Instance>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "v1.Instance";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Instance {
return new Instance().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Instance {
return new Instance().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Instance {
return new Instance().fromJsonString(jsonString, options);
}
static equals(a: Instance | PlainMessage<Instance> | undefined, b: Instance | PlainMessage<Instance> | undefined): boolean {
return proto3.util.equals(Instance, a, b);
}
}
+12 -1
View File
@@ -6,7 +6,7 @@
import { Empty, MethodKind } from "@bufbuild/protobuf";
import { Config, Repo } from "./config_pb.js";
import { OperationEvent, OperationList } from "./operations_pb.js";
import { ClearHistoryRequest, ForgetRequest, GetOperationsRequest, ListSnapshotFilesRequest, ListSnapshotFilesResponse, ListSnapshotsRequest, LogDataRequest, RestoreSnapshotRequest } from "./service_pb.js";
import { ClearHistoryRequest, ForgetRequest, GetOperationsRequest, ListSnapshotFilesRequest, ListSnapshotFilesResponse, ListSnapshotsRequest, LogDataRequest, RestoreSnapshotRequest, RunCommandRequest } from "./service_pb.js";
import { ResticSnapshotList } from "./restic_pb.js";
import { BytesValue, Int64Value, StringList, StringValue } from "../types/value_pb.js";
@@ -178,6 +178,17 @@ export const Backrest = {
O: BytesValue,
kind: MethodKind.Unary,
},
/**
* RunCommand executes a generic restic command on the repository.
*
* @generated from rpc v1.Backrest.RunCommand
*/
runCommand: {
name: "RunCommand",
I: RunCommandRequest,
O: BytesValue,
kind: MethodKind.ServerStreaming,
},
/**
* GetDownloadURL returns a signed download URL given a forget operation ID.
*
+43
View File
@@ -503,3 +503,46 @@ export class LsEntry extends Message<LsEntry> {
}
}
/**
* @generated from message v1.RunCommandRequest
*/
export class RunCommandRequest extends Message<RunCommandRequest> {
/**
* @generated from field: string repo_id = 1;
*/
repoId = "";
/**
* @generated from field: string command = 2;
*/
command = "";
constructor(data?: PartialMessage<RunCommandRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "v1.RunCommandRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "repo_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "command", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RunCommandRequest {
return new RunCommandRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RunCommandRequest {
return new RunCommandRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RunCommandRequest {
return new RunCommandRequest().fromJsonString(jsonString, options);
}
static equals(a: RunCommandRequest | PlainMessage<RunCommandRequest> | undefined, b: RunCommandRequest | PlainMessage<RunCommandRequest> | undefined): boolean {
return proto3.util.equals(RunCommandRequest, a, b);
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}
+11 -1
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Plan } from "../../gen/ts/v1/config_pb";
import { Flex, Tabs, Tooltip, Typography } from "antd";
import { Button, Flex, Tabs, Tooltip, Typography } from "antd";
import { useAlertApi } from "../components/Alerts";
import { OperationList } from "../components/OperationList";
import { OperationTree } from "../components/OperationTree";
@@ -9,9 +9,11 @@ import { backrestService } from "../api";
import { GetOperationsRequest } from "../../gen/ts/v1/service_pb";
import { SpinButton } from "../components/SpinButton";
import { shouldHideStatus } from "../state/oplog";
import { useShowModal } from "../components/ModalManager";
export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
const alertsApi = useAlertApi()!;
const showModal = useShowModal();
const handleBackupNow = async () => {
try {
@@ -77,6 +79,14 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
Clear Error History
</SpinButton>
</Tooltip>
<Tooltip title="Open a restic shell to run commands on the repository.">
<Button type="default" onClick={async () => {
const { RunCommandModal } = await import("./RunCommandModal");
showModal(<RunCommandModal repoId={plan.repo!} />);
}}>
Run Command
</Button>
</Tooltip>
</Flex>
<Tabs
defaultActiveKey="1"
+11 -1
View File
@@ -1,6 +1,6 @@
import React, { useContext, useEffect, useState } from "react";
import { Repo } from "../../gen/ts/v1/config_pb";
import { Col, Empty, Flex, Row, Spin, TabsProps, Tabs, Tooltip, Typography } from "antd";
import { Col, Empty, Flex, Row, Spin, TabsProps, Tabs, Tooltip, Typography, Button } from "antd";
import { OperationList } from "../components/OperationList";
import { OperationTree } from "../components/OperationTree";
import { MAX_OPERATION_HISTORY, STATS_OPERATION_HISTORY } from "../constants";
@@ -14,10 +14,12 @@ import { SpinButton } from "../components/SpinButton";
import { useConfig } from "../components/ConfigProvider";
import { useAlertApi } from "../components/Alerts";
import { LineChart } from "@mui/x-charts";
import { useShowModal } from "../components/ModalManager";
export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
const [config, setConfig] = useConfig();
const showModal = useShowModal();
// Task handlers
const handleIndexNow = async () => {
@@ -99,6 +101,14 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
Compute Stats
</SpinButton>
</Tooltip>
<Tooltip title="Open a restic shell to run commands on the repository.">
<Button type="default" onClick={async () => {
const { RunCommandModal } = await import("./RunCommandModal");
showModal(<RunCommandModal repoId={repo.id!} />);
}}>
Run Command
</Button>
</Tooltip>
</Flex>
<Tabs
defaultActiveKey={items[0].key}
+82
View File
@@ -0,0 +1,82 @@
import { Button, Input, Modal, Space } from "antd";
import React from "react";
import { useShowModal } from "../components/ModalManager";
import { backrestService } from "../api";
import { SpinButton } from "../components/SpinButton";
import { ConnectError } from "@connectrpc/connect";
interface Invocation {
command: string;
output: string;
error: string;
}
export const RunCommandModal = ({ repoId }: { repoId: string }) => {
const showModal = useShowModal();
const [command, setCommand] = React.useState("");
const [running, setRunning] = React.useState(false);
const [invocations, setInvocations] = React.useState<Invocation[]>([]);
const handleCancel = () => {
showModal(null);
}
const doExecute = async () => {
if (running) return;
setRunning(true);
const newInvocation = { command, output: "", error: "" };
setInvocations((invocations) => [newInvocation, ...invocations]);
let segments: string[] = [];
try {
for await (const bytes of backrestService.runCommand({ repoId, command })) {
const output = new TextDecoder("utf-8").decode(bytes.value);
segments.push(output);
setInvocations((invocations) => {
const copy = [...invocations];
copy[0] = {
...copy[0],
output: segments.join(""),
}
return copy;
});
}
} catch (e: any) {
setInvocations((invocations) => {
const copy = [...invocations];
copy[0] = {
...copy[0],
error: (e as Error).message,
}
return copy;
});
} finally {
setRunning(false);
}
}
return <Modal
open={true}
onCancel={handleCancel}
title={"Run Command in repo " + repoId}
width="80vw"
footer={[]}
>
<Space.Compact style={{ width: '100%' }}>
<Input placeholder="Run a restic comamnd e.g. 'help' to print help text" onChange={(e) => setCommand(e.target.value)} onKeyUp={(e) => {
if (e.key === "Enter") {
doExecute();
}
}} />
<SpinButton type="primary" onClickAsync={doExecute}>Execute</SpinButton>
</Space.Compact>
{invocations.map((invocation, i) => <div key={i}>
{invocation.output ? <pre>{invocation.output}</pre> : null}
{invocation.error ? <pre style={{ color: "red" }}>{invocation.error}</pre> : null}
</div>)}
</Modal>
}