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} );