feat: implement prune support (#25)

This commit is contained in:
Gareth
2023-12-02 18:29:33 -08:00
committed by GitHub
parent d4d94b35e1
commit a311b0a3fb
19 changed files with 554 additions and 104 deletions
+15 -46
View File
@@ -377,12 +377,9 @@ type PrunePolicy struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
MaxFrequencyDays int32 `protobuf:"varint,1,opt,name=max_frequency_days,json=maxFrequencyDays,proto3" json:"max_frequency_days,omitempty"` // max frequency of prune runs in days. If 0, prune will be run on every backup.
// Types that are assignable to Policy:
//
// *PrunePolicy_MaxUnusedPercent
// *PrunePolicy_MaxUnusedBytes
Policy isPrunePolicy_Policy `protobuf_oneof:"policy"`
MaxFrequencyDays int32 `protobuf:"varint,1,opt,name=max_frequency_days,json=maxFrequencyDays,proto3" json:"max_frequency_days,omitempty"` // max frequency of prune runs in days. If 0, prune will be run on every backup.
MaxUnusedPercent int32 `protobuf:"varint,100,opt,name=max_unused_percent,json=maxUnusedPercent,proto3" json:"max_unused_percent,omitempty"` // max percentage of repo size that can be unused before prune is run.
MaxUnusedBytes int32 `protobuf:"varint,101,opt,name=max_unused_bytes,json=maxUnusedBytes,proto3" json:"max_unused_bytes,omitempty"` // max number of bytes that can be unused before prune is run.
}
func (x *PrunePolicy) Reset() {
@@ -424,43 +421,20 @@ func (x *PrunePolicy) GetMaxFrequencyDays() int32 {
return 0
}
func (m *PrunePolicy) GetPolicy() isPrunePolicy_Policy {
if m != nil {
return m.Policy
}
return nil
}
func (x *PrunePolicy) GetMaxUnusedPercent() int32 {
if x, ok := x.GetPolicy().(*PrunePolicy_MaxUnusedPercent); ok {
if x != nil {
return x.MaxUnusedPercent
}
return 0
}
func (x *PrunePolicy) GetMaxUnusedBytes() int32 {
if x, ok := x.GetPolicy().(*PrunePolicy_MaxUnusedBytes); ok {
if x != nil {
return x.MaxUnusedBytes
}
return 0
}
type isPrunePolicy_Policy interface {
isPrunePolicy_Policy()
}
type PrunePolicy_MaxUnusedPercent struct {
MaxUnusedPercent int32 `protobuf:"varint,100,opt,name=max_unused_percent,json=maxUnusedPercent,proto3,oneof"` // max percentage of repo size that can be unused before prune is run.
}
type PrunePolicy_MaxUnusedBytes struct {
MaxUnusedBytes int32 `protobuf:"varint,101,opt,name=max_unused_bytes,json=maxUnusedBytes,proto3,oneof"` // max number of bytes that can be unused before prune is run.
}
func (*PrunePolicy_MaxUnusedPercent) isPrunePolicy_Policy() {}
func (*PrunePolicy_MaxUnusedBytes) isPrunePolicy_Policy() {}
var File_v1_config_proto protoreflect.FileDescriptor
var file_v1_config_proto_rawDesc = []byte{
@@ -512,21 +486,20 @@ var file_v1_config_proto_rawDesc = []byte{
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, 0x22, 0xa1, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x75, 0x6e,
0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x93, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x75, 0x6e,
0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x66,
0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x64, 0x61, 0x79, 0x73, 0x18, 0x01, 0x20,
0x01, 0x28, 0x05, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63,
0x79, 0x44, 0x61, 0x79, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75,
0x79, 0x44, 0x61, 0x79, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75,
0x73, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x64, 0x20, 0x01, 0x28,
0x05, 0x48, 0x00, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x50, 0x65,
0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75,
0x73, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x48,
0x00, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65,
0x73, 0x42, 0x08, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 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,
0x05, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x50, 0x65, 0x72, 0x63,
0x65, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65,
0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x6d,
0x61, 0x78, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 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,
}
var (
@@ -628,10 +601,6 @@ func file_v1_config_proto_init() {
}
}
}
file_v1_config_proto_msgTypes[4].OneofWrappers = []interface{}{
(*PrunePolicy_MaxUnusedPercent)(nil),
(*PrunePolicy_MaxUnusedBytes)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
+28 -14
View File
@@ -446,7 +446,7 @@ var file_v1_service_proto_rawDesc = []byte{
0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05,
0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69,
0x6d, 0x65, 0x32, 0x81, 0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12,
0x6d, 0x65, 0x32, 0xa1, 0x07, 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,
@@ -488,7 +488,17 @@ var file_v1_service_proto_rawDesc = []byte{
0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63,
0x6d, 0x64, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74,
0x6d, 0x64, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x4d, 0x0a, 0x05, 0x50, 0x72, 0x75,
0x6e, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e,
0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x18,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x63,
0x6d, 0x64, 0x2f, 0x70, 0x72, 0x75, 0x6e, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x46, 0x6f, 0x72, 0x67,
0x65, 0x74, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e,
0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63,
0x6d, 0x64, 0x2f, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 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,
@@ -538,18 +548,22 @@ var file_v1_service_proto_depIdxs = []int32{
0, // 6: v1.ResticUI.ListSnapshots:input_type -> v1.ListSnapshotsRequest
2, // 7: v1.ResticUI.ListSnapshotFiles:input_type -> v1.ListSnapshotFilesRequest
8, // 8: v1.ResticUI.Backup:input_type -> types.StringValue
8, // 9: v1.ResticUI.PathAutocomplete:input_type -> types.StringValue
6, // 10: v1.ResticUI.GetConfig:output_type -> v1.Config
6, // 11: v1.ResticUI.SetConfig:output_type -> v1.Config
6, // 12: v1.ResticUI.AddRepo:output_type -> v1.Config
9, // 13: v1.ResticUI.GetOperationEvents:output_type -> v1.OperationEvent
10, // 14: v1.ResticUI.GetOperations:output_type -> v1.OperationList
11, // 15: v1.ResticUI.ListSnapshots:output_type -> v1.ResticSnapshotList
3, // 16: v1.ResticUI.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
5, // 17: v1.ResticUI.Backup:output_type -> google.protobuf.Empty
12, // 18: v1.ResticUI.PathAutocomplete:output_type -> types.StringList
10, // [10:19] is the sub-list for method output_type
1, // [1:10] is the sub-list for method input_type
8, // 9: v1.ResticUI.Prune:input_type -> types.StringValue
8, // 10: v1.ResticUI.Forget:input_type -> types.StringValue
8, // 11: v1.ResticUI.PathAutocomplete:input_type -> types.StringValue
6, // 12: v1.ResticUI.GetConfig:output_type -> v1.Config
6, // 13: v1.ResticUI.SetConfig:output_type -> v1.Config
6, // 14: v1.ResticUI.AddRepo:output_type -> v1.Config
9, // 15: v1.ResticUI.GetOperationEvents:output_type -> v1.OperationEvent
10, // 16: v1.ResticUI.GetOperations:output_type -> v1.OperationList
11, // 17: v1.ResticUI.ListSnapshots:output_type -> v1.ResticSnapshotList
3, // 18: v1.ResticUI.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
5, // 19: v1.ResticUI.Backup:output_type -> google.protobuf.Empty
5, // 20: v1.ResticUI.Prune:output_type -> google.protobuf.Empty
5, // 21: v1.ResticUI.Forget:output_type -> google.protobuf.Empty
12, // 22: v1.ResticUI.PathAutocomplete:output_type -> types.StringList
12, // [12:23] is the sub-list for method output_type
1, // [1:12] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
+170
View File
@@ -272,6 +272,74 @@ func local_request_ResticUI_Backup_0(ctx context.Context, marshaler runtime.Mars
}
func request_ResticUI_Prune_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.Prune(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_ResticUI_Prune_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.Prune(ctx, &protoReq)
return msg, metadata, err
}
func request_ResticUI_Forget_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.Forget(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_ResticUI_Forget_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.Forget(ctx, &protoReq)
return msg, metadata, err
}
func request_ResticUI_PathAutocomplete_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq types.StringValue
var metadata runtime.ServerMetadata
@@ -494,6 +562,56 @@ func RegisterResticUIHandlerServer(ctx context.Context, mux *runtime.ServeMux, s
})
mux.Handle("POST", pattern_ResticUI_Prune_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/Prune", runtime.WithHTTPPathPattern("/v1/cmd/prune"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_ResticUI_Prune_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_Prune_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_ResticUI_Forget_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/Forget", runtime.WithHTTPPathPattern("/v1/cmd/forget"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_ResticUI_Forget_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_Forget_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_ResticUI_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -736,6 +854,50 @@ func RegisterResticUIHandlerClient(ctx context.Context, mux *runtime.ServeMux, c
})
mux.Handle("POST", pattern_ResticUI_Prune_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/Prune", runtime.WithHTTPPathPattern("/v1/cmd/prune"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_ResticUI_Prune_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_Prune_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_ResticUI_Forget_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/Forget", runtime.WithHTTPPathPattern("/v1/cmd/forget"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_ResticUI_Forget_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_Forget_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_ResticUI_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -778,6 +940,10 @@ var (
pattern_ResticUI_Backup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "cmd", "backup"}, ""))
pattern_ResticUI_Prune_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "cmd", "prune"}, ""))
pattern_ResticUI_Forget_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "cmd", "forget"}, ""))
pattern_ResticUI_PathAutocomplete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "autocomplete", "path"}, ""))
)
@@ -798,5 +964,9 @@ var (
forward_ResticUI_Backup_0 = runtime.ForwardResponseMessage
forward_ResticUI_Prune_0 = runtime.ForwardResponseMessage
forward_ResticUI_Forget_0 = runtime.ForwardResponseMessage
forward_ResticUI_PathAutocomplete_0 = runtime.ForwardResponseMessage
)
+74
View File
@@ -29,6 +29,8 @@ const (
ResticUI_ListSnapshots_FullMethodName = "/v1.ResticUI/ListSnapshots"
ResticUI_ListSnapshotFiles_FullMethodName = "/v1.ResticUI/ListSnapshotFiles"
ResticUI_Backup_FullMethodName = "/v1.ResticUI/Backup"
ResticUI_Prune_FullMethodName = "/v1.ResticUI/Prune"
ResticUI_Forget_FullMethodName = "/v1.ResticUI/Forget"
ResticUI_PathAutocomplete_FullMethodName = "/v1.ResticUI/PathAutocomplete"
)
@@ -45,6 +47,8 @@ type ResticUIClient interface {
ListSnapshotFiles(ctx context.Context, in *ListSnapshotFilesRequest, opts ...grpc.CallOption) (*ListSnapshotFilesResponse, error)
// Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued.
Backup(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
Prune(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
Forget(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error)
}
@@ -152,6 +156,24 @@ func (c *resticUIClient) Backup(ctx context.Context, in *types.StringValue, opts
return out, nil
}
func (c *resticUIClient) Prune(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, ResticUI_Prune_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *resticUIClient) Forget(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, ResticUI_Forget_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *resticUIClient) PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) {
out := new(types.StringList)
err := c.cc.Invoke(ctx, ResticUI_PathAutocomplete_FullMethodName, in, out, opts...)
@@ -174,6 +196,8 @@ type ResticUIServer interface {
ListSnapshotFiles(context.Context, *ListSnapshotFilesRequest) (*ListSnapshotFilesResponse, error)
// Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued.
Backup(context.Context, *types.StringValue) (*emptypb.Empty, error)
Prune(context.Context, *types.StringValue) (*emptypb.Empty, error)
Forget(context.Context, *types.StringValue) (*emptypb.Empty, error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error)
mustEmbedUnimplementedResticUIServer()
@@ -207,6 +231,12 @@ func (UnimplementedResticUIServer) ListSnapshotFiles(context.Context, *ListSnaps
func (UnimplementedResticUIServer) Backup(context.Context, *types.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Backup not implemented")
}
func (UnimplementedResticUIServer) Prune(context.Context, *types.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Prune not implemented")
}
func (UnimplementedResticUIServer) Forget(context.Context, *types.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Forget not implemented")
}
func (UnimplementedResticUIServer) PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) {
return nil, status.Errorf(codes.Unimplemented, "method PathAutocomplete not implemented")
}
@@ -370,6 +400,42 @@ func _ResticUI_Backup_Handler(srv interface{}, ctx context.Context, dec func(int
return interceptor(ctx, in, info, handler)
}
func _ResticUI_Prune_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).Prune(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ResticUI_Prune_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ResticUIServer).Prune(ctx, req.(*types.StringValue))
}
return interceptor(ctx, in, info, handler)
}
func _ResticUI_Forget_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).Forget(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ResticUI_Forget_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ResticUIServer).Forget(ctx, req.(*types.StringValue))
}
return interceptor(ctx, in, info, handler)
}
func _ResticUI_PathAutocomplete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(types.StringValue)
if err := dec(in); err != nil {
@@ -423,6 +489,14 @@ var ResticUI_ServiceDesc = grpc.ServiceDesc{
MethodName: "Backup",
Handler: _ResticUI_Backup_Handler,
},
{
MethodName: "Prune",
Handler: _ResticUI_Prune_Handler,
},
{
MethodName: "Forget",
Handler: _ResticUI_Forget_Handler,
},
{
MethodName: "PathAutocomplete",
Handler: _ResticUI_PathAutocomplete_Handler,
+27
View File
@@ -244,6 +244,33 @@ func (s *Server) Backup(ctx context.Context, req *types.StringValue) (*emptypb.E
return &emptypb.Empty{}, nil
}
func (s *Server) Forget(ctx context.Context, req *types.StringValue) (*emptypb.Empty, error) {
plan, err := s.orchestrator.GetPlan(req.Value)
if err != nil {
return nil, fmt.Errorf("failed to get plan %q: %w", req.Value, err)
}
at := time.Now()
s.orchestrator.ScheduleTask(orchestrator.NewOneofForgetTask(s.orchestrator, plan, "", at), orchestrator.TaskPriorityInteractive+orchestrator.TaskPriorityForget)
s.orchestrator.ScheduleTask(orchestrator.NewOneofForgetTask(s.orchestrator, plan, "", at), orchestrator.TaskPriorityInteractive+orchestrator.TaskPriorityIndexSnapshots)
return &emptypb.Empty{}, nil
}
func (s *Server) Prune(ctx context.Context, req *types.StringValue) (*emptypb.Empty, error) {
plan, err := s.orchestrator.GetPlan(req.Value)
if err != nil {
return nil, fmt.Errorf("failed to get plan %q: %w", req.Value, err)
}
at := time.Now()
s.orchestrator.ScheduleTask(orchestrator.NewOneofPruneTask(s.orchestrator, plan, "", at, true), orchestrator.TaskPriorityInteractive+orchestrator.TaskPriorityPrune)
return &emptypb.Empty{}, nil
}
func (s *Server) PathAutocomplete(ctx context.Context, path *types.StringValue) (*types.StringList, error) {
ents, err := os.ReadDir(path.Value)
if errors.Is(err, os.ErrNotExist) {
+9 -6
View File
@@ -142,17 +142,20 @@ func backupHelper(ctx context.Context, orchestrator *Orchestrator, plan *v1.Plan
}
zap.L().Info("Backup complete", zap.String("plan", plan.Id), zap.Duration("duration", time.Since(startTime)), zap.Any("summary", summary))
// schedule followup tasks
at := time.Now()
if plan.Retention != nil {
orchestrator.ScheduleTask(NewOneofForgetTask(orchestrator, plan, op.SnapshotId, at), TaskPriorityForget)
}
orchestrator.ScheduleTask(NewOneofIndexSnapshotsTask(orchestrator, plan, at), TaskPriorityIndexSnapshots)
return nil
})
if err != nil {
return fmt.Errorf("backup operation: %w", err)
}
at := time.Now()
if plan.Retention != nil {
orchestrator.ScheduleTask(NewOneofForgetTask(orchestrator, plan, op.SnapshotId, at), taskPriorityForget)
}
orchestrator.ScheduleTask(NewOneofIndexSnapshotsTask(orchestrator, plan, at), taskPriorityIndexSnapshots)
return nil
}
+4 -6
View File
@@ -9,7 +9,6 @@ import (
v1 "github.com/garethgeorge/resticui/gen/go/v1"
"github.com/garethgeorge/resticui/internal/oplog/indexutil"
"github.com/hashicorp/go-multierror"
"go.uber.org/zap"
)
// ForgetTask tracks a forget operation.
@@ -104,16 +103,15 @@ func (t *ForgetTask) Run(ctx context.Context) error {
}
}
if len(forgot) > 0 {
t.orchestrator.ScheduleTask(NewOneofPruneTask(t.orchestrator, t.plan, t.op.SnapshotId, time.Now(), false), TaskPriorityPrune)
}
return err
}); err != nil {
return err
}
if repo.repoConfig.PrunePolicy != nil {
// TODO: schedule a prune task.
zap.S().Warn("repo specified a prune policy, automatic pruning is not yet implemented.")
}
return nil
}
+5 -4
View File
@@ -20,10 +20,11 @@ var ErrRepoInitializationFailed = errors.New("repo initialization failed")
var ErrPlanNotFound = errors.New("plan not found")
const (
TaskPriorityDefault = iota
TaskPriorityInteractive // higher priority than default scheduled operations e.g. the user clicked to run an operation
taskPriorityIndexSnapshots // higher priority than interactive operations e.g. a system operation like a forget or index (typically scheduled by another task that wants work done immediately after it's completion).
taskPriorityForget
TaskPriorityDefault = iota
TaskPriorityIndexSnapshots
TaskPriorityPrune
TaskPriorityForget
TaskPriorityInteractive // highest priority (add other priorities to this value for offsets)
)
// Orchestrator is responsible for managing repos and backups.
+144 -2
View File
@@ -2,12 +2,154 @@ package orchestrator
import (
"bytes"
"context"
"fmt"
"sync"
"time"
v1 "github.com/garethgeorge/resticui/gen/go/v1"
"github.com/garethgeorge/resticui/internal/oplog/indexutil"
"go.uber.org/zap"
)
func pruneHelper() {
// TODO: This is a stub.
// PruneTask tracks a forget operation.
type PruneTask struct {
name string
orchestrator *Orchestrator // owning orchestrator
plan *v1.Plan
linkSnapshot string // snapshot to link the task to.
op *v1.Operation
at *time.Time
force bool
}
var _ Task = &PruneTask{}
func NewOneofPruneTask(orchestrator *Orchestrator, plan *v1.Plan, linkSnapshot string, at time.Time, force bool) *PruneTask {
return &PruneTask{
orchestrator: orchestrator,
plan: plan,
at: &at,
linkSnapshot: linkSnapshot,
force: force, // overrides the PrunePolicy's MaxFrequencyDays
}
}
func (t *PruneTask) Name() string {
return fmt.Sprintf("prune for plan %q", t.plan.Id)
}
func (t *PruneTask) Next(now time.Time) *time.Time {
ret := t.at
if ret != nil {
t.at = nil
t.op = &v1.Operation{
PlanId: t.plan.Id,
RepoId: t.plan.Repo,
SnapshotId: t.linkSnapshot,
UnixTimeStartMs: timeToUnixMillis(*ret),
Status: v1.OperationStatus_STATUS_PENDING,
Op: &v1.Operation_OperationForget{},
}
}
return ret
}
func (t *PruneTask) getNextPruneTime(repo *RepoOrchestrator, policy *v1.PrunePolicy) (time.Time, error) {
var lastPruneTime time.Time
t.orchestrator.OpLog.ForEachByRepo(t.plan.Repo, indexutil.CollectLastN(1000), func(op *v1.Operation) error {
if _, ok := op.Op.(*v1.Operation_OperationPrune); ok {
lastPruneTime = time.Unix(0, op.UnixTimeStartMs*int64(time.Millisecond))
}
return nil
})
if repo.repoConfig.PrunePolicy != nil {
return lastPruneTime.Add(time.Duration(repo.repoConfig.PrunePolicy.MaxFrequencyDays) * 24 * time.Hour), nil
} else {
return lastPruneTime.Add(7 * 24 * time.Hour), nil // default to 7 days.
}
}
func (t *PruneTask) Run(ctx context.Context) error {
t.op.UnixTimeStartMs = curTimeMillis()
return WithOperation(t.orchestrator.OpLog, t.op, func() error {
repo, err := t.orchestrator.GetRepo(t.plan.Repo)
if err != nil {
return fmt.Errorf("get repo %v: %w", t.plan.Repo, err)
}
opPrune := &v1.Operation_OperationPrune{
OperationPrune: &v1.OperationPrune{},
}
t.op.Op = opPrune
if !t.force {
nextPruneTime, err := t.getNextPruneTime(repo, repo.repoConfig.PrunePolicy)
if err != nil {
return fmt.Errorf("get next prune time: %w", err)
}
if nextPruneTime.After(time.Now()) {
opPrune.OperationPrune.Output = "Skipping prune operation.\nPrune will next run at (or after): " + nextPruneTime.String() + "\nAdjust prune policy's MaxFrequencyDays to increase or decrease the interval."
return nil
}
}
ctx, cancel := context.WithCancel(ctx)
interval := time.NewTicker(1 * time.Second)
defer interval.Stop()
var buf synchronizedBuffer
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-interval.C:
output := buf.String()
if len(output) > 8*1024 { // only provide live status upto the first 8K of output.
output = output[:len(output)-8*1024]
}
if opPrune.OperationPrune.Output != output {
opPrune.OperationPrune.Output = buf.String()
if err := t.orchestrator.OpLog.Update(t.op); err != nil {
zap.L().Error("update prune operation with status output", zap.Error(err))
}
}
case <-ctx.Done():
return
}
}
}()
if err := repo.Prune(ctx, &buf); err != nil {
cancel()
return fmt.Errorf("prune: %w", err)
}
cancel()
wg.Wait()
// TODO: it would be best to store the output in separate storage for large status data.
output := buf.String()
if len(output) > 8*1024 { // only provide live status upto the first 8K of output.
output = output[:len(output)-8*1024]
}
t.op.Op = &v1.Operation_OperationPrune{
OperationPrune: &v1.OperationPrune{
Output: output,
},
}
return nil
})
}
func (t *PruneTask) Cancel(withStatus v1.OperationStatus) error {
return nil
}
// synchronizedBuffer is used for collecting prune command's output
+14 -1
View File
@@ -139,10 +139,23 @@ func (r *RepoOrchestrator) Prune(ctx context.Context, output io.Writer) error {
r.mu.Lock()
defer r.mu.Unlock()
policy := r.repoConfig.PrunePolicy
l := zap.L().With(zap.String("repo", r.repoConfig.Id))
var opts []restic.GenericOption
if policy != nil {
if policy.MaxUnusedBytes != 0 {
opts = append(opts, restic.WithFlags("--max-unused", fmt.Sprintf("%v", policy.MaxUnusedBytes)))
} else if policy.MaxUnusedPercent != 0 {
opts = append(opts, restic.WithFlags("--max-unused", fmt.Sprintf("%v", policy.MaxUnusedPercent)))
}
} else {
opts = append(opts, restic.WithFlags("--max-unused", "25%"))
}
l.Debug("Prune snapshots")
err := r.repo.Prune(ctx, output)
err := r.repo.Prune(ctx, output, opts...)
if err != nil {
return fmt.Errorf("prune snapshots for repo %v: %w", r.repoConfig.Id, err)
}
+2
View File
@@ -239,6 +239,8 @@ func (r *Repo) Prune(ctx context.Context, pruneOutput io.Writer, opts ...Generic
cmd.Stdout = writer
cmd.Stderr = writer
writer.Write([]byte("command: " + strings.Join(cmd.Args, " ") + "\n"))
if err := cmd.Run(); err != nil {
return NewCmdError(cmd, buf.Bytes(), err)
}
+2 -5
View File
@@ -49,9 +49,6 @@ message RetentionPolicy {
message PrunePolicy {
int32 max_frequency_days = 1; // max frequency of prune runs in days. If 0, prune will be run on every backup.
oneof policy {
int32 max_unused_percent = 100; // max percentage of repo size that can be unused before prune is run.
int32 max_unused_bytes = 101; // max number of bytes that can be unused before prune is run.
}
int32 max_unused_percent = 100; // max percentage of repo size that can be unused before prune is run.
int32 max_unused_bytes = 101; // max number of bytes that can be unused before prune is run.
}
+14
View File
@@ -67,6 +67,20 @@ service ResticUI {
};
}
rpc Prune(types.StringValue) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/v1/cmd/prune"
body: "*"
};
}
rpc Forget(types.StringValue) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/v1/cmd/forget"
body: "*"
};
}
// PathAutocomplete provides path autocompletion options for a given filesystem path.
rpc PathAutocomplete (types.StringValue) returns (types.StringList) {
option (google.api.http) = {
+4 -15
View File
@@ -3,15 +3,6 @@
/*
* This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY
*/
type Absent<T, K extends keyof T> = { [k in Exclude<keyof T, K>]?: undefined };
type OneOf<T> =
| { [k in keyof T]?: undefined }
| (
keyof T extends infer K ?
(K extends string & keyof T ? { [k in K]: T[K] } & Absent<T, K>
: never)
: never);
export type Config = {
modno?: number
host?: string
@@ -48,10 +39,8 @@ export type RetentionPolicy = {
keepWithinDuration?: string
}
type BasePrunePolicy = {
export type PrunePolicy = {
maxFrequencyDays?: number
}
export type PrunePolicy = BasePrunePolicy
& OneOf<{ maxUnusedPercent: number; maxUnusedBytes: number }>
maxUnusedPercent?: number
maxUnusedBytes?: number
}
+6
View File
@@ -72,6 +72,12 @@ export class ResticUI {
static Backup(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise<GoogleProtobufEmpty.Empty> {
return fm.fetchReq<TypesValue.StringValue, GoogleProtobufEmpty.Empty>(`/v1/cmd/backup`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)})
}
static Prune(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise<GoogleProtobufEmpty.Empty> {
return fm.fetchReq<TypesValue.StringValue, GoogleProtobufEmpty.Empty>(`/v1/cmd/prune`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)})
}
static Forget(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise<GoogleProtobufEmpty.Empty> {
return fm.fetchReq<TypesValue.StringValue, GoogleProtobufEmpty.Empty>(`/v1/cmd/forget`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)})
}
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)})
}
+23
View File
@@ -300,6 +300,29 @@ export const OperationRow = ({
/>
</List.Item>
);
} else if (operation.operationPrune) {
const prune = operation.operationPrune;
return (
<List.Item>
<List.Item.Meta
title={<>Prune Operation</>}
avatar={<DeleteOutlined style={{ color }} />}
description={
<Collapse
size="small"
destroyInactivePanel
items={[
{
key: 1,
label: "Prune Output",
children: <pre>{prune.output}</pre>,
},
]}
/>
}
/>
</List.Item>
);
}
};
+3
View File
@@ -131,6 +131,9 @@ export const OperationTree = ({
if (b.operations[0].operationForget) {
return <>Forget {formatTime(b.displayTime)}</>;
}
if (b.operations[0].operationPrune) {
return <>Prune {formatTime(b.displayTime)}</>;
}
}
if (b.status === OperationStatus.STATUS_PENDING) {
+4 -4
View File
@@ -234,7 +234,7 @@ export const AddPlanModal = ({
noStyle
>
<URIAutocomplete
style={{ width: "100%" }}
style={{ width: "90%" }}
onBlur={() => form.validateFields()}
/>
</Form.Item>
@@ -252,7 +252,7 @@ export const AddPlanModal = ({
<Button
type="dashed"
onClick={() => add()}
style={{ width: "60%" }}
style={{ width: "90%" }}
icon={<PlusOutlined />}
>
Add Path
@@ -289,7 +289,7 @@ export const AddPlanModal = ({
noStyle
>
<URIAutocomplete
style={{ width: "60%" }}
style={{ width: "90%" }}
onBlur={() => form.validateFields()}
/>
</Form.Item>
@@ -307,7 +307,7 @@ export const AddPlanModal = ({
<Button
type="dashed"
onClick={() => add()}
style={{ width: "60%" }}
style={{ width: "90%" }}
icon={<PlusOutlined />}
>
Add Exclusion Glob
+6 -1
View File
@@ -40,7 +40,12 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
};
const handlePruneNow = () => {
alertsApi.warning("Not implemented yet :(");
try {
ResticUI.Prune({ value: plan.id }, { pathPrefix: "/api" });
alertsApi.success("Prune scheduled.");
} catch (e: any) {
alertsApi.error("Failed to schedule prune: " + e.message);
}
};
const handleUnlockNow = () => {