feat: repo can be created through UI

This commit is contained in:
garethgeorge
2023-11-11 20:43:00 -08:00
parent cccdd297c1
commit 9ccade5ccd
23 changed files with 1270 additions and 116 deletions
+206
View File
@@ -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
}
+199 -50
View File
@@ -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,
},
+44 -32
View File
@@ -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
+86
View File
@@ -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
)
+42 -4
View File
@@ -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{
{
+2 -1
View File
@@ -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
+2
View File
@@ -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=
+18
View File
@@ -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.
-1
View File
@@ -22,7 +22,6 @@ type ConfigStore interface {
func NewDefaultConfig() *v1.Config {
return &v1.Config{
LogDir: "/var/log/resticui",
Repos: []*v1.Repo{},
Plans: []*v1.Plan{},
}
+55 -14
View File
@@ -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
}
+13
View File
@@ -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;
}
+15 -1
View File
@@ -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
}
+8
View File
@@ -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: "*"
};
}
}
+12
View File
@@ -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[]
}
+13 -1
View File
@@ -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
}
+4
View File
@@ -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<V1Events.Event>, initReq?: fm.InitReq): Promise<void> {
return fm.fetchStreamingRequest<GoogleProtobufEmpty.Empty, V1Events.Event>(`/v1/events?${fm.renderURLSearchParams(req, [])}`, entityNotifier, {...initReq, method: "GET"})
}
static PathAutocomplete(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise<TypesValue.StringList> {
return fm.fetchReq<TypesValue.StringValue, TypesValue.StringList>(`/v1/autocomplete/path`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)})
}
}
+38
View File
@@ -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<React.ReactNode | null>([]);
return (
<ModalContext.Provider
value={{
model: modal,
setModel: setModals,
}}
>
{modal}
{children}
</ModalContext.Provider>
);
};
export const useShowModal = () => {
const context = useContext(ModalContext);
return context.setModel;
};
+49
View File
@@ -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 <AutoComplete options={showOptions} onSearch={onChange} {...props} />;
};
+4 -1
View File
@@ -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 (
<RecoilRoot>
<AlertContextProvider>{children}</AlertContextProvider>
<AlertContextProvider>
<ModalContextProvider>{children}</ModalContextProvider>
</AlertContextProvider>
</RecoilRoot>
);
};
+14 -7
View File
@@ -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<Config> => {
return await ResticUI.GetConfig(
{},
{
pathPrefix: "/api/",
}
);
return await ResticUI.GetConfig({}, { pathPrefix: "/api/" });
};
export const addRepo = async (repo: Repo): Promise<Config> => {
return await ResticUI.AddRepo(repo, {
pathPrefix: "/api/",
});
};
export const setConfig = async (config: Config): Promise<Config> => {
return await ResticUI.SetConfig(config, {
pathPrefix: "/api/",
});
};
+102
View File
@@ -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<Plan> | 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 (
<>
<Modal
open={true}
title="Add Plan"
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
<Form layout={"vertical"} autoComplete="off" form={form}>
{/* Plan.id */}
<Form.Item<Plan>
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",
},
]}
>
<Input />
</Form.Item>
{/* Plan.repo */}
<Form.Item<Plan>
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",
},
]}
>
<Input />
</Form.Item>
{/* Plan.paths */}
{/* Plan.excludes */}
{/* Plan.cron */}
{/* Plan.retention */}
<Form.Item shouldUpdate label="Preview">
{() => (
<Typography>
<pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>
</Typography>
)}
</Form.Item>
</Form>
</Modal>
</>
);
};
+316
View File
@@ -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<Repo> | null;
}) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const showModal = useShowModal();
const alertsApi = useAlertApi()!;
const [form] = Form.useForm<Repo>();
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 (
<>
<Modal
open={true}
title={template ? "Add Restic Repository" : "Edit Restic Repository"}
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
<Form layout={"vertical"} autoComplete="off" form={form}>
{/* Repo.id */}
<Form.Item<Repo>
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",
},
]}
>
<Input />
</Form.Item>
{/* Repo.uri */}
<Tooltip
title={
<>
Valid Repo URIs are:
<ul>
<li>Local filesystem path</li>
<li>S3 e.g. s3:// ...</li>
<li>SFTP e.g. sftp://user@host:/repo-path</li>
<li>
See{" "}
<a href="https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#preparing-a-new-repository">
restic docs
</a>{" "}
for more info.
</li>
</ul>
</>
}
>
<Form.Item<Repo>
hasFeedback
name="uri"
label="Repo URI"
initialValue={template.id}
validateTrigger={["onChange", "onBlur"]}
rules={[
{
required: true,
message: "Please input repo URI",
},
]}
>
<Input />
</Form.Item>
</Tooltip>
{/* Repo.password */}
<Form.Item<Repo>
hasFeedback
name="password"
label="Password"
initialValue={template.password}
validateTrigger={["onChange", "onBlur"]}
rules={[
{
required: true,
message: "Please input repo name",
},
{
pattern: nameRegex,
message: "Invalid symbol",
},
]}
>
<Input />
</Form.Item>
{/* Repo.env */}
<Form.List
name="env"
rules={[
{
validator: async (_, envVars) => {
let uri = form.getFieldValue("uri");
return await envVarSetValidator(uri, envVars);
},
},
]}
initialValue={[]}
>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field, index) => (
<Form.Item
label={index === 0 ? "Environment Variables" : ""}
required={false}
key={field.key}
>
<Form.Item
{...field}
validateTrigger={["onChange", "onBlur"]}
initialValue={""}
rules={[
{
required: true,
whitespace: true,
pattern: /^[\w-]+=.*$/,
message:
"Environment variable must be in format KEY=VALUE",
},
]}
noStyle
>
<Input placeholder="KEY=VALUE" style={{ width: "60%" }} />
</Form.Item>
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(field.name)}
style={{ paddingLeft: "5px" }}
/>
</Form.Item>
))}
<Form.Item
label={fields.length === 0 ? "Environment Variables" : ""}
>
<Button
type="dashed"
onClick={() => add()}
style={{ width: "60%" }}
icon={<PlusOutlined />}
>
Set Environment Variable
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</>
)}
</Form.List>
{/* Repo.flags */}
<Form.List name="flags" initialValue={[]}>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field, index) => (
<Form.Item
label={index === 0 ? "(Advanced) Flag Overrides" : ""}
required={false}
key={field.key}
>
<Form.Item
{...field}
validateTrigger={["onChange", "onBlur"]}
initialValue={""}
rules={[
{
required: true,
whitespace: true,
pattern: /^\-\-[A-Za-z0-9_\-]*$/,
message:
"Value should be a CLI flag e.g. see restic --help",
},
]}
noStyle
>
<Input placeholder="--flag" style={{ width: "60%" }} />
</Form.Item>
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(field.name)}
style={{ paddingLeft: "5px" }}
/>
</Form.Item>
))}
<Form.Item
label={fields.length === 0 ? "(Advanced) Flag Overrides" : ""}
>
<Button
type="dashed"
onClick={() => add()}
style={{ width: "60%" }}
icon={<PlusOutlined />}
>
Set Environment Variable
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</>
)}
</Form.List>
<Form.Item shouldUpdate label="Preview">
{() => (
<Typography>
<pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>
</Typography>
)}
</Form.Item>
</Form>
</Modal>
</>
);
};
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
)
);
};
+28 -4
View File
@@ -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(<Spin spinning={true} fullscreen />);
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 = () => {
/>
</Sider>
<Layout style={{ padding: "0 24px 24px" }}>
<Breadcrumb style={{ margin: "16px 0" }}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
</Breadcrumb>
<Breadcrumb
style={{ margin: "16px 0" }}
items={[{ title: "Home" }]}
></Breadcrumb>
<Content
style={{
padding: 24,
@@ -70,6 +80,8 @@ export const App: React.FC = () => {
};
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: <PlusOutlined />,
label: "Add Plan",
onClick: () => {
showModal(<AddPlanModal template={null} />);
},
},
...configPlans.map((plan) => {
return {
key: "p-" + plan.id,
icon: <CheckCircleOutlined style={{ color: "green" }} />,
label: plan.id,
onClick: () => {
showModal(<AddPlanModal template={plan} />);
},
};
}),
];
@@ -95,12 +113,18 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
key: "add-repo",
icon: <PlusOutlined />,
label: "Add Repo",
onClick: () => {
showModal(<AddRepoModel template={null} />);
},
},
...configRepos.map((repo) => {
return {
key: "r-" + repo.id,
icon: <CheckCircleOutlined style={{ color: "green" }} />,
label: repo.id,
onClick: () => {
showModal(<AddRepoModel template={repo} />);
},
};
}),
];