From 9ccade5ccd97f4e485d52ad5c675be6b0a4a1049 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 11 Nov 2023 20:43:00 -0800 Subject: [PATCH] feat: repo can be created through UI --- gen/go/types/value.pb.go | 206 +++++++++++++++ gen/go/v1/config.pb.go | 249 ++++++++++++++---- gen/go/v1/service.pb.go | 76 +++--- gen/go/v1/service.pb.gw.go | 86 ++++++ gen/go/v1/service_grpc.pb.go | 46 +++- go.mod | 3 +- go.sum | 2 + internal/api/server.go | 18 ++ internal/config/config.go | 1 - internal/config/validate.go | 69 ++++- proto/types/value.proto | 13 + proto/v1/config.proto | 16 +- proto/v1/service.proto | 8 + webui/gen/ts/types/value.pb.ts | 12 + webui/gen/ts/v1/config.pb.ts | 14 +- webui/gen/ts/v1/service.pb.ts | 4 + webui/src/components/ModalManager.tsx | 38 +++ webui/src/components/URIAutocomplete.tsx | 49 ++++ webui/src/index.tsx | 5 +- webui/src/state/config.ts | 21 +- webui/src/views/AddPlanModel.tsx | 102 ++++++++ webui/src/views/AddRepoModel.tsx | 316 +++++++++++++++++++++++ webui/src/views/App.tsx | 32 ++- 23 files changed, 1270 insertions(+), 116 deletions(-) create mode 100644 gen/go/types/value.pb.go create mode 100644 proto/types/value.proto create mode 100644 webui/gen/ts/types/value.pb.ts create mode 100644 webui/src/components/ModalManager.tsx create mode 100644 webui/src/components/URIAutocomplete.tsx diff --git a/gen/go/types/value.pb.go b/gen/go/types/value.pb.go new file mode 100644 index 00000000..529fba08 --- /dev/null +++ b/gen/go/types/value.pb.go @@ -0,0 +1,206 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: types/value.proto + +package types + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + 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 StringValue struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *StringValue) Reset() { + *x = StringValue{} + if protoimpl.UnsafeEnabled { + mi := &file_types_value_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StringValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringValue) ProtoMessage() {} + +func (x *StringValue) ProtoReflect() protoreflect.Message { + mi := &file_types_value_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 StringValue.ProtoReflect.Descriptor instead. +func (*StringValue) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{0} +} + +func (x *StringValue) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type StringList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` +} + +func (x *StringList) Reset() { + *x = StringList{} + if protoimpl.UnsafeEnabled { + mi := &file_types_value_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StringList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringList) ProtoMessage() {} + +func (x *StringList) ProtoReflect() protoreflect.Message { + mi := &file_types_value_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 StringList.ProtoReflect.Descriptor instead. +func (*StringList) Descriptor() ([]byte, []int) { + return file_types_value_proto_rawDescGZIP(), []int{1} +} + +func (x *StringList) GetValues() []string { + if x != nil { + return x.Values + } + return nil +} + +var File_types_value_proto protoreflect.FileDescriptor + +var file_types_value_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x22, 0x23, 0x0a, 0x0b, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 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, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, + 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, + 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_types_value_proto_rawDescOnce sync.Once + file_types_value_proto_rawDescData = file_types_value_proto_rawDesc +) + +func file_types_value_proto_rawDescGZIP() []byte { + file_types_value_proto_rawDescOnce.Do(func() { + file_types_value_proto_rawDescData = protoimpl.X.CompressGZIP(file_types_value_proto_rawDescData) + }) + return file_types_value_proto_rawDescData +} + +var file_types_value_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_types_value_proto_goTypes = []interface{}{ + (*StringValue)(nil), // 0: types.StringValue + (*StringList)(nil), // 1: types.StringList +} +var file_types_value_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_types_value_proto_init() } +func file_types_value_proto_init() { + if File_types_value_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_types_value_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StringValue); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_types_value_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StringList); 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_types_value_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_types_value_proto_goTypes, + DependencyIndexes: file_types_value_proto_depIdxs, + MessageInfos: file_types_value_proto_msgTypes, + }.Build() + File_types_value_proto = out.File + file_types_value_proto_rawDesc = nil + file_types_value_proto_goTypes = nil + file_types_value_proto_depIdxs = nil +} diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index 7e816218..e937f02e 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -26,7 +26,6 @@ type Config struct { unknownFields protoimpl.UnknownFields Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` - LogDir string `protobuf:"bytes,2,opt,name=log_dir,proto3" json:"log_dir,omitempty"` Repos []*Repo `protobuf:"bytes,3,rep,name=repos,proto3" json:"repos,omitempty"` Plans []*Plan `protobuf:"bytes,4,rep,name=plans,proto3" json:"plans,omitempty"` } @@ -70,13 +69,6 @@ func (x *Config) GetVersion() int32 { return 0 } -func (x *Config) GetLogDir() string { - if x != nil { - return x.LogDir - } - return "" -} - func (x *Config) GetRepos() []*Repo { if x != nil { return x.Repos @@ -230,10 +222,12 @@ type Plan struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // unique but human readable ID for this plan. - Repo string `protobuf:"bytes,2,opt,name=repo,proto3" json:"repo,omitempty"` // ID of the repo to use. - Paths []string `protobuf:"bytes,4,rep,name=paths,proto3" json:"paths,omitempty"` // paths to include in the backup. - Excludes []string `protobuf:"bytes,5,rep,name=excludes,proto3" json:"excludes,omitempty"` // glob patterns to exclude. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // unique but human readable ID for this plan. + Repo string `protobuf:"bytes,2,opt,name=repo,proto3" json:"repo,omitempty"` // ID of the repo to use. + Paths []string `protobuf:"bytes,4,rep,name=paths,proto3" json:"paths,omitempty"` // paths to include in the backup. + Excludes []string `protobuf:"bytes,5,rep,name=excludes,proto3" json:"excludes,omitempty"` // glob patterns to exclude. + Cron string `protobuf:"bytes,6,opt,name=cron,proto3" json:"cron,omitempty"` + Retention *RetentionPolicy `protobuf:"bytes,7,opt,name=retention,proto3" json:"retention,omitempty"` } func (x *Plan) Reset() { @@ -296,38 +290,179 @@ func (x *Plan) GetExcludes() []string { return nil } +func (x *Plan) GetCron() string { + if x != nil { + return x.Cron + } + return "" +} + +func (x *Plan) GetRetention() *RetentionPolicy { + if x != nil { + return x.Retention + } + return nil +} + +type RetentionPolicy struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // max_unused_limit is used to decide when forget should be run. + MaxUnusedLimit string `protobuf:"bytes,1,opt,name=max_unused_limit,json=maxUnusedLimit,proto3" json:"max_unused_limit,omitempty"` // e.g. a percentage i.e. 25% or a number of megabytes. + KeepLastN int32 `protobuf:"varint,2,opt,name=keep_last_n,json=keepLastN,proto3" json:"keep_last_n,omitempty"` // keep the last n snapshots. + KeepHourly int32 `protobuf:"varint,3,opt,name=keep_hourly,json=keepHourly,proto3" json:"keep_hourly,omitempty"` // keep the last n hourly snapshots. + KeepDaily int32 `protobuf:"varint,4,opt,name=keep_daily,json=keepDaily,proto3" json:"keep_daily,omitempty"` // keep the last n daily snapshots. + KeepWeekly int32 `protobuf:"varint,5,opt,name=keep_weekly,json=keepWeekly,proto3" json:"keep_weekly,omitempty"` // keep the last n weekly snapshots. + KeepMonthly int32 `protobuf:"varint,6,opt,name=keep_monthly,json=keepMonthly,proto3" json:"keep_monthly,omitempty"` // keep the last n monthly snapshots. + KeepYearly int32 `protobuf:"varint,7,opt,name=keep_yearly,json=keepYearly,proto3" json:"keep_yearly,omitempty"` // keep the last n yearly snapshots. + KeepWithinDuration string `protobuf:"bytes,8,opt,name=keep_within_duration,json=keepWithinDuration,proto3" json:"keep_within_duration,omitempty"` // keep snapshots within a duration e.g. 1y2m3d4h5m6s +} + +func (x *RetentionPolicy) Reset() { + *x = RetentionPolicy{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RetentionPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetentionPolicy) ProtoMessage() {} + +func (x *RetentionPolicy) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[4] + 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 RetentionPolicy.ProtoReflect.Descriptor instead. +func (*RetentionPolicy) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{4} +} + +func (x *RetentionPolicy) GetMaxUnusedLimit() string { + if x != nil { + return x.MaxUnusedLimit + } + return "" +} + +func (x *RetentionPolicy) GetKeepLastN() int32 { + if x != nil { + return x.KeepLastN + } + return 0 +} + +func (x *RetentionPolicy) GetKeepHourly() int32 { + if x != nil { + return x.KeepHourly + } + return 0 +} + +func (x *RetentionPolicy) GetKeepDaily() int32 { + if x != nil { + return x.KeepDaily + } + return 0 +} + +func (x *RetentionPolicy) GetKeepWeekly() int32 { + if x != nil { + return x.KeepWeekly + } + return 0 +} + +func (x *RetentionPolicy) GetKeepMonthly() int32 { + if x != nil { + return x.KeepMonthly + } + return 0 +} + +func (x *RetentionPolicy) GetKeepYearly() int32 { + if x != nil { + return x.KeepYearly + } + return 0 +} + +func (x *RetentionPolicy) GetKeepWithinDuration() string { + if x != nil { + return x.KeepWithinDuration + } + return "" +} + var File_v1_config_proto protoreflect.FileDescriptor var file_v1_config_proto_rawDesc = []byte{ 0x0a, 0x0f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x02, 0x76, 0x31, 0x22, 0x7c, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x6f, 0x12, 0x02, 0x76, 0x31, 0x22, 0x62, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, - 0x5f, 0x64, 0x69, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x5f, - 0x64, 0x69, 0x72, 0x12, 0x1e, 0x0a, 0x05, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x52, 0x05, 0x72, 0x65, - 0x70, 0x6f, 0x73, 0x12, 0x1e, 0x0a, 0x05, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x05, 0x70, 0x6c, - 0x61, 0x6e, 0x73, 0x22, 0x36, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x6c, 0x0a, 0x04, 0x52, - 0x65, 0x70, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, - 0x65, 0x6e, 0x76, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x22, 0x5c, 0x0a, 0x04, 0x50, 0x6c, 0x61, - 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x65, - 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x65, - 0x78, 0x63, 0x6c, 0x75, 0x64, 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, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x05, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x52, 0x05, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x12, 0x1e, 0x0a, 0x05, 0x70, 0x6c, 0x61, + 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6c, + 0x61, 0x6e, 0x52, 0x05, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x22, 0x36, 0x0a, 0x04, 0x55, 0x73, 0x65, + 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x22, 0x6c, 0x0a, 0x04, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x1a, 0x0a, 0x08, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, + 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x22, + 0xa3, 0x01, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x6f, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x14, 0x0a, 0x05, + 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, + 0x68, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x12, 0x12, + 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x72, + 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x09, 0x72, 0x65, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb2, 0x02, 0x0a, 0x0f, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x61, 0x78, + 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x4c, 0x69, + 0x6d, 0x69, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x6c, 0x61, 0x73, 0x74, + 0x5f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x4c, 0x61, + 0x73, 0x74, 0x4e, 0x12, 0x1f, 0x0a, 0x0b, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x68, 0x6f, 0x75, 0x72, + 0x6c, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6b, 0x65, 0x65, 0x70, 0x48, 0x6f, + 0x75, 0x72, 0x6c, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x64, 0x61, 0x69, + 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x44, 0x61, + 0x69, 0x6c, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x77, 0x65, 0x65, 0x6b, + 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6b, 0x65, 0x65, 0x70, 0x57, 0x65, + 0x65, 0x6b, 0x6c, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x6d, 0x6f, 0x6e, + 0x74, 0x68, 0x6c, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x6b, 0x65, 0x65, 0x70, + 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6b, 0x65, 0x65, 0x70, 0x5f, + 0x79, 0x65, 0x61, 0x72, 0x6c, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6b, 0x65, + 0x65, 0x70, 0x59, 0x65, 0x61, 0x72, 0x6c, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x6b, 0x65, 0x65, 0x70, + 0x5f, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6b, 0x65, 0x65, 0x70, 0x57, 0x69, 0x74, 0x68, + 0x69, 0x6e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, + 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, + 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -342,21 +477,23 @@ func file_v1_config_proto_rawDescGZIP() []byte { return file_v1_config_proto_rawDescData } -var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_v1_config_proto_goTypes = []interface{}{ - (*Config)(nil), // 0: v1.Config - (*User)(nil), // 1: v1.User - (*Repo)(nil), // 2: v1.Repo - (*Plan)(nil), // 3: v1.Plan + (*Config)(nil), // 0: v1.Config + (*User)(nil), // 1: v1.User + (*Repo)(nil), // 2: v1.Repo + (*Plan)(nil), // 3: v1.Plan + (*RetentionPolicy)(nil), // 4: v1.RetentionPolicy } var file_v1_config_proto_depIdxs = []int32{ 2, // 0: v1.Config.repos:type_name -> v1.Repo 3, // 1: v1.Config.plans:type_name -> v1.Plan - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 4, // 2: v1.Plan.retention:type_name -> v1.RetentionPolicy + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_v1_config_proto_init() } @@ -413,6 +550,18 @@ func file_v1_config_proto_init() { return nil } } + file_v1_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RetentionPolicy); 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{ @@ -420,7 +569,7 @@ func file_v1_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_config_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index 1cd4d17d..74c1a6fa 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -7,6 +7,7 @@ package v1 import ( + types "github.com/garethgeorge/resticui/gen/go/types" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -27,50 +28,61 @@ var file_v1_service_proto_rawDesc = []byte{ 0x0a, 0x10, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 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, 0x65, 0x76, 0x65, 0x6e, - 0x74, 0x73, 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, 0x32, 0x8e, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, - 0x12, 0x43, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x3b, 0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, - 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x44, - 0x0a, 0x09, 0x47, 0x65, 0x74, 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, 0x09, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x12, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, - 0x74, 0x73, 0x30, 0x01, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x74, 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, 0x32, 0xeb, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, + 0x63, 0x55, 0x49, 0x12, 0x43, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, + 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, 0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, + 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, + 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, + 0x6f, 0x12, 0x44, 0x0a, 0x09, 0x47, 0x65, 0x74, 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, 0x09, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x65, + 0x76, 0x65, 0x6e, 0x74, 0x73, 0x30, 0x01, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, + 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, + 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, + 0x73, 0x74, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, + 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2f, + 0x70, 0x61, 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var file_v1_service_proto_goTypes = []interface{}{ - (*emptypb.Empty)(nil), // 0: google.protobuf.Empty - (*Config)(nil), // 1: v1.Config - (*Repo)(nil), // 2: v1.Repo - (*Event)(nil), // 3: v1.Event + (*emptypb.Empty)(nil), // 0: google.protobuf.Empty + (*Config)(nil), // 1: v1.Config + (*Repo)(nil), // 2: v1.Repo + (*types.StringValue)(nil), // 3: types.StringValue + (*Event)(nil), // 4: v1.Event + (*types.StringList)(nil), // 5: types.StringList } var file_v1_service_proto_depIdxs = []int32{ 0, // 0: v1.ResticUI.GetConfig:input_type -> google.protobuf.Empty 1, // 1: v1.ResticUI.SetConfig:input_type -> v1.Config 2, // 2: v1.ResticUI.AddRepo:input_type -> v1.Repo 0, // 3: v1.ResticUI.GetEvents:input_type -> google.protobuf.Empty - 1, // 4: v1.ResticUI.GetConfig:output_type -> v1.Config - 1, // 5: v1.ResticUI.SetConfig:output_type -> v1.Config - 1, // 6: v1.ResticUI.AddRepo:output_type -> v1.Config - 3, // 7: v1.ResticUI.GetEvents:output_type -> v1.Event - 4, // [4:8] is the sub-list for method output_type - 0, // [0:4] is the sub-list for method input_type + 3, // 4: v1.ResticUI.PathAutocomplete:input_type -> types.StringValue + 1, // 5: v1.ResticUI.GetConfig:output_type -> v1.Config + 1, // 6: v1.ResticUI.SetConfig:output_type -> v1.Config + 1, // 7: v1.ResticUI.AddRepo:output_type -> v1.Config + 4, // 8: v1.ResticUI.GetEvents:output_type -> v1.Event + 5, // 9: v1.ResticUI.PathAutocomplete:output_type -> types.StringList + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name diff --git a/gen/go/v1/service.pb.gw.go b/gen/go/v1/service.pb.gw.go index 14b8857b..8835f202 100644 --- a/gen/go/v1/service.pb.gw.go +++ b/gen/go/v1/service.pb.gw.go @@ -13,6 +13,7 @@ import ( "io" "net/http" + "github.com/garethgeorge/resticui/gen/go/types" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" "google.golang.org/grpc" @@ -135,6 +136,40 @@ func request_ResticUI_GetEvents_0(ctx context.Context, marshaler runtime.Marshal } +func request_ResticUI_PathAutocomplete_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq types.StringValue + var metadata runtime.ServerMetadata + + 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.PathAutocomplete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_ResticUI_PathAutocomplete_0(ctx context.Context, marshaler runtime.Marshaler, server ResticUIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq types.StringValue + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.PathAutocomplete(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterResticUIHandlerServer registers the http handlers for service ResticUI to "mux". // UnaryRPC :call ResticUIServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -223,6 +258,31 @@ func RegisterResticUIHandlerServer(ctx context.Context, mux *runtime.ServeMux, s return }) + mux.Handle("POST", pattern_ResticUI_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/v1.ResticUI/PathAutocomplete", runtime.WithHTTPPathPattern("/v1/autocomplete/path")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ResticUI_PathAutocomplete_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ResticUI_PathAutocomplete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -352,6 +412,28 @@ func RegisterResticUIHandlerClient(ctx context.Context, mux *runtime.ServeMux, c }) + mux.Handle("POST", pattern_ResticUI_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/v1.ResticUI/PathAutocomplete", runtime.WithHTTPPathPattern("/v1/autocomplete/path")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ResticUI_PathAutocomplete_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ResticUI_PathAutocomplete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -363,6 +445,8 @@ var ( pattern_ResticUI_AddRepo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "config", "repo"}, "")) pattern_ResticUI_GetEvents_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "events"}, "")) + + pattern_ResticUI_PathAutocomplete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "autocomplete", "path"}, "")) ) var ( @@ -373,4 +457,6 @@ var ( forward_ResticUI_AddRepo_0 = runtime.ForwardResponseMessage forward_ResticUI_GetEvents_0 = runtime.ForwardResponseStream + + forward_ResticUI_PathAutocomplete_0 = runtime.ForwardResponseMessage ) diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index 068634ab..cc46df01 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -8,6 +8,7 @@ package v1 import ( context "context" + types "github.com/garethgeorge/resticui/gen/go/types" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -20,10 +21,11 @@ import ( const _ = grpc.SupportPackageIsVersion7 const ( - ResticUI_GetConfig_FullMethodName = "/v1.ResticUI/GetConfig" - ResticUI_SetConfig_FullMethodName = "/v1.ResticUI/SetConfig" - ResticUI_AddRepo_FullMethodName = "/v1.ResticUI/AddRepo" - ResticUI_GetEvents_FullMethodName = "/v1.ResticUI/GetEvents" + ResticUI_GetConfig_FullMethodName = "/v1.ResticUI/GetConfig" + ResticUI_SetConfig_FullMethodName = "/v1.ResticUI/SetConfig" + ResticUI_AddRepo_FullMethodName = "/v1.ResticUI/AddRepo" + ResticUI_GetEvents_FullMethodName = "/v1.ResticUI/GetEvents" + ResticUI_PathAutocomplete_FullMethodName = "/v1.ResticUI/PathAutocomplete" ) // ResticUIClient is the client API for ResticUI service. @@ -34,6 +36,7 @@ type ResticUIClient interface { SetConfig(ctx context.Context, in *Config, opts ...grpc.CallOption) (*Config, error) AddRepo(ctx context.Context, in *Repo, opts ...grpc.CallOption) (*Config, error) GetEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ResticUI_GetEventsClient, error) + PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) } type resticUIClient struct { @@ -103,6 +106,15 @@ func (x *resticUIGetEventsClient) Recv() (*Event, error) { return m, nil } +func (c *resticUIClient) PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) { + out := new(types.StringList) + err := c.cc.Invoke(ctx, ResticUI_PathAutocomplete_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ResticUIServer is the server API for ResticUI service. // All implementations must embed UnimplementedResticUIServer // for forward compatibility @@ -111,6 +123,7 @@ type ResticUIServer interface { SetConfig(context.Context, *Config) (*Config, error) AddRepo(context.Context, *Repo) (*Config, error) GetEvents(*emptypb.Empty, ResticUI_GetEventsServer) error + PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) mustEmbedUnimplementedResticUIServer() } @@ -130,6 +143,9 @@ func (UnimplementedResticUIServer) AddRepo(context.Context, *Repo) (*Config, err func (UnimplementedResticUIServer) GetEvents(*emptypb.Empty, ResticUI_GetEventsServer) error { return status.Errorf(codes.Unimplemented, "method GetEvents not implemented") } +func (UnimplementedResticUIServer) PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) { + return nil, status.Errorf(codes.Unimplemented, "method PathAutocomplete not implemented") +} func (UnimplementedResticUIServer) mustEmbedUnimplementedResticUIServer() {} // UnsafeResticUIServer may be embedded to opt out of forward compatibility for this service. @@ -218,6 +234,24 @@ func (x *resticUIGetEventsServer) Send(m *Event) error { return x.ServerStream.SendMsg(m) } +func _ResticUI_PathAutocomplete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(types.StringValue) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResticUIServer).PathAutocomplete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResticUI_PathAutocomplete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResticUIServer).PathAutocomplete(ctx, req.(*types.StringValue)) + } + return interceptor(ctx, in, info, handler) +} + // ResticUI_ServiceDesc is the grpc.ServiceDesc for ResticUI service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -237,6 +271,10 @@ var ResticUI_ServiceDesc = grpc.ServiceDesc{ MethodName: "AddRepo", Handler: _ResticUI_AddRepo_Handler, }, + { + MethodName: "PathAutocomplete", + Handler: _ResticUI_PathAutocomplete_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/go.mod b/go.mod index f2f60fcd..44f4369e 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/garethgeorge/resticui go 1.21.3 require ( + github.com/gitploy-io/cronexpr v0.2.2 github.com/google/renameio v1.0.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 github.com/hashicorp/go-multierror v1.1.1 go.uber.org/zap v1.26.0 - golang.org/x/sync v0.5.0 google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 @@ -22,6 +22,7 @@ require ( github.com/vine-io/vine v1.6.16 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.18.0 // indirect + golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect diff --git a/go.sum b/go.sum index 65d95da8..b8ebe353 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gitploy-io/cronexpr v0.2.2 h1:Au+wK6FqmOLAF7AkW6q4gnrNXTe3rEW97XFZ4chy0xs= +github.com/gitploy-io/cronexpr v0.2.2/go.mod h1:Uep5sbzUSocMZvJ1s0lNI9zi37s5iUI1llkw3vRGK9M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= diff --git a/internal/api/server.go b/internal/api/server.go index 36175334..012bc6f9 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -2,11 +2,14 @@ package api import ( "context" + "errors" "fmt" + "os" "sync" "sync/atomic" "time" + "github.com/garethgeorge/resticui/gen/go/types" v1 "github.com/garethgeorge/resticui/gen/go/v1" "github.com/garethgeorge/resticui/internal/config" "github.com/garethgeorge/resticui/pkg/restic" @@ -103,6 +106,21 @@ func (s *Server) GetEvents(_ *emptypb.Empty, stream v1.ResticUI_GetEventsServer) } } +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) { + return &types.StringList{}, nil + } else if err != nil { + return nil, err + } + + var paths []string + for _, ent := range ents { + paths = append(paths, ent.Name()) + } + + return &types.StringList{Values: paths}, nil +} // PublishEvent publishes an event to all GetEvents streams. It is effectively a multicast. diff --git a/internal/config/config.go b/internal/config/config.go index feec4510..f987bbc6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -22,7 +22,6 @@ type ConfigStore interface { func NewDefaultConfig() *v1.Config { return &v1.Config{ - LogDir: "/var/log/resticui", Repos: []*v1.Repo{}, Plans: []*v1.Plan{}, } diff --git a/internal/config/validate.go b/internal/config/validate.go index c12eb455..da8aab00 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -3,17 +3,15 @@ package config import ( "errors" "fmt" + "regexp" "strings" v1 "github.com/garethgeorge/resticui/gen/go/v1" + "github.com/gitploy-io/cronexpr" "github.com/hashicorp/go-multierror" ) func validateConfig(c *v1.Config) error { - if c.LogDir == "" { - return errors.New("log_dir is required") - } - var err error repos := make(map[string]*v1.Repo) if c.Repos != nil { @@ -27,16 +25,9 @@ func validateConfig(c *v1.Config) error { if c.Plans != nil { for _, plan := range c.Plans { - if plan.Paths == nil || len(plan.Paths) == 0 { - err = multierror.Append(err, fmt.Errorf("plan %s: path is required", plan.GetId())) - } - - if plan.Repo == "" { - err = multierror.Append(err,fmt.Errorf("plan %s: repo is required", plan.GetId())) - } - - if _, ok := repos[plan.Repo]; !ok { - err = multierror.Append(err, fmt.Errorf("plan %s: repo %s not found", plan.GetId(), plan.Repo)) + err := validatePlan(plan, repos); + if err != nil { + err = multierror.Append(err, fmt.Errorf("plan %s: %w", plan.GetId(), err)) } } } @@ -64,5 +55,55 @@ func validateRepo(repo *v1.Repo) error { } } + return err +} + +func validatePlan(plan *v1.Plan, repos map[string]*v1.Repo) error { + var err error + if plan.Paths == nil || len(plan.Paths) == 0 { + err = multierror.Append(err, fmt.Errorf("path is required")) + } + + if plan.Repo == "" { + err = multierror.Append(err,fmt.Errorf("repo is required")) + } + + if _, ok := repos[plan.Repo]; !ok { + err = multierror.Append(err, fmt.Errorf("repo %q not found", plan.Repo)) + } + + + if _, e := cronexpr.Parse(plan.GetCron()); err != nil { + err = multierror.Append(err, fmt.Errorf("invalid cron %q: %w", plan.GetCron(), e)) + } + + if plan.GetRetention() != nil { + if e := validateRetention(plan.GetRetention()); e != nil { + err = multierror.Append(err, fmt.Errorf("invalid retention policy: %w", e)) + } + } + + return err +} + +func validateRetention(policy *v1.RetentionPolicy) error { + var err error + if policy.GetKeepWithinDuration() != "" { + match, e := regexp.Match(`(\d+h)?(\d+m)?(\d+s)?`, []byte(policy.GetKeepWithinDuration())) + if e != nil { + panic(e) // regex error + } + if !match { + err = multierror.Append(err, fmt.Errorf("invalid keep_within_duration %q", policy.GetKeepWithinDuration())) + } + + if policy.GetKeepLastN() != 0 || policy.GetKeepHourly() != 0 || policy.GetKeepDaily() != 0 || policy.GetKeepWeekly() != 0 || policy.GetKeepMonthly() != 0 || policy.GetKeepYearly() != 0 { + err = multierror.Append(err, fmt.Errorf("keep_within_duration cannot be used with other retention settings")) + } + } else { + if policy.GetKeepLastN() == 0 && policy.GetKeepHourly() == 0 && policy.GetKeepDaily() == 0 && policy.GetKeepWeekly() == 0 && policy.GetKeepMonthly() == 0 && policy.GetKeepYearly() == 0 { + err = multierror.Append(err, fmt.Errorf("at least one retention policy must be set")) + } + } return err } \ No newline at end of file diff --git a/proto/types/value.proto b/proto/types/value.proto new file mode 100644 index 00000000..d0fc0f41 --- /dev/null +++ b/proto/types/value.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package types; + +option go_package = "github.com/garethgeorge/resticui/gen/go/types"; + +message StringValue { + string value = 1; +} + +message StringList { + repeated string values = 1; +} \ No newline at end of file diff --git a/proto/v1/config.proto b/proto/v1/config.proto index 01c3ca84..6ae089ee 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -6,7 +6,6 @@ option go_package = "github.com/garethgeorge/resticui/go/proto/v1"; message Config { int32 version = 1; - string log_dir = 2 [json_name="log_dir"]; repeated Repo repos = 3 [json_name="repos"]; repeated Plan plans = 4 [json_name="plans"]; } @@ -29,4 +28,19 @@ message Plan { string repo = 2 [json_name="repo"]; // ID of the repo to use. repeated string paths = 4 [json_name="paths"]; // paths to include in the backup. repeated string excludes = 5 [json_name="excludes"]; // glob patterns to exclude. + string cron = 6 [json_name="cron"]; + RetentionPolicy retention = 7 [json_name="retention"]; +} + +message RetentionPolicy { + // max_unused_limit is used to decide when forget should be run. + string max_unused_limit = 1; // e.g. a percentage i.e. 25% or a number of megabytes. + + int32 keep_last_n = 2; // keep the last n snapshots. + int32 keep_hourly = 3; // keep the last n hourly snapshots. + int32 keep_daily = 4; // keep the last n daily snapshots. + int32 keep_weekly = 5; // keep the last n weekly snapshots. + int32 keep_monthly = 6; // keep the last n monthly snapshots. + int32 keep_yearly = 7; // keep the last n yearly snapshots. + string keep_within_duration = 8; // keep snapshots within a duration e.g. 1y2m3d4h5m6s } diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 434fdacb..baff26a3 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -6,6 +6,7 @@ option go_package = "github.com/garethgeorge/resticui/go/proto/v1"; import "v1/config.proto"; import "v1/events.proto"; +import "types/value.proto"; import "google/protobuf/empty.proto"; import "google/api/annotations.proto"; @@ -35,4 +36,11 @@ service ResticUI { get: "/v1/events" }; } + + rpc PathAutocomplete (types.StringValue) returns (types.StringList) { + option (google.api.http) = { + post: "/v1/autocomplete/path" + body: "*" + }; + } } diff --git a/webui/gen/ts/types/value.pb.ts b/webui/gen/ts/types/value.pb.ts new file mode 100644 index 00000000..97348c10 --- /dev/null +++ b/webui/gen/ts/types/value.pb.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +// @ts-nocheck +/* +* This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY +*/ +export type StringValue = { + value?: string +} + +export type StringList = { + values?: string[] +} \ No newline at end of file diff --git a/webui/gen/ts/v1/config.pb.ts b/webui/gen/ts/v1/config.pb.ts index 290f5c3d..3144820b 100644 --- a/webui/gen/ts/v1/config.pb.ts +++ b/webui/gen/ts/v1/config.pb.ts @@ -5,7 +5,6 @@ */ export type Config = { version?: number - logDir?: string repos?: Repo[] plans?: Plan[] } @@ -28,4 +27,17 @@ export type Plan = { repo?: string paths?: string[] excludes?: string[] + cron?: string + retention?: RetentionPolicy +} + +export type RetentionPolicy = { + maxUnusedLimit?: string + keepLastN?: number + keepHourly?: number + keepDaily?: number + keepWeekly?: number + keepMonthly?: number + keepYearly?: number + keepWithinDuration?: 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 2d1c2da0..8cd208b6 100644 --- a/webui/gen/ts/v1/service.pb.ts +++ b/webui/gen/ts/v1/service.pb.ts @@ -6,6 +6,7 @@ import * as fm from "../fetch.pb" import * as GoogleProtobufEmpty from "../google/protobuf/empty.pb" +import * as TypesValue from "../types/value.pb" import * as V1Config from "./config.pb" import * as V1Events from "./events.pb" export class ResticUI { @@ -21,4 +22,7 @@ export class ResticUI { static GetEvents(req: GoogleProtobufEmpty.Empty, entityNotifier?: fm.NotifyStreamEntityArrival, initReq?: fm.InitReq): Promise { return fm.fetchStreamingRequest(`/v1/events?${fm.renderURLSearchParams(req, [])}`, entityNotifier, {...initReq, method: "GET"}) } + static PathAutocomplete(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise { + return fm.fetchReq(`/v1/autocomplete/path`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) + } } \ No newline at end of file diff --git a/webui/src/components/ModalManager.tsx b/webui/src/components/ModalManager.tsx new file mode 100644 index 00000000..e19da5bd --- /dev/null +++ b/webui/src/components/ModalManager.tsx @@ -0,0 +1,38 @@ +import { Spin } from "antd"; +import React, { useContext } from "react"; +import { createContext } from "react"; + +const ModalContext = createContext<{ + model: React.ReactNode | null; + setModel: (model: React.ReactNode | null) => void; +}>({ + model: null, + setModel: () => { + throw new Error("add a ModelContextProvider to your hierarchy"); + }, +}); + +export const ModalContextProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [modal, setModals] = React.useState([]); + + return ( + + {modal} + {children} + + ); +}; + +export const useShowModal = () => { + const context = useContext(ModalContext); + return context.setModel; +}; diff --git a/webui/src/components/URIAutocomplete.tsx b/webui/src/components/URIAutocomplete.tsx new file mode 100644 index 00000000..c2d0cbad --- /dev/null +++ b/webui/src/components/URIAutocomplete.tsx @@ -0,0 +1,49 @@ +import { AutoComplete } from "antd"; +import React, { useEffect, useState } from "react"; +import { ResticUI } from "../../gen/ts/v1/service.pb"; +import { StringList } from "../../gen/ts/types/value.pb"; + +let timeout: NodeJS.Timeout | undefined = undefined; + +export const URIAutocomplete = (props: React.PropsWithChildren) => { + const [value, setValue] = useState(""); + const [options, setOptions] = useState<{ value: string }[]>([]); + const [showOptions, setShowOptions] = useState<{ value: string }[]>([]); + + useEffect(() => { + setShowOptions(options.filter((o) => o.value.indexOf(value) !== -1)); + }, [options]); + + const onChange = (value: string) => { + setValue(value); + + const lastSlash = value.lastIndexOf("/"); + if (lastSlash !== -1) { + value = value.substring(0, lastSlash); + } + + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + ResticUI.PathAutocomplete({ value: value + "/" }, { pathPrefix: "/api" }) + .then((res: StringList) => { + if (!res.values) { + return; + } + const vals = res.values.map((v) => { + return { + value: value + "/" + v, + }; + }); + setOptions(vals); + }) + .catch((e) => { + console.log("Path autocomplete error: ", e); + }); + }, 100); + }; + + return ; +}; diff --git a/webui/src/index.tsx b/webui/src/index.tsx index e3c0db13..e5f933ea 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -4,11 +4,14 @@ import { App } from "./views/App"; import { RecoilRoot } from "recoil"; import ErrorBoundary from "antd/es/alert/ErrorBoundary"; import { AlertContextProvider } from "./components/Alerts"; +import { ModalContextProvider } from "./components/ModalManager"; const Root = ({ children }: { children: React.ReactNode }) => { return ( - {children} + + {children} + ); }; diff --git a/webui/src/state/config.ts b/webui/src/state/config.ts index 8e6eba4f..a2baee46 100644 --- a/webui/src/state/config.ts +++ b/webui/src/state/config.ts @@ -1,5 +1,5 @@ import { atom, useSetRecoilState } from "recoil"; -import { Config } from "../../gen/ts/v1/config.pb"; +import { Config, Repo } from "../../gen/ts/v1/config.pb"; import { ResticUI } from "../../gen/ts/v1/service.pb"; export const configState = atom({ @@ -8,10 +8,17 @@ export const configState = atom({ }); export const fetchConfig = async (): Promise => { - return await ResticUI.GetConfig( - {}, - { - pathPrefix: "/api/", - } - ); + return await ResticUI.GetConfig({}, { pathPrefix: "/api/" }); }; + +export const addRepo = async (repo: Repo): Promise => { + return await ResticUI.AddRepo(repo, { + pathPrefix: "/api/", + }); +}; + +export const setConfig = async (config: Config): Promise => { + return await ResticUI.SetConfig(config, { + pathPrefix: "/api/", + }); +}; \ No newline at end of file diff --git a/webui/src/views/AddPlanModel.tsx b/webui/src/views/AddPlanModel.tsx index e69de29b..976fa1e8 100644 --- a/webui/src/views/AddPlanModel.tsx +++ b/webui/src/views/AddPlanModel.tsx @@ -0,0 +1,102 @@ +import { Form, Modal, Input, Typography } from "antd"; +import React, { useState } from "react"; +import { useShowModal } from "../components/ModalManager"; +import { Plan } from "../../gen/ts/v1/config.pb"; + +const nameRegex = /^[a-zA-Z0-9\-_ ]+$/; + +export const AddPlanModal = ({ + template, +}: { + template: Partial | null; +}) => { + const [confirmLoading, setConfirmLoading] = useState(false); + const showModal = useShowModal(); + const [form] = Form.useForm(); + + template = template || {}; + + const handleOk = () => { + setConfirmLoading(true); + setTimeout(() => { + showModal(null); + setConfirmLoading(false); + }, 2000); + }; + + const handleCancel = () => { + showModal(null); + }; + + return ( + <> + +
+ {/* Plan.id */} + + hasFeedback + name="id" + label="Plan Name" + initialValue={template.id} + validateTrigger={["onChange", "onBlur"]} + rules={[ + { + required: true, + message: "Please input plan name", + }, + { + pattern: nameRegex, + message: "Invalid symbol", + }, + ]} + > + + + + {/* Plan.repo */} + + hasFeedback + name="repo" + label="Repo Name" + initialValue={template.repo} + validateTrigger={["onChange", "onBlur"]} + rules={[ + { + required: true, + message: "Please input repo name", + }, + { + pattern: nameRegex, + message: "Invalid symbol", + }, + ]} + > + + + + {/* Plan.paths */} + + {/* Plan.excludes */} + + {/* Plan.cron */} + + {/* Plan.retention */} + + + {() => ( + +
{JSON.stringify(form.getFieldsValue(), null, 2)}
+
+ )} +
+ +
+ + ); +}; diff --git a/webui/src/views/AddRepoModel.tsx b/webui/src/views/AddRepoModel.tsx index e69de29b..553a0177 100644 --- a/webui/src/views/AddRepoModel.tsx +++ b/webui/src/views/AddRepoModel.tsx @@ -0,0 +1,316 @@ +import { + Form, + Modal, + Input, + Typography, + AutoComplete, + Tooltip, + Button, +} from "antd"; +import React, { useState } from "react"; +import { useShowModal } from "../components/ModalManager"; +import { Repo } from "../../gen/ts/v1/config.pb"; +import { URIAutocomplete } from "../components/URIAutocomplete"; +import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; +import { useAlertApi } from "../components/Alerts"; +import { ResticUI } from "../../gen/ts/v1/service.pb"; + +export const AddRepoModel = ({ + template, +}: { + template: Partial | null; +}) => { + const [confirmLoading, setConfirmLoading] = useState(false); + const showModal = useShowModal(); + const alertsApi = useAlertApi()!; + const [form] = Form.useForm(); + + template = template || {}; + + const handleOk = () => { + const errors = form + .getFieldsError() + .map((e) => e.errors) + .flat(); + if (errors.length > 0) { + alertsApi.warning("Please fix form errors " + errors.join(", ")); + return; + } + setConfirmLoading(true); + + const repo = form.getFieldsValue() as Repo; + + if (template === null) { + // We are in the create repo flow, create the new repo via the service + ResticUI.AddRepo(repo, { + pathPrefix: "/api", + }) + .then((res) => { + showModal(null); + alertsApi.success("Added repo " + repo.uri); + }) + .catch((e) => { + alertsApi.error("Error adding repo: " + e.message, 15); + }) + .finally(() => { + setConfirmLoading(false); + }); + } else { + } + }; + + const handleCancel = () => { + showModal(null); + }; + + return ( + <> + +
+ {/* Repo.id */} + + hasFeedback + name="id" + label="Repo Name" + initialValue={template.id} + validateTrigger={["onChange", "onBlur"]} + rules={[ + { + required: true, + message: "Please input plan name", + }, + { + pattern: nameRegex, + message: "Invalid symbol", + }, + ]} + > + + + + {/* Repo.uri */} + + + Valid Repo URIs are: +
    +
  • Local filesystem path
  • +
  • S3 e.g. s3:// ...
  • +
  • SFTP e.g. sftp://user@host:/repo-path
  • +
  • + See{" "} + + restic docs + {" "} + for more info. +
  • +
+ + } + > + + hasFeedback + name="uri" + label="Repo URI" + initialValue={template.id} + validateTrigger={["onChange", "onBlur"]} + rules={[ + { + required: true, + message: "Please input repo URI", + }, + ]} + > + + +
+ + {/* Repo.password */} + + hasFeedback + name="password" + label="Password" + initialValue={template.password} + validateTrigger={["onChange", "onBlur"]} + rules={[ + { + required: true, + message: "Please input repo name", + }, + { + pattern: nameRegex, + message: "Invalid symbol", + }, + ]} + > + + + + {/* Repo.env */} + { + let uri = form.getFieldValue("uri"); + return await envVarSetValidator(uri, envVars); + }, + }, + ]} + initialValue={[]} + > + {(fields, { add, remove }, { errors }) => ( + <> + {fields.map((field, index) => ( + + + + + remove(field.name)} + style={{ paddingLeft: "5px" }} + /> + + ))} + + + + + + )} + + + {/* Repo.flags */} + + {(fields, { add, remove }, { errors }) => ( + <> + {fields.map((field, index) => ( + + + + + remove(field.name)} + style={{ paddingLeft: "5px" }} + /> + + ))} + + + + + + )} + + + + {() => ( + +
{JSON.stringify(form.getFieldsValue(), null, 2)}
+
+ )} +
+ +
+ + ); +}; + +const nameRegex = /^[a-zA-Z0-9\-_ ]+$/; + +const expectedEnvVars: { [scheme: string]: string[] } = { + s3: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"], + b2: ["B2_ACCOUNT_ID", "B2_ACCOUNT_KEY"], +}; + +const envVarSetValidator = (uri: string | undefined, envVars: string[]) => { + if (!uri) { + return Promise.resolve(); + } + let schemeIdx = uri.indexOf(":"); + if (schemeIdx === -1) { + return Promise.resolve(); + } + + let scheme = uri.substring(0, schemeIdx); + let expected = expectedEnvVars[scheme]; + if (!expected) { + return Promise.resolve(); + } + + let missing: string[] = []; + for (let e of expected) { + if (!envVars.includes(e)) { + missing.push(e); + } + } + + return Promise.reject( + new Error( + "Missing env vars " + missing.join(", ") + " for scheme " + scheme + ) + ); +}; diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index f146660d..377e8254 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -11,6 +11,9 @@ import { configState, fetchConfig } from "../state/config"; import { useRecoilState } from "recoil"; import { Config } from "../../gen/ts/v1/config.pb"; import { AlertContextProvider, useAlertApi } from "../components/Alerts"; +import { useShowModal, useShowSpinner } from "../components/ModalManager"; +import { AddPlanModal } from "./AddPlanModel"; +import { AddRepoModel } from "./AddRepoModel"; const { Header, Content, Sider } = Layout; @@ -21,14 +24,20 @@ export const App: React.FC = () => { const [config, setConfig] = useRecoilState(configState); const alertApi = useAlertApi()!; + const showModal = useShowModal(); useEffect(() => { + showModal(); + fetchConfig() .then((config) => { setConfig(config); }) .catch((err) => { - alertApi.error(err.message, 60); + alertApi.error(err.message, 0); + }) + .finally(() => { + showModal(null); }); }, []); @@ -50,9 +59,10 @@ export const App: React.FC = () => { /> - - Home - + { }; const getSidenavItems = (config: Config | null): MenuProps["items"] => { + const showModal = useShowModal(); + if (!config) return []; const configPlans = config.plans || []; @@ -80,12 +92,18 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { key: "add-plan", icon: , label: "Add Plan", + onClick: () => { + showModal(); + }, }, ...configPlans.map((plan) => { return { key: "p-" + plan.id, icon: , label: plan.id, + onClick: () => { + showModal(); + }, }; }), ]; @@ -95,12 +113,18 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { key: "add-repo", icon: , label: "Add Repo", + onClick: () => { + showModal(); + }, }, ...configRepos.map((repo) => { return { key: "r-" + repo.id, icon: , label: repo.id, + onClick: () => { + showModal(); + }, }; }), ];