mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-26 02:46:28 +00:00
feat: implement prune support (#25)
This commit is contained in:
+15
-46
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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) = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user