mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-27 19:36:29 +00:00
fix: secure download URLs when downloading tar archive of exported files
This commit is contained in:
+41
-35
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -154,8 +154,6 @@ Task: "{{ .Task }}" at {{ .FormatTime .CurTime }}
|
||||
{{ if .Error -}}
|
||||
Error: {{ .Error }}
|
||||
{{ end }}
|
||||
{{ if .Items -}}
|
||||
|
||||
`
|
||||
|
||||
var templateForSnapshotStart = `
|
||||
|
||||
+5
-13
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -196,7 +196,13 @@ export const OperationRow = ({
|
||||
) : null}
|
||||
{operation.status == OperationStatus.STATUS_SUCCESS ? (<>
|
||||
<br />
|
||||
<Button type="link" href={"/download/" + operation.id.toString(16) + '/'} target="_blank">Download File(s)</Button>
|
||||
<Button type="link" onClick={() => {
|
||||
backrestService.getDownloadURL({ value: operation.id }).then((resp) => {
|
||||
window.open(resp.value, "_blank");
|
||||
}).catch((e) => {
|
||||
alertApi?.error("Failed to fetch download URL: " + e.message);
|
||||
});
|
||||
}}>Download File(s)</Button>
|
||||
</>) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user