diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go
index 95a88d2..f59267c 100644
--- a/gen/go/v1/service.pb.go
+++ b/gen/go/v1/service.pb.go
@@ -741,7 +741,7 @@ var file_v1_service_proto_rawDesc = []byte{
0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a,
0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74,
0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0xa7, 0x08, 0x0a, 0x08, 0x42, 0x61,
+ 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0xe2, 0x08, 0x0a, 0x08, 0x42, 0x61,
0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31,
@@ -800,18 +800,22 @@ var file_v1_service_proto_rawDesc = []byte{
0x32, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x12, 0x2e, 0x76, 0x31, 0x2e,
0x4c, 0x6f, 0x67, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11,
0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75,
- 0x65, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74,
- 0x6f, 0x72, 0x79, 0x12, 0x17, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69,
- 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67,
- 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
- 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75,
- 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70,
- 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11,
- 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73,
- 0x74, 0x22, 0x00, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
- 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62,
- 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76,
- 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f,
+ 0x61, 0x64, 0x55, 0x52, 0x4c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e,
+ 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73,
+ 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x41,
+ 0x0a, 0x0c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x17,
+ 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+ 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
+ 0x00, 0x12, 0x3b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d,
+ 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74,
+ 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65,
+ 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x42, 0x2c,
+ 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72,
+ 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65,
+ 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -866,28 +870,30 @@ var file_v1_service_proto_depIdxs = []int32{
12, // 14: v1.Backrest.Stats:input_type -> types.StringValue
13, // 15: v1.Backrest.Cancel:input_type -> types.Int64Value
7, // 16: v1.Backrest.GetLogs:input_type -> v1.LogDataRequest
- 0, // 17: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest
- 12, // 18: v1.Backrest.PathAutocomplete:input_type -> types.StringValue
- 10, // 19: v1.Backrest.GetConfig:output_type -> v1.Config
- 10, // 20: v1.Backrest.SetConfig:output_type -> v1.Config
- 10, // 21: v1.Backrest.AddRepo:output_type -> v1.Config
- 14, // 22: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent
- 15, // 23: v1.Backrest.GetOperations:output_type -> v1.OperationList
- 16, // 24: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList
- 6, // 25: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
- 9, // 26: v1.Backrest.IndexSnapshots:output_type -> google.protobuf.Empty
- 9, // 27: v1.Backrest.Backup:output_type -> google.protobuf.Empty
- 9, // 28: v1.Backrest.Prune:output_type -> google.protobuf.Empty
- 9, // 29: v1.Backrest.Forget:output_type -> google.protobuf.Empty
- 9, // 30: v1.Backrest.Restore:output_type -> google.protobuf.Empty
- 9, // 31: v1.Backrest.Unlock:output_type -> google.protobuf.Empty
- 9, // 32: v1.Backrest.Stats:output_type -> google.protobuf.Empty
- 9, // 33: v1.Backrest.Cancel:output_type -> google.protobuf.Empty
- 17, // 34: v1.Backrest.GetLogs:output_type -> types.BytesValue
- 9, // 35: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty
- 18, // 36: v1.Backrest.PathAutocomplete:output_type -> types.StringList
- 19, // [19:37] is the sub-list for method output_type
- 1, // [1:19] is the sub-list for method input_type
+ 13, // 17: v1.Backrest.GetDownloadURL:input_type -> types.Int64Value
+ 0, // 18: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest
+ 12, // 19: v1.Backrest.PathAutocomplete:input_type -> types.StringValue
+ 10, // 20: v1.Backrest.GetConfig:output_type -> v1.Config
+ 10, // 21: v1.Backrest.SetConfig:output_type -> v1.Config
+ 10, // 22: v1.Backrest.AddRepo:output_type -> v1.Config
+ 14, // 23: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent
+ 15, // 24: v1.Backrest.GetOperations:output_type -> v1.OperationList
+ 16, // 25: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList
+ 6, // 26: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
+ 9, // 27: v1.Backrest.IndexSnapshots:output_type -> google.protobuf.Empty
+ 9, // 28: v1.Backrest.Backup:output_type -> google.protobuf.Empty
+ 9, // 29: v1.Backrest.Prune:output_type -> google.protobuf.Empty
+ 9, // 30: v1.Backrest.Forget:output_type -> google.protobuf.Empty
+ 9, // 31: v1.Backrest.Restore:output_type -> google.protobuf.Empty
+ 9, // 32: v1.Backrest.Unlock:output_type -> google.protobuf.Empty
+ 9, // 33: v1.Backrest.Stats:output_type -> google.protobuf.Empty
+ 9, // 34: v1.Backrest.Cancel:output_type -> google.protobuf.Empty
+ 17, // 35: v1.Backrest.GetLogs:output_type -> types.BytesValue
+ 12, // 36: v1.Backrest.GetDownloadURL:output_type -> types.StringValue
+ 9, // 37: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty
+ 18, // 38: v1.Backrest.PathAutocomplete:output_type -> types.StringList
+ 20, // [20:39] is the sub-list for method output_type
+ 1, // [1:20] is the sub-list for method input_type
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
diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go
index ad13e56..0cfe9e9 100644
--- a/gen/go/v1/service_grpc.pb.go
+++ b/gen/go/v1/service_grpc.pb.go
@@ -37,6 +37,7 @@ const (
Backrest_Stats_FullMethodName = "/v1.Backrest/Stats"
Backrest_Cancel_FullMethodName = "/v1.Backrest/Cancel"
Backrest_GetLogs_FullMethodName = "/v1.Backrest/GetLogs"
+ Backrest_GetDownloadURL_FullMethodName = "/v1.Backrest/GetDownloadURL"
Backrest_ClearHistory_FullMethodName = "/v1.Backrest/ClearHistory"
Backrest_PathAutocomplete_FullMethodName = "/v1.Backrest/PathAutocomplete"
)
@@ -68,8 +69,10 @@ type BackrestClient interface {
Stats(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*emptypb.Empty, error)
- // GetBigOperationData returns the keyed large data for the given operation.
+ // GetLogs returns the keyed large data for the given operation.
GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (*types.BytesValue, error)
+ // GetDownloadURL returns a signed download URL given a forget operation ID.
+ GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error)
// Clears the history of operations
ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
@@ -251,6 +254,15 @@ func (c *backrestClient) GetLogs(ctx context.Context, in *LogDataRequest, opts .
return out, nil
}
+func (c *backrestClient) GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error) {
+ out := new(types.StringValue)
+ err := c.cc.Invoke(ctx, Backrest_GetDownloadURL_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
func (c *backrestClient) ClearHistory(ctx context.Context, in *ClearHistoryRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Backrest_ClearHistory_FullMethodName, in, out, opts...)
@@ -296,8 +308,10 @@ type BackrestServer interface {
Stats(context.Context, *types.StringValue) (*emptypb.Empty, error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error)
- // GetBigOperationData returns the keyed large data for the given operation.
+ // GetLogs returns the keyed large data for the given operation.
GetLogs(context.Context, *LogDataRequest) (*types.BytesValue, error)
+ // GetDownloadURL returns a signed download URL given a forget operation ID.
+ GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error)
// Clears the history of operations
ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
@@ -357,6 +371,9 @@ func (UnimplementedBackrestServer) Cancel(context.Context, *types.Int64Value) (*
func (UnimplementedBackrestServer) GetLogs(context.Context, *LogDataRequest) (*types.BytesValue, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetLogs not implemented")
}
+func (UnimplementedBackrestServer) GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetDownloadURL not implemented")
+}
func (UnimplementedBackrestServer) ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ClearHistory not implemented")
}
@@ -667,6 +684,24 @@ func _Backrest_GetLogs_Handler(srv interface{}, ctx context.Context, dec func(in
return interceptor(ctx, in, info, handler)
}
+func _Backrest_GetDownloadURL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(types.Int64Value)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(BackrestServer).GetDownloadURL(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Backrest_GetDownloadURL_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(BackrestServer).GetDownloadURL(ctx, req.(*types.Int64Value))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
func _Backrest_ClearHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearHistoryRequest)
if err := dec(in); err != nil {
@@ -770,6 +805,10 @@ var Backrest_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetLogs",
Handler: _Backrest_GetLogs_Handler,
},
+ {
+ MethodName: "GetDownloadURL",
+ Handler: _Backrest_GetDownloadURL_Handler,
+ },
{
MethodName: "ClearHistory",
Handler: _Backrest_ClearHistory_Handler,
diff --git a/gen/go/v1/v1connect/service.connect.go b/gen/go/v1/v1connect/service.connect.go
index af6003c..3599447 100644
--- a/gen/go/v1/v1connect/service.connect.go
+++ b/gen/go/v1/v1connect/service.connect.go
@@ -69,6 +69,8 @@ const (
BackrestCancelProcedure = "/v1.Backrest/Cancel"
// BackrestGetLogsProcedure is the fully-qualified name of the Backrest's GetLogs RPC.
BackrestGetLogsProcedure = "/v1.Backrest/GetLogs"
+ // BackrestGetDownloadURLProcedure is the fully-qualified name of the Backrest's GetDownloadURL RPC.
+ BackrestGetDownloadURLProcedure = "/v1.Backrest/GetDownloadURL"
// BackrestClearHistoryProcedure is the fully-qualified name of the Backrest's ClearHistory RPC.
BackrestClearHistoryProcedure = "/v1.Backrest/ClearHistory"
// BackrestPathAutocompleteProcedure is the fully-qualified name of the Backrest's PathAutocomplete
@@ -95,6 +97,7 @@ var (
backrestStatsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Stats")
backrestCancelMethodDescriptor = backrestServiceDescriptor.Methods().ByName("Cancel")
backrestGetLogsMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetLogs")
+ backrestGetDownloadURLMethodDescriptor = backrestServiceDescriptor.Methods().ByName("GetDownloadURL")
backrestClearHistoryMethodDescriptor = backrestServiceDescriptor.Methods().ByName("ClearHistory")
backrestPathAutocompleteMethodDescriptor = backrestServiceDescriptor.Methods().ByName("PathAutocomplete")
)
@@ -124,8 +127,10 @@ type BackrestClient interface {
Stats(context.Context, *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error)
- // GetBigOperationData returns the keyed large data for the given operation.
+ // GetLogs returns the keyed large data for the given operation.
GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error)
+ // GetDownloadURL returns a signed download URL given a forget operation ID.
+ GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error)
// Clears the history of operations
ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
@@ -238,6 +243,12 @@ func NewBackrestClient(httpClient connect.HTTPClient, baseURL string, opts ...co
connect.WithSchema(backrestGetLogsMethodDescriptor),
connect.WithClientOptions(opts...),
),
+ getDownloadURL: connect.NewClient[types.Int64Value, types.StringValue](
+ httpClient,
+ baseURL+BackrestGetDownloadURLProcedure,
+ connect.WithSchema(backrestGetDownloadURLMethodDescriptor),
+ connect.WithClientOptions(opts...),
+ ),
clearHistory: connect.NewClient[v1.ClearHistoryRequest, emptypb.Empty](
httpClient,
baseURL+BackrestClearHistoryProcedure,
@@ -271,6 +282,7 @@ type backrestClient struct {
stats *connect.Client[types.StringValue, emptypb.Empty]
cancel *connect.Client[types.Int64Value, emptypb.Empty]
getLogs *connect.Client[v1.LogDataRequest, types.BytesValue]
+ getDownloadURL *connect.Client[types.Int64Value, types.StringValue]
clearHistory *connect.Client[v1.ClearHistoryRequest, emptypb.Empty]
pathAutocomplete *connect.Client[types.StringValue, types.StringList]
}
@@ -355,6 +367,11 @@ func (c *backrestClient) GetLogs(ctx context.Context, req *connect.Request[v1.Lo
return c.getLogs.CallUnary(ctx, req)
}
+// GetDownloadURL calls v1.Backrest.GetDownloadURL.
+func (c *backrestClient) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) {
+ return c.getDownloadURL.CallUnary(ctx, req)
+}
+
// ClearHistory calls v1.Backrest.ClearHistory.
func (c *backrestClient) ClearHistory(ctx context.Context, req *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) {
return c.clearHistory.CallUnary(ctx, req)
@@ -390,8 +407,10 @@ type BackrestHandler interface {
Stats(context.Context, *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error)
- // GetBigOperationData returns the keyed large data for the given operation.
+ // GetLogs returns the keyed large data for the given operation.
GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error)
+ // GetDownloadURL returns a signed download URL given a forget operation ID.
+ GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error)
// Clears the history of operations
ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
@@ -500,6 +519,12 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str
connect.WithSchema(backrestGetLogsMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
+ backrestGetDownloadURLHandler := connect.NewUnaryHandler(
+ BackrestGetDownloadURLProcedure,
+ svc.GetDownloadURL,
+ connect.WithSchema(backrestGetDownloadURLMethodDescriptor),
+ connect.WithHandlerOptions(opts...),
+ )
backrestClearHistoryHandler := connect.NewUnaryHandler(
BackrestClearHistoryProcedure,
svc.ClearHistory,
@@ -546,6 +571,8 @@ func NewBackrestHandler(svc BackrestHandler, opts ...connect.HandlerOption) (str
backrestCancelHandler.ServeHTTP(w, r)
case BackrestGetLogsProcedure:
backrestGetLogsHandler.ServeHTTP(w, r)
+ case BackrestGetDownloadURLProcedure:
+ backrestGetDownloadURLHandler.ServeHTTP(w, r)
case BackrestClearHistoryProcedure:
backrestClearHistoryHandler.ServeHTTP(w, r)
case BackrestPathAutocompleteProcedure:
@@ -623,6 +650,10 @@ func (UnimplementedBackrestHandler) GetLogs(context.Context, *connect.Request[v1
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetLogs is not implemented"))
}
+func (UnimplementedBackrestHandler) GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetDownloadURL is not implemented"))
+}
+
func (UnimplementedBackrestHandler) ClearHistory(context.Context, *connect.Request[v1.ClearHistoryRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.ClearHistory is not implemented"))
}
diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go
index 1ad6580..a1963eb 100644
--- a/internal/api/backresthandler.go
+++ b/internal/api/backresthandler.go
@@ -2,6 +2,8 @@ package api
import (
"context"
+ "encoding/binary"
+ "encoding/hex"
"errors"
"fmt"
"os"
@@ -466,6 +468,26 @@ func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.L
return connect.NewResponse(&types.BytesValue{Value: data}), nil
}
+func (s *BackrestHandler) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) {
+ op, err := s.oplog.Get(req.Msg.Value)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get operation %v: %w", req.Msg.Value, err)
+ }
+ _, ok := op.Op.(*v1.Operation_OperationRestore)
+ if !ok {
+ return nil, fmt.Errorf("operation %v is not a restore operation", req.Msg.Value)
+ }
+ b := make([]byte, 8)
+ binary.BigEndian.PutUint64(b, uint64(op.Id))
+ signature, err := generateSignature(b)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate signature: %w", err)
+ }
+ return connect.NewResponse(&types.StringValue{
+ Value: fmt.Sprintf("./download/%x-%s/", op.Id, hex.EncodeToString(signature)),
+ }), nil
+}
+
func (s *BackrestHandler) PathAutocomplete(ctx context.Context, path *connect.Request[types.StringValue]) (*connect.Response[types.StringList], error) {
ents, err := os.ReadDir(path.Msg.Value)
if errors.Is(err, os.ErrNotExist) {
diff --git a/internal/api/downloadhandler.go b/internal/api/downloadhandler.go
index b28860e..38dfa83 100644
--- a/internal/api/downloadhandler.go
+++ b/internal/api/downloadhandler.go
@@ -1,7 +1,11 @@
package api
import (
- "archive/zip"
+ "archive/tar"
+ "compress/gzip"
+ "crypto/hmac"
+ "encoding/binary"
+ "encoding/hex"
"fmt"
"io"
"net/http"
@@ -9,6 +13,7 @@ import (
"path/filepath"
"strconv"
"strings"
+ "time"
v1 "github.com/garethgeorge/backrest/gen/go/v1"
"github.com/garethgeorge/backrest/internal/oplog"
@@ -18,18 +23,18 @@ import (
func NewDownloadHandler(oplog *oplog.OpLog) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path[1:]
- sep := strings.Index(p, "/")
- if sep == -1 {
+
+ opID, signature, filePath, err := parseDownloadPath(p)
+ if err != nil {
http.Error(w, "invalid path", http.StatusBadRequest)
return
}
- restoreID := p[:sep]
- filePath := p[sep+1:]
- opID, err := strconv.ParseInt(restoreID, 16, 64)
- if err != nil {
- http.Error(w, "invalid restore ID: "+err.Error(), http.StatusBadRequest)
+
+ if ok, err := checkDownloadURLSignature(opID, signature); err != nil || !ok {
+ http.Error(w, fmt.Sprintf("invalid signature: %v", err), http.StatusForbidden)
return
}
+
op, err := oplog.Get(int64(opID))
if err != nil {
http.Error(w, "restore not found", http.StatusNotFound)
@@ -47,12 +52,14 @@ func NewDownloadHandler(oplog *oplog.OpLog) http.Handler {
}
fullPath := filepath.Join(targetPath, filePath)
- w.Header().Set("Content-Disposition", "attachment; filename=archive.zip")
- w.Header().Set("Content-Type", "application/zip")
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=archive-%v.tar.gz", time.Now().Format("2006-01-02-15-04-05")))
+ w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Transfer-Encoding", "binary")
- z := zip.NewWriter(w)
- zap.L().Info("creating zip archive", zap.String("path", fullPath))
+ gzw := gzip.NewWriter(w)
+ defer gzw.Close()
+ t := tar.NewWriter(gzw)
+ zap.L().Info("creating tar archive", zap.String("path", fullPath))
if err := filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
@@ -60,25 +67,84 @@ func NewDownloadHandler(oplog *oplog.OpLog) http.Handler {
if info.IsDir() {
return nil
}
+
+ stat, err := os.Stat(path)
+ if err != nil {
+ zap.L().Warn("error stating file", zap.String("path", path), zap.Error(err))
+ return nil
+ }
file, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
zap.L().Warn("error opening file", zap.String("path", path), zap.Error(err))
return nil
}
defer file.Close()
- f, err := z.Create(path[len(fullPath)+1:])
- if err != nil {
- return fmt.Errorf("add file to zip archive: %w", err)
+
+ if err := t.WriteHeader(&tar.Header{
+ Name: path[len(fullPath)+1:],
+ Size: stat.Size(),
+ Mode: int64(stat.Mode()),
+ ModTime: stat.ModTime(),
+ }); err != nil {
+ zap.L().Warn("error writing tar header", zap.String("path", path), zap.Error(err))
+ return nil
+ }
+ if n, err := io.CopyN(t, file, stat.Size()); err != nil {
+ zap.L().Warn("error copying file to tar archive", zap.String("path", path), zap.Error(err))
+ } else if n != stat.Size() {
+ zap.L().Warn("error copying file to tar archive: short write", zap.String("path", path))
}
- io.Copy(f, file)
return nil
}); err != nil {
- zap.S().Errorf("error creating zip archive: %v", err)
- http.Error(w, "error creating zip archive", http.StatusInternalServerError)
+ zap.S().Errorf("error creating tar archive: %v", err)
+ http.Error(w, "error creating tar archive", http.StatusInternalServerError)
}
- if err := z.Close(); err != nil {
- zap.S().Errorf("error closing zip archive: %v", err)
- http.Error(w, "error closing zip archive", http.StatusInternalServerError)
+ t.Flush()
+ if err := t.Close(); err != nil {
+ zap.S().Errorf("error closing tar archive: %v", err)
+ http.Error(w, "error closing tar archive", http.StatusInternalServerError)
}
})
}
+
+func parseDownloadPath(p string) (int64, string, string, error) {
+ sep := strings.Index(p, "/")
+ if sep == -1 {
+ return 0, "", "", fmt.Errorf("invalid path")
+ }
+ restoreID := p[:sep]
+ filePath := p[sep+1:]
+
+ dash := strings.Index(restoreID, "-")
+ if dash == -1 {
+ return 0, "", "", fmt.Errorf("invalid restore ID")
+ }
+ opID, err := strconv.ParseInt(restoreID[:dash], 16, 64)
+ if err != nil {
+ return 0, "", "", fmt.Errorf("invalid restore ID: %w", err)
+ }
+ signature := restoreID[dash+1:]
+ return opID, signature, filePath, nil
+}
+
+func checkDownloadURLSignature(id int64, signature string) (bool, error) {
+ wantSignatureBytes, err := signOperationIDForDownload(id)
+ if err != nil {
+ return false, err
+ }
+ signatureBytes, err := hex.DecodeString(signature)
+ if err != nil {
+ return false, err
+ }
+ return hmac.Equal(wantSignatureBytes, signatureBytes), nil
+}
+
+func signOperationIDForDownload(id int64) ([]byte, error) {
+ opIDBytes := make([]byte, 8)
+ binary.BigEndian.PutUint64(opIDBytes, uint64(id))
+ signature, err := generateSignature(opIDBytes)
+ if err != nil {
+ return nil, err
+ }
+ return signature, nil
+}
diff --git a/internal/api/signing.go b/internal/api/signing.go
new file mode 100644
index 0000000..3b9e129
--- /dev/null
+++ b/internal/api/signing.go
@@ -0,0 +1,26 @@
+package api
+
+import (
+ "crypto"
+ "crypto/hmac"
+ "crypto/rand"
+)
+
+var (
+ secret = make([]byte, 32)
+)
+
+func init() {
+ n, err := rand.Read(secret)
+ if n != 32 || err != nil {
+ panic("failed to generate secret key")
+ }
+}
+
+func generateSignature(data []byte) ([]byte, error) {
+ h := hmac.New(crypto.SHA256.New, secret)
+ if n, err := h.Write(data); n != len(data) || err != nil {
+ panic("failed to write data to hmac")
+ }
+ return h.Sum(nil), nil
+}
diff --git a/internal/hook/hookvars.go b/internal/hook/hookvars.go
index fc37fae..7ebeed1 100644
--- a/internal/hook/hookvars.go
+++ b/internal/hook/hookvars.go
@@ -154,8 +154,6 @@ Task: "{{ .Task }}" at {{ .FormatTime .CurTime }}
{{ if .Error -}}
Error: {{ .Error }}
{{ end }}
-{{ if .Items -}}
-
`
var templateForSnapshotStart = `
diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go
index 478aa3c..f68ffa1 100644
--- a/internal/oplog/oplog.go
+++ b/internal/oplog/oplog.go
@@ -87,8 +87,7 @@ func NewOpLog(databasePath string) (*OpLog, error) {
// Scan checks the log for incomplete operations. Should only be called at startup.
func (o *OpLog) Scan(onIncomplete func(op *v1.Operation)) error {
- removeIds := make([]int64, 0)
-
+ zap.L().Debug("scanning oplog for incomplete operations")
err := o.db.Update(func(tx *bolt.Tx) error {
sysBucket := tx.Bucket(SystemBucket)
opLogBucket := tx.Bucket(OpLogBucket)
@@ -96,7 +95,7 @@ func (o *OpLog) Scan(onIncomplete func(op *v1.Operation)) error {
if lastValidated := sysBucket.Get([]byte("last_validated")); lastValidated != nil {
c.Seek(lastValidated)
}
- for k, v := c.First(); k != nil; k, v = c.Next() {
+ for k, v := c.Next(); k != nil; k, v = c.Next() {
op := &v1.Operation{}
if err := proto.Unmarshal(v, op); err != nil {
zap.L().Error("error unmarshalling operation, there may be corruption in the oplog", zap.Error(err))
@@ -104,20 +103,18 @@ func (o *OpLog) Scan(onIncomplete func(op *v1.Operation)) error {
}
if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_SYSTEM_CANCELLED || op.Status == v1.OperationStatus_STATUS_USER_CANCELLED || op.Status == v1.OperationStatus_STATUS_UNKNOWN {
- // remove pending or user cancelled operations.
- removeIds = append(removeIds, op.Id)
+ o.deleteOperationHelper(tx, op.Id)
continue
} else if op.Status == v1.OperationStatus_STATUS_INPROGRESS {
onIncomplete(op)
- removeIds = append(removeIds, op.Id)
}
if err := o.addOperationHelper(tx, op); err != nil {
zap.L().Error("error re-adding operation, there may be corruption in the oplog", zap.Error(err))
- continue
}
}
if lastValidated, _ := c.Last(); lastValidated != nil {
+ zap.L().Debug("checkpointing last_validated key")
if err := sysBucket.Put([]byte("last_validated"), lastValidated); err != nil {
return fmt.Errorf("checkpointing last_validated key: %w", err)
}
@@ -127,12 +124,7 @@ func (o *OpLog) Scan(onIncomplete func(op *v1.Operation)) error {
if err != nil {
return fmt.Errorf("scanning log: %v", err)
}
-
- if len(removeIds) > 0 {
- if err := o.Delete(removeIds...); err != nil {
- return fmt.Errorf("removing incomplete operations: %w", err)
- }
- }
+ zap.L().Debug("scan complete")
return nil
}
diff --git a/internal/protoutil/validation.go b/internal/protoutil/validation.go
index 77ab4b3..bd31bf6 100644
--- a/internal/protoutil/validation.go
+++ b/internal/protoutil/validation.go
@@ -24,7 +24,7 @@ func ValidateOperation(op *v1.Operation) error {
return errors.New("operation.plan_id is required")
}
if op.InstanceId == "" {
- zap.L().Warn("operation.instance_id should typically be set")
+ zap.L().Warn("operation.instance_id should typically be set", zap.Any("operation", op))
}
if op.SnapshotId != "" {
if err := restic.ValidateSnapshotId(op.SnapshotId); err != nil {
diff --git a/proto/v1/service.proto b/proto/v1/service.proto
index ca473ee..a57c744 100644
--- a/proto/v1/service.proto
+++ b/proto/v1/service.proto
@@ -50,9 +50,12 @@ service Backrest {
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
rpc Cancel(types.Int64Value) returns (google.protobuf.Empty) {}
- // GetBigOperationData returns the keyed large data for the given operation.
+ // GetLogs returns the keyed large data for the given operation.
rpc GetLogs(LogDataRequest) returns (types.BytesValue) {}
+ // GetDownloadURL returns a signed download URL given a forget operation ID.
+ rpc GetDownloadURL(types.Int64Value) returns (types.StringValue) {}
+
// Clears the history of operations
rpc ClearHistory(ClearHistoryRequest) returns (google.protobuf.Empty) {}
diff --git a/webui/gen/ts/v1/service_connect.ts b/webui/gen/ts/v1/service_connect.ts
index 4816f66..34df21f 100644
--- a/webui/gen/ts/v1/service_connect.ts
+++ b/webui/gen/ts/v1/service_connect.ts
@@ -168,7 +168,7 @@ export const Backrest = {
kind: MethodKind.Unary,
},
/**
- * GetBigOperationData returns the keyed large data for the given operation.
+ * GetLogs returns the keyed large data for the given operation.
*
* @generated from rpc v1.Backrest.GetLogs
*/
@@ -178,6 +178,17 @@ export const Backrest = {
O: BytesValue,
kind: MethodKind.Unary,
},
+ /**
+ * GetDownloadURL returns a signed download URL given a forget operation ID.
+ *
+ * @generated from rpc v1.Backrest.GetDownloadURL
+ */
+ getDownloadURL: {
+ name: "GetDownloadURL",
+ I: Int64Value,
+ O: StringValue,
+ kind: MethodKind.Unary,
+ },
/**
* Clears the history of operations
*
diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx
index 9e42996..bf27299 100644
--- a/webui/src/components/OperationRow.tsx
+++ b/webui/src/components/OperationRow.tsx
@@ -196,7 +196,13 @@ export const OperationRow = ({
) : null}
{operation.status == OperationStatus.STATUS_SUCCESS ? (<>
-
+
>) : null}
>
);