diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index 34adb64f..292abb09 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -256,7 +256,6 @@ func newRootMux( authedMux.Handle(backrestPath, backrestHandler) syncStatePath, syncStateHandlerUnauthed := v1syncconnect.NewBackrestSyncStateServiceHandler(syncStateHandler) authedMux.Handle(syncStatePath, syncStateHandlerUnauthed) - authedMux.Handle("/download/", http.StripPrefix("/download", downloadHandler)) authedMux.Handle("/metrics", metric.GetRegistry().Handler()) // Unauthenticated routes @@ -265,6 +264,7 @@ func newRootMux( unauthedMux.Handle(authPath, authHandler) syncPath, syncHandlerUnauthed := v1syncconnect.NewBackrestSyncServiceHandler(syncHandler) unauthedMux.Handle(syncPath, syncHandlerUnauthed) + unauthedMux.Handle("/download/", http.StripPrefix("/download", downloadHandler)) // Root mux to dispatch to authenticated or unauthenticated handlers rootMux := http.NewServeMux() diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index 7efa35c1..caa566ba 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -683,6 +683,58 @@ func (x *LogDataRequest) GetRef() string { return "" } +type GetDownloadURLRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OpId int64 `protobuf:"varint,1,opt,name=op_id,json=opId,proto3" json:"op_id,omitempty"` + FilePath string `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDownloadURLRequest) Reset() { + *x = GetDownloadURLRequest{} + mi := &file_v1_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDownloadURLRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDownloadURLRequest) ProtoMessage() {} + +func (x *GetDownloadURLRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDownloadURLRequest.ProtoReflect.Descriptor instead. +func (*GetDownloadURLRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{10} +} + +func (x *GetDownloadURLRequest) GetOpId() int64 { + if x != nil { + return x.OpId + } + return 0 +} + +func (x *GetDownloadURLRequest) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + type LsEntry struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -701,7 +753,7 @@ type LsEntry struct { func (x *LsEntry) Reset() { *x = LsEntry{} - mi := &file_v1_service_proto_msgTypes[10] + mi := &file_v1_service_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -713,7 +765,7 @@ func (x *LsEntry) String() string { func (*LsEntry) ProtoMessage() {} func (x *LsEntry) ProtoReflect() protoreflect.Message { - mi := &file_v1_service_proto_msgTypes[10] + mi := &file_v1_service_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -726,7 +778,7 @@ func (x *LsEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use LsEntry.ProtoReflect.Descriptor instead. func (*LsEntry) Descriptor() ([]byte, []int) { - return file_v1_service_proto_rawDescGZIP(), []int{10} + return file_v1_service_proto_rawDescGZIP(), []int{11} } func (x *LsEntry) GetName() string { @@ -809,7 +861,7 @@ type RunCommandRequest struct { func (x *RunCommandRequest) Reset() { *x = RunCommandRequest{} - mi := &file_v1_service_proto_msgTypes[11] + mi := &file_v1_service_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -821,7 +873,7 @@ func (x *RunCommandRequest) String() string { func (*RunCommandRequest) ProtoMessage() {} func (x *RunCommandRequest) ProtoReflect() protoreflect.Message { - mi := &file_v1_service_proto_msgTypes[11] + mi := &file_v1_service_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -834,7 +886,7 @@ func (x *RunCommandRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RunCommandRequest.ProtoReflect.Descriptor instead. func (*RunCommandRequest) Descriptor() ([]byte, []int) { - return file_v1_service_proto_rawDescGZIP(), []int{11} + return file_v1_service_proto_rawDescGZIP(), []int{12} } func (x *RunCommandRequest) GetRepoId() string { @@ -863,7 +915,7 @@ type SummaryDashboardResponse struct { func (x *SummaryDashboardResponse) Reset() { *x = SummaryDashboardResponse{} - mi := &file_v1_service_proto_msgTypes[12] + mi := &file_v1_service_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -875,7 +927,7 @@ func (x *SummaryDashboardResponse) String() string { func (*SummaryDashboardResponse) ProtoMessage() {} func (x *SummaryDashboardResponse) ProtoReflect() protoreflect.Message { - mi := &file_v1_service_proto_msgTypes[12] + mi := &file_v1_service_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -888,7 +940,7 @@ func (x *SummaryDashboardResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SummaryDashboardResponse.ProtoReflect.Descriptor instead. func (*SummaryDashboardResponse) Descriptor() ([]byte, []int) { - return file_v1_service_proto_rawDescGZIP(), []int{12} + return file_v1_service_proto_rawDescGZIP(), []int{13} } func (x *SummaryDashboardResponse) GetRepoSummaries() []*SummaryDashboardResponse_Summary { @@ -939,7 +991,7 @@ type SummaryDashboardResponse_Summary struct { func (x *SummaryDashboardResponse_Summary) Reset() { *x = SummaryDashboardResponse_Summary{} - mi := &file_v1_service_proto_msgTypes[13] + mi := &file_v1_service_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -951,7 +1003,7 @@ func (x *SummaryDashboardResponse_Summary) String() string { func (*SummaryDashboardResponse_Summary) ProtoMessage() {} func (x *SummaryDashboardResponse_Summary) ProtoReflect() protoreflect.Message { - mi := &file_v1_service_proto_msgTypes[13] + mi := &file_v1_service_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -964,7 +1016,7 @@ func (x *SummaryDashboardResponse_Summary) ProtoReflect() protoreflect.Message { // Deprecated: Use SummaryDashboardResponse_Summary.ProtoReflect.Descriptor instead. func (*SummaryDashboardResponse_Summary) Descriptor() ([]byte, []int) { - return file_v1_service_proto_rawDescGZIP(), []int{12, 0} + return file_v1_service_proto_rawDescGZIP(), []int{13, 0} } func (x *SummaryDashboardResponse_Summary) GetId() string { @@ -1057,7 +1109,7 @@ type SummaryDashboardResponse_BackupChart struct { func (x *SummaryDashboardResponse_BackupChart) Reset() { *x = SummaryDashboardResponse_BackupChart{} - mi := &file_v1_service_proto_msgTypes[14] + mi := &file_v1_service_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1069,7 +1121,7 @@ func (x *SummaryDashboardResponse_BackupChart) String() string { func (*SummaryDashboardResponse_BackupChart) ProtoMessage() {} func (x *SummaryDashboardResponse_BackupChart) ProtoReflect() protoreflect.Message { - mi := &file_v1_service_proto_msgTypes[14] + mi := &file_v1_service_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1082,7 +1134,7 @@ func (x *SummaryDashboardResponse_BackupChart) ProtoReflect() protoreflect.Messa // Deprecated: Use SummaryDashboardResponse_BackupChart.ProtoReflect.Descriptor instead. func (*SummaryDashboardResponse_BackupChart) Descriptor() ([]byte, []int) { - return file_v1_service_proto_rawDescGZIP(), []int{12, 1} + return file_v1_service_proto_rawDescGZIP(), []int{13, 1} } func (x *SummaryDashboardResponse_BackupChart) GetFlowId() []int64 { @@ -1192,7 +1244,10 @@ const file_v1_service_proto_rawDesc = "" + "\x04path\x18\x01 \x01(\tR\x04path\x12%\n" + "\aentries\x18\x02 \x03(\v2\v.v1.LsEntryR\aentries\"\"\n" + "\x0eLogDataRequest\x12\x10\n" + - "\x03ref\x18\x01 \x01(\tR\x03ref\"\xd3\x01\n" + + "\x03ref\x18\x01 \x01(\tR\x03ref\"I\n" + + "\x15GetDownloadURLRequest\x12\x13\n" + + "\x05op_id\x18\x01 \x01(\x03R\x04opId\x12\x1b\n" + + "\tfile_path\x18\x02 \x01(\tR\bfilePath\"\xd3\x01\n" + "\aLsEntry\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04type\x18\x02 \x01(\tR\x04type\x12\x12\n" + @@ -1235,7 +1290,7 @@ const file_v1_service_proto_rawDesc = "" + "durationMs\x12+\n" + "\x06status\x18\x04 \x03(\x0e2\x13.v1.OperationStatusR\x06status\x12\x1f\n" + "\vbytes_added\x18\x05 \x03(\x03R\n" + - "bytesAdded2\xa7\t\n" + + "bytesAdded2\xaf\t\n" + "\bBackrest\x121\n" + "\tGetConfig\x12\x16.google.protobuf.Empty\x1a\n" + ".v1.Config\"\x00\x12%\n" + @@ -1260,8 +1315,8 @@ const file_v1_service_proto_rawDesc = "" + "\x06Cancel\x12\x11.types.Int64Value\x1a\x16.google.protobuf.Empty\"\x00\x124\n" + "\aGetLogs\x12\x12.v1.LogDataRequest\x1a\x11.types.BytesValue\"\x000\x01\x128\n" + "\n" + - "RunCommand\x12\x15.v1.RunCommandRequest\x1a\x11.types.Int64Value\"\x00\x129\n" + - "\x0eGetDownloadURL\x12\x11.types.Int64Value\x1a\x12.types.StringValue\"\x00\x12A\n" + + "RunCommand\x12\x15.v1.RunCommandRequest\x1a\x11.types.Int64Value\"\x00\x12A\n" + + "\x0eGetDownloadURL\x12\x19.v1.GetDownloadURLRequest\x1a\x12.types.StringValue\"\x00\x12A\n" + "\fClearHistory\x12\x17.v1.ClearHistoryRequest\x1a\x16.google.protobuf.Empty\"\x00\x12;\n" + "\x10PathAutocomplete\x12\x12.types.StringValue\x1a\x11.types.StringList\"\x00\x12M\n" + "\x13GetSummaryDashboard\x12\x16.google.protobuf.Empty\x1a\x1c.v1.SummaryDashboardResponse\"\x00B,Z*github.com/garethgeorge/backrest/gen/go/v1b\x06proto3" @@ -1279,7 +1334,7 @@ func file_v1_service_proto_rawDescGZIP() []byte { } var file_v1_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_v1_service_proto_goTypes = []any{ (DoRepoTaskRequest_Task)(0), // 0: v1.DoRepoTaskRequest.Task (*OpSelector)(nil), // 1: v1.OpSelector @@ -1292,73 +1347,74 @@ var file_v1_service_proto_goTypes = []any{ (*ListSnapshotFilesRequest)(nil), // 8: v1.ListSnapshotFilesRequest (*ListSnapshotFilesResponse)(nil), // 9: v1.ListSnapshotFilesResponse (*LogDataRequest)(nil), // 10: v1.LogDataRequest - (*LsEntry)(nil), // 11: v1.LsEntry - (*RunCommandRequest)(nil), // 12: v1.RunCommandRequest - (*SummaryDashboardResponse)(nil), // 13: v1.SummaryDashboardResponse - (*SummaryDashboardResponse_Summary)(nil), // 14: v1.SummaryDashboardResponse.Summary - (*SummaryDashboardResponse_BackupChart)(nil), // 15: v1.SummaryDashboardResponse.BackupChart - (OperationStatus)(0), // 16: v1.OperationStatus - (*emptypb.Empty)(nil), // 17: google.protobuf.Empty - (*Config)(nil), // 18: v1.Config - (*Repo)(nil), // 19: v1.Repo - (*types.StringValue)(nil), // 20: types.StringValue - (*types.Int64Value)(nil), // 21: types.Int64Value - (*types.BoolValue)(nil), // 22: types.BoolValue - (*OperationEvent)(nil), // 23: v1.OperationEvent - (*OperationList)(nil), // 24: v1.OperationList - (*ResticSnapshotList)(nil), // 25: v1.ResticSnapshotList - (*types.BytesValue)(nil), // 26: types.BytesValue - (*types.StringList)(nil), // 27: types.StringList + (*GetDownloadURLRequest)(nil), // 11: v1.GetDownloadURLRequest + (*LsEntry)(nil), // 12: v1.LsEntry + (*RunCommandRequest)(nil), // 13: v1.RunCommandRequest + (*SummaryDashboardResponse)(nil), // 14: v1.SummaryDashboardResponse + (*SummaryDashboardResponse_Summary)(nil), // 15: v1.SummaryDashboardResponse.Summary + (*SummaryDashboardResponse_BackupChart)(nil), // 16: v1.SummaryDashboardResponse.BackupChart + (OperationStatus)(0), // 17: v1.OperationStatus + (*emptypb.Empty)(nil), // 18: google.protobuf.Empty + (*Config)(nil), // 19: v1.Config + (*Repo)(nil), // 20: v1.Repo + (*types.StringValue)(nil), // 21: types.StringValue + (*types.Int64Value)(nil), // 22: types.Int64Value + (*types.BoolValue)(nil), // 23: types.BoolValue + (*OperationEvent)(nil), // 24: v1.OperationEvent + (*OperationList)(nil), // 25: v1.OperationList + (*ResticSnapshotList)(nil), // 26: v1.ResticSnapshotList + (*types.BytesValue)(nil), // 27: types.BytesValue + (*types.StringList)(nil), // 28: types.StringList } var file_v1_service_proto_depIdxs = []int32{ 0, // 0: v1.DoRepoTaskRequest.task:type_name -> v1.DoRepoTaskRequest.Task 1, // 1: v1.ClearHistoryRequest.selector:type_name -> v1.OpSelector 1, // 2: v1.GetOperationsRequest.selector:type_name -> v1.OpSelector - 11, // 3: v1.ListSnapshotFilesResponse.entries:type_name -> v1.LsEntry - 14, // 4: v1.SummaryDashboardResponse.repo_summaries:type_name -> v1.SummaryDashboardResponse.Summary - 14, // 5: v1.SummaryDashboardResponse.plan_summaries:type_name -> v1.SummaryDashboardResponse.Summary - 15, // 6: v1.SummaryDashboardResponse.Summary.recent_backups:type_name -> v1.SummaryDashboardResponse.BackupChart - 16, // 7: v1.SummaryDashboardResponse.BackupChart.status:type_name -> v1.OperationStatus - 17, // 8: v1.Backrest.GetConfig:input_type -> google.protobuf.Empty - 18, // 9: v1.Backrest.SetConfig:input_type -> v1.Config - 19, // 10: v1.Backrest.CheckRepoExists:input_type -> v1.Repo - 19, // 11: v1.Backrest.AddRepo:input_type -> v1.Repo - 20, // 12: v1.Backrest.RemoveRepo:input_type -> types.StringValue - 17, // 13: v1.Backrest.GetOperationEvents:input_type -> google.protobuf.Empty + 12, // 3: v1.ListSnapshotFilesResponse.entries:type_name -> v1.LsEntry + 15, // 4: v1.SummaryDashboardResponse.repo_summaries:type_name -> v1.SummaryDashboardResponse.Summary + 15, // 5: v1.SummaryDashboardResponse.plan_summaries:type_name -> v1.SummaryDashboardResponse.Summary + 16, // 6: v1.SummaryDashboardResponse.Summary.recent_backups:type_name -> v1.SummaryDashboardResponse.BackupChart + 17, // 7: v1.SummaryDashboardResponse.BackupChart.status:type_name -> v1.OperationStatus + 18, // 8: v1.Backrest.GetConfig:input_type -> google.protobuf.Empty + 19, // 9: v1.Backrest.SetConfig:input_type -> v1.Config + 20, // 10: v1.Backrest.CheckRepoExists:input_type -> v1.Repo + 20, // 11: v1.Backrest.AddRepo:input_type -> v1.Repo + 21, // 12: v1.Backrest.RemoveRepo:input_type -> types.StringValue + 18, // 13: v1.Backrest.GetOperationEvents:input_type -> google.protobuf.Empty 6, // 14: v1.Backrest.GetOperations:input_type -> v1.GetOperationsRequest 5, // 15: v1.Backrest.ListSnapshots:input_type -> v1.ListSnapshotsRequest 8, // 16: v1.Backrest.ListSnapshotFiles:input_type -> v1.ListSnapshotFilesRequest - 20, // 17: v1.Backrest.Backup:input_type -> types.StringValue + 21, // 17: v1.Backrest.Backup:input_type -> types.StringValue 2, // 18: v1.Backrest.DoRepoTask:input_type -> v1.DoRepoTaskRequest 4, // 19: v1.Backrest.Forget:input_type -> v1.ForgetRequest 7, // 20: v1.Backrest.Restore:input_type -> v1.RestoreSnapshotRequest - 21, // 21: v1.Backrest.Cancel:input_type -> types.Int64Value + 22, // 21: v1.Backrest.Cancel:input_type -> types.Int64Value 10, // 22: v1.Backrest.GetLogs:input_type -> v1.LogDataRequest - 12, // 23: v1.Backrest.RunCommand:input_type -> v1.RunCommandRequest - 21, // 24: v1.Backrest.GetDownloadURL:input_type -> types.Int64Value + 13, // 23: v1.Backrest.RunCommand:input_type -> v1.RunCommandRequest + 11, // 24: v1.Backrest.GetDownloadURL:input_type -> v1.GetDownloadURLRequest 3, // 25: v1.Backrest.ClearHistory:input_type -> v1.ClearHistoryRequest - 20, // 26: v1.Backrest.PathAutocomplete:input_type -> types.StringValue - 17, // 27: v1.Backrest.GetSummaryDashboard:input_type -> google.protobuf.Empty - 18, // 28: v1.Backrest.GetConfig:output_type -> v1.Config - 18, // 29: v1.Backrest.SetConfig:output_type -> v1.Config - 22, // 30: v1.Backrest.CheckRepoExists:output_type -> types.BoolValue - 18, // 31: v1.Backrest.AddRepo:output_type -> v1.Config - 18, // 32: v1.Backrest.RemoveRepo:output_type -> v1.Config - 23, // 33: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent - 24, // 34: v1.Backrest.GetOperations:output_type -> v1.OperationList - 25, // 35: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList + 21, // 26: v1.Backrest.PathAutocomplete:input_type -> types.StringValue + 18, // 27: v1.Backrest.GetSummaryDashboard:input_type -> google.protobuf.Empty + 19, // 28: v1.Backrest.GetConfig:output_type -> v1.Config + 19, // 29: v1.Backrest.SetConfig:output_type -> v1.Config + 23, // 30: v1.Backrest.CheckRepoExists:output_type -> types.BoolValue + 19, // 31: v1.Backrest.AddRepo:output_type -> v1.Config + 19, // 32: v1.Backrest.RemoveRepo:output_type -> v1.Config + 24, // 33: v1.Backrest.GetOperationEvents:output_type -> v1.OperationEvent + 25, // 34: v1.Backrest.GetOperations:output_type -> v1.OperationList + 26, // 35: v1.Backrest.ListSnapshots:output_type -> v1.ResticSnapshotList 9, // 36: v1.Backrest.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse - 17, // 37: v1.Backrest.Backup:output_type -> google.protobuf.Empty - 17, // 38: v1.Backrest.DoRepoTask:output_type -> google.protobuf.Empty - 17, // 39: v1.Backrest.Forget:output_type -> google.protobuf.Empty - 17, // 40: v1.Backrest.Restore:output_type -> google.protobuf.Empty - 17, // 41: v1.Backrest.Cancel:output_type -> google.protobuf.Empty - 26, // 42: v1.Backrest.GetLogs:output_type -> types.BytesValue - 21, // 43: v1.Backrest.RunCommand:output_type -> types.Int64Value - 20, // 44: v1.Backrest.GetDownloadURL:output_type -> types.StringValue - 17, // 45: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty - 27, // 46: v1.Backrest.PathAutocomplete:output_type -> types.StringList - 13, // 47: v1.Backrest.GetSummaryDashboard:output_type -> v1.SummaryDashboardResponse + 18, // 37: v1.Backrest.Backup:output_type -> google.protobuf.Empty + 18, // 38: v1.Backrest.DoRepoTask:output_type -> google.protobuf.Empty + 18, // 39: v1.Backrest.Forget:output_type -> google.protobuf.Empty + 18, // 40: v1.Backrest.Restore:output_type -> google.protobuf.Empty + 18, // 41: v1.Backrest.Cancel:output_type -> google.protobuf.Empty + 27, // 42: v1.Backrest.GetLogs:output_type -> types.BytesValue + 22, // 43: v1.Backrest.RunCommand:output_type -> types.Int64Value + 21, // 44: v1.Backrest.GetDownloadURL:output_type -> types.StringValue + 18, // 45: v1.Backrest.ClearHistory:output_type -> google.protobuf.Empty + 28, // 46: v1.Backrest.PathAutocomplete:output_type -> types.StringList + 14, // 47: v1.Backrest.GetSummaryDashboard:output_type -> v1.SummaryDashboardResponse 28, // [28:48] is the sub-list for method output_type 8, // [8:28] is the sub-list for method input_type 8, // [8:8] is the sub-list for extension type_name @@ -1381,7 +1437,7 @@ func file_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_service_proto_rawDesc), len(file_v1_service_proto_rawDesc)), NumEnums: 1, - NumMessages: 15, + NumMessages: 16, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index 1e3306e7..a174a5cd 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -70,8 +70,8 @@ type BackrestClient interface { GetLogs(ctx context.Context, in *LogDataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[types.BytesValue], error) // RunCommand executes a generic restic command on the repository. RunCommand(ctx context.Context, in *RunCommandRequest, opts ...grpc.CallOption) (*types.Int64Value, 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) + // GetDownloadURL returns a signed download URL given an operation ID and file path. + GetDownloadURL(ctx context.Context, in *GetDownloadURLRequest, 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. @@ -266,7 +266,7 @@ func (c *backrestClient) RunCommand(ctx context.Context, in *RunCommandRequest, return out, nil } -func (c *backrestClient) GetDownloadURL(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*types.StringValue, error) { +func (c *backrestClient) GetDownloadURL(ctx context.Context, in *GetDownloadURLRequest, opts ...grpc.CallOption) (*types.StringValue, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(types.StringValue) err := c.cc.Invoke(ctx, Backrest_GetDownloadURL_FullMethodName, in, out, cOpts...) @@ -333,8 +333,8 @@ type BackrestServer interface { GetLogs(*LogDataRequest, grpc.ServerStreamingServer[types.BytesValue]) error // RunCommand executes a generic restic command on the repository. RunCommand(context.Context, *RunCommandRequest) (*types.Int64Value, error) - // GetDownloadURL returns a signed download URL given a forget operation ID. - GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error) + // GetDownloadURL returns a signed download URL given an operation ID and file path. + GetDownloadURL(context.Context, *GetDownloadURLRequest) (*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. @@ -399,7 +399,7 @@ func (UnimplementedBackrestServer) GetLogs(*LogDataRequest, grpc.ServerStreaming func (UnimplementedBackrestServer) RunCommand(context.Context, *RunCommandRequest) (*types.Int64Value, error) { return nil, status.Errorf(codes.Unimplemented, "method RunCommand not implemented") } -func (UnimplementedBackrestServer) GetDownloadURL(context.Context, *types.Int64Value) (*types.StringValue, error) { +func (UnimplementedBackrestServer) GetDownloadURL(context.Context, *GetDownloadURLRequest) (*types.StringValue, error) { return nil, status.Errorf(codes.Unimplemented, "method GetDownloadURL not implemented") } func (UnimplementedBackrestServer) ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error) { @@ -707,7 +707,7 @@ func _Backrest_RunCommand_Handler(srv interface{}, ctx context.Context, dec func } func _Backrest_GetDownloadURL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(types.Int64Value) + in := new(GetDownloadURLRequest) if err := dec(in); err != nil { return nil, err } @@ -719,7 +719,7 @@ func _Backrest_GetDownloadURL_Handler(srv interface{}, ctx context.Context, dec FullMethod: Backrest_GetDownloadURL_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(BackrestServer).GetDownloadURL(ctx, req.(*types.Int64Value)) + return srv.(BackrestServer).GetDownloadURL(ctx, req.(*GetDownloadURLRequest)) } return interceptor(ctx, in, info, handler) } diff --git a/gen/go/v1/v1connect/service.connect.go b/gen/go/v1/v1connect/service.connect.go index ecbfa8e7..e9ce9f3c 100644 --- a/gen/go/v1/v1connect/service.connect.go +++ b/gen/go/v1/v1connect/service.connect.go @@ -107,8 +107,8 @@ type BackrestClient interface { GetLogs(context.Context, *connect.Request[v1.LogDataRequest]) (*connect.ServerStreamForClient[types.BytesValue], error) // RunCommand executes a generic restic command on the repository. RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) - // GetDownloadURL returns a signed download URL given a forget operation ID. - GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) + // GetDownloadURL returns a signed download URL given an operation ID and file path. + GetDownloadURL(context.Context, *connect.Request[v1.GetDownloadURLRequest]) (*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. @@ -224,7 +224,7 @@ func NewBackrestClient(httpClient connect.HTTPClient, baseURL string, opts ...co connect.WithSchema(backrestMethods.ByName("RunCommand")), connect.WithClientOptions(opts...), ), - getDownloadURL: connect.NewClient[types.Int64Value, types.StringValue]( + getDownloadURL: connect.NewClient[v1.GetDownloadURLRequest, types.StringValue]( httpClient, baseURL+BackrestGetDownloadURLProcedure, connect.WithSchema(backrestMethods.ByName("GetDownloadURL")), @@ -269,7 +269,7 @@ type backrestClient struct { cancel *connect.Client[types.Int64Value, emptypb.Empty] getLogs *connect.Client[v1.LogDataRequest, types.BytesValue] runCommand *connect.Client[v1.RunCommandRequest, types.Int64Value] - getDownloadURL *connect.Client[types.Int64Value, types.StringValue] + getDownloadURL *connect.Client[v1.GetDownloadURLRequest, types.StringValue] clearHistory *connect.Client[v1.ClearHistoryRequest, emptypb.Empty] pathAutocomplete *connect.Client[types.StringValue, types.StringList] getSummaryDashboard *connect.Client[emptypb.Empty, v1.SummaryDashboardResponse] @@ -356,7 +356,7 @@ func (c *backrestClient) RunCommand(ctx context.Context, req *connect.Request[v1 } // GetDownloadURL calls v1.Backrest.GetDownloadURL. -func (c *backrestClient) GetDownloadURL(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) { +func (c *backrestClient) GetDownloadURL(ctx context.Context, req *connect.Request[v1.GetDownloadURLRequest]) (*connect.Response[types.StringValue], error) { return c.getDownloadURL.CallUnary(ctx, req) } @@ -400,8 +400,8 @@ type BackrestHandler interface { GetLogs(context.Context, *connect.Request[v1.LogDataRequest], *connect.ServerStream[types.BytesValue]) error // RunCommand executes a generic restic command on the repository. RunCommand(context.Context, *connect.Request[v1.RunCommandRequest]) (*connect.Response[types.Int64Value], error) - // GetDownloadURL returns a signed download URL given a forget operation ID. - GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) + // GetDownloadURL returns a signed download URL given an operation ID and file path. + GetDownloadURL(context.Context, *connect.Request[v1.GetDownloadURLRequest]) (*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. @@ -652,7 +652,7 @@ func (UnimplementedBackrestHandler) RunCommand(context.Context, *connect.Request return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.RunCommand is not implemented")) } -func (UnimplementedBackrestHandler) GetDownloadURL(context.Context, *connect.Request[types.Int64Value]) (*connect.Response[types.StringValue], error) { +func (UnimplementedBackrestHandler) GetDownloadURL(context.Context, *connect.Request[v1.GetDownloadURLRequest]) (*connect.Response[types.StringValue], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("v1.Backrest.GetDownloadURL is not implemented")) } diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index ba0b7765..fc3cf894 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -3,7 +3,6 @@ package api import ( "bytes" "context" - "encoding/hex" "errors" "fmt" "io" @@ -750,25 +749,32 @@ func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.L } -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) +func (s *BackrestHandler) GetDownloadURL(ctx context.Context, req *connect.Request[v1.GetDownloadURLRequest]) (*connect.Response[types.StringValue], error) { + op, err := s.oplog.Get(req.Msg.OpId) if err != nil { - return nil, fmt.Errorf("failed to get operation %v: %w", req.Msg.Value, err) + return nil, fmt.Errorf("failed to get operation %v: %w", req.Msg.OpId, err) } + var opType string switch op.Op.(type) { case *v1.Operation_OperationIndexSnapshot: + opType = "snapshot" case *v1.Operation_OperationRestore: + opType = "restore" default: - return nil, fmt.Errorf("operation %v is not a restore or snapshot operation", req.Msg.Value) + return nil, fmt.Errorf("operation %v is not a restore or snapshot operation", req.Msg.OpId) } - signature, err := signInt64(op.Id) // the signature authenticates the download URL. Note that the shared URL will be valid for any downloader. + token, err := signDownloadToken(DownloadTokenPayload{ + OpID: op.Id, + Type: opType, + FilePath: req.Msg.FilePath, + }) if err != nil { - return nil, fmt.Errorf("failed to generate signature: %w", err) + return nil, fmt.Errorf("failed to generate token: %w", err) } return connect.NewResponse(&types.StringValue{ - Value: fmt.Sprintf("./download/%x-%s/", op.Id, hex.EncodeToString(signature)), + Value: fmt.Sprintf("./download/%s/", token), }), nil } diff --git a/internal/api/downloadhandler.go b/internal/api/downloadhandler.go index 1eb5d8bd..bb12a213 100644 --- a/internal/api/downloadhandler.go +++ b/internal/api/downloadhandler.go @@ -4,8 +4,6 @@ import ( "archive/tar" "bytes" "compress/gzip" - "crypto/hmac" - "encoding/hex" "errors" "fmt" "io" @@ -13,7 +11,6 @@ import ( "os" "path/filepath" "runtime" - "strconv" "strings" "time" @@ -25,20 +22,20 @@ import ( func NewDownloadHandler(oplog *oplog.OpLog, orchestrator *orchestrator.Orchestrator) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - p := r.URL.Path[1:] - - opID, signature, filePath, err := parseDownloadPath(p) - if err != nil { - http.Error(w, "invalid path", http.StatusBadRequest) - return + tokenStr := strings.TrimSuffix(r.URL.Path[1:], "/") + // The URL might have trailing path which we ignore for token verification but might use for validation. + if sep := strings.Index(tokenStr, "/"); sep != -1 { + tokenStr = tokenStr[:sep] } - if ok, err := checkDownloadURLSignature(opID, signature); err != nil || !ok { + payload, err := verifyDownloadToken(tokenStr) + if err != nil { http.Error(w, fmt.Sprintf("invalid signature: %v", err), http.StatusForbidden) return } - op, err := oplog.Get(int64(opID)) + filePath := payload.FilePath + op, err := oplog.Get(payload.OpID) if err != nil { http.Error(w, "restore not found", http.StatusNotFound) return @@ -144,38 +141,6 @@ func handleRestoreDownload(w http.ResponseWriter, r *http.Request, op *v1.Operat } } -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 := signInt64(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 tarDirectory(w io.Writer, dirpath string) error { tw := tar.NewWriter(w) defer tw.Close() diff --git a/internal/api/signing.go b/internal/api/signing.go index 975d6db5..65e4ad5c 100644 --- a/internal/api/signing.go +++ b/internal/api/signing.go @@ -1,10 +1,12 @@ package api import ( - "crypto" - "crypto/hmac" "crypto/rand" - "encoding/binary" + "encoding/json" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" ) var ( @@ -18,16 +20,44 @@ func init() { } } -func sign(data []byte) ([]byte, error) { - h := hmac.New(crypto.SHA256.New, secret) - if n, err := h.Write(data); n != len(data) || err != nil { - return nil, err - } - return h.Sum(nil), nil +type DownloadTokenPayload struct { + OpID int64 `json:"op_id"` + Type string `json:"type"` // "snapshot" or "restore" + FilePath string `json:"file_path"` } -func signInt64(data int64) ([]byte, error) { - dataBytes := make([]byte, 8) - binary.BigEndian.PutUint64(dataBytes, uint64(data)) - return sign(dataBytes) +func signDownloadToken(payload DownloadTokenPayload) (string, error) { + jsonPayload, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("failed to marshal payload: %w", err) + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + Subject: string(jsonPayload), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + }) + + return token.SignedString(secret) +} + +func verifyDownloadToken(tokenString string) (*DownloadTokenPayload, error) { + token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { + return secret, nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to parse token: %w", err) + } + + claims, ok := token.Claims.(*jwt.RegisteredClaims) + if !ok || !token.Valid { + return nil, fmt.Errorf("invalid token claims") + } + + var payload DownloadTokenPayload + if err := json.Unmarshal([]byte(claims.Subject), &payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal payload from subject: %w", err) + } + + return &payload, nil } diff --git a/internal/api/signing_test.go b/internal/api/signing_test.go new file mode 100644 index 00000000..dacb623c --- /dev/null +++ b/internal/api/signing_test.go @@ -0,0 +1,30 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDownloadToken(t *testing.T) { + payload := DownloadTokenPayload{ + OpID: 12345, + Type: "snapshot", + FilePath: "/path/to/file", + } + + token, err := signDownloadToken(payload) + assert.NoError(t, err) + assert.NotEmpty(t, token) + + verified, err := verifyDownloadToken(token) + assert.NoError(t, err) + assert.Equal(t, payload.OpID, verified.OpID) + assert.Equal(t, payload.Type, verified.Type) + assert.Equal(t, payload.FilePath, verified.FilePath) +} + +func TestVerifyInvalidToken(t *testing.T) { + _, err := verifyDownloadToken("invalid.token.here") + assert.Error(t, err) +} diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 4f0788f4..6b3c54ef 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -51,8 +51,8 @@ service Backrest { // RunCommand executes a generic restic command on the repository. rpc RunCommand(RunCommandRequest) returns (types.Int64Value) {} - // GetDownloadURL returns a signed download URL given a forget operation ID. - rpc GetDownloadURL(types.Int64Value) returns (types.StringValue) {} + // GetDownloadURL returns a signed download URL given an operation ID and file path. + rpc GetDownloadURL(GetDownloadURLRequest) returns (types.StringValue) {} // Clears the history of operations rpc ClearHistory(ClearHistoryRequest) returns (google.protobuf.Empty) {} @@ -133,6 +133,11 @@ message LogDataRequest { string ref = 1; } +message GetDownloadURLRequest { + int64 op_id = 1; + string file_path = 2; +} + message LsEntry { string name = 1; string type = 2; diff --git a/webui/gen/ts/v1/service_pb.ts b/webui/gen/ts/v1/service_pb.ts index 321117d1..e508b06f 100644 --- a/webui/gen/ts/v1/service_pb.ts +++ b/webui/gen/ts/v1/service_pb.ts @@ -21,7 +21,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file v1/service.proto. */ export const file_v1_service: GenFile = /*@__PURE__*/ - fileDesc("ChB2MS9zZXJ2aWNlLnByb3RvEgJ2MSK/AgoKT3BTZWxlY3RvchILCgNpZHMYASADKAMSGAoLaW5zdGFuY2VfaWQYBiABKAlIAIgBARIkChdvcmlnaW5hbF9pbnN0YW5jZV9rZXlpZBgIIAEoCUgBiAEBEhYKCXJlcG9fZ3VpZBgHIAEoCUgCiAEBEhQKB3BsYW5faWQYAyABKAlIA4gBARIYCgtzbmFwc2hvdF9pZBgEIAEoCUgEiAEBEhQKB2Zsb3dfaWQYBSABKANIBYgBARIWCgltb2Rub19ndGUYCSABKANIBogBAUIOCgxfaW5zdGFuY2VfaWRCGgoYX29yaWdpbmFsX2luc3RhbmNlX2tleWlkQgwKCl9yZXBvX2d1aWRCCgoIX3BsYW5faWRCDgoMX3NuYXBzaG90X2lkQgoKCF9mbG93X2lkQgwKCl9tb2Rub19ndGUiwAEKEURvUmVwb1Rhc2tSZXF1ZXN0Eg8KB3JlcG9faWQYASABKAkSKAoEdGFzaxgCIAEoDjIaLnYxLkRvUmVwb1Rhc2tSZXF1ZXN0LlRhc2sicAoEVGFzaxINCglUQVNLX05PTkUQABIYChRUQVNLX0lOREVYX1NOQVBTSE9UUxABEg4KClRBU0tfUFJVTkUQAhIOCgpUQVNLX0NIRUNLEAMSDgoKVEFTS19TVEFUUxAEEg8KC1RBU0tfVU5MT0NLEAUiTAoTQ2xlYXJIaXN0b3J5UmVxdWVzdBIgCghzZWxlY3RvchgBIAEoCzIOLnYxLk9wU2VsZWN0b3ISEwoLb25seV9mYWlsZWQYAiABKAgiRgoNRm9yZ2V0UmVxdWVzdBIPCgdyZXBvX2lkGAEgASgJEg8KB3BsYW5faWQYAiABKAkSEwoLc25hcHNob3RfaWQYAyABKAkiOAoUTGlzdFNuYXBzaG90c1JlcXVlc3QSDwoHcmVwb19pZBgBIAEoCRIPCgdwbGFuX2lkGAIgASgJIkgKFEdldE9wZXJhdGlvbnNSZXF1ZXN0EiAKCHNlbGVjdG9yGAEgASgLMg4udjEuT3BTZWxlY3RvchIOCgZsYXN0X24YAiABKAMibQoWUmVzdG9yZVNuYXBzaG90UmVxdWVzdBIPCgdwbGFuX2lkGAEgASgJEg8KB3JlcG9faWQYBSABKAkSEwoLc25hcHNob3RfaWQYAiABKAkSDAoEcGF0aBgDIAEoCRIOCgZ0YXJnZXQYBCABKAkiUAoYTGlzdFNuYXBzaG90RmlsZXNSZXF1ZXN0EhEKCXJlcG9fZ3VpZBgBIAEoCRITCgtzbmFwc2hvdF9pZBgCIAEoCRIMCgRwYXRoGAMgASgJIkcKGUxpc3RTbmFwc2hvdEZpbGVzUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIcCgdlbnRyaWVzGAIgAygLMgsudjEuTHNFbnRyeSIdCg5Mb2dEYXRhUmVxdWVzdBILCgNyZWYYASABKAkilgEKB0xzRW50cnkSDAoEbmFtZRgBIAEoCRIMCgR0eXBlGAIgASgJEgwKBHBhdGgYAyABKAkSCwoDdWlkGAQgASgDEgsKA2dpZBgFIAEoAxIMCgRzaXplGAYgASgDEgwKBG1vZGUYByABKAMSDQoFbXRpbWUYCCABKAkSDQoFYXRpbWUYCSABKAkSDQoFY3RpbWUYCiABKAkiNQoRUnVuQ29tbWFuZFJlcXVlc3QSDwoHcmVwb19pZBgBIAEoCRIPCgdjb21tYW5kGAIgASgJIrUFChhTdW1tYXJ5RGFzaGJvYXJkUmVzcG9uc2USPAoOcmVwb19zdW1tYXJpZXMYASADKAsyJC52MS5TdW1tYXJ5RGFzaGJvYXJkUmVzcG9uc2UuU3VtbWFyeRI8Cg5wbGFuX3N1bW1hcmllcxgCIAMoCzIkLnYxLlN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZS5TdW1tYXJ5EhMKC2NvbmZpZ19wYXRoGAogASgJEhEKCWRhdGFfcGF0aBgLIAEoCRruAgoHU3VtbWFyeRIKCgJpZBgBIAEoCRIdChViYWNrdXBzX2ZhaWxlZF8zMGRheXMYAiABKAMSIwobYmFja3Vwc193YXJuaW5nX2xhc3RfMzBkYXlzGAMgASgDEiMKG2JhY2t1cHNfc3VjY2Vzc19sYXN0XzMwZGF5cxgEIAEoAxIhChlieXRlc19zY2FubmVkX2xhc3RfMzBkYXlzGAUgASgDEh8KF2J5dGVzX2FkZGVkX2xhc3RfMzBkYXlzGAYgASgDEhcKD3RvdGFsX3NuYXBzaG90cxgHIAEoAxIZChFieXRlc19zY2FubmVkX2F2ZxgIIAEoAxIXCg9ieXRlc19hZGRlZF9hdmcYCSABKAMSGwoTbmV4dF9iYWNrdXBfdGltZV9tcxgKIAEoAxJACg5yZWNlbnRfYmFja3VwcxgLIAEoCzIoLnYxLlN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZS5CYWNrdXBDaGFydBqDAQoLQmFja3VwQ2hhcnQSDwoHZmxvd19pZBgBIAMoAxIUCgx0aW1lc3RhbXBfbXMYAiADKAMSEwoLZHVyYXRpb25fbXMYAyADKAMSIwoGc3RhdHVzGAQgAygOMhMudjEuT3BlcmF0aW9uU3RhdHVzEhMKC2J5dGVzX2FkZGVkGAUgAygDMqcJCghCYWNrcmVzdBIxCglHZXRDb25maWcSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaCi52MS5Db25maWciABIlCglTZXRDb25maWcSCi52MS5Db25maWcaCi52MS5Db25maWciABIvCg9DaGVja1JlcG9FeGlzdHMSCC52MS5SZXBvGhAudHlwZXMuQm9vbFZhbHVlIgASIQoHQWRkUmVwbxIILnYxLlJlcG8aCi52MS5Db25maWciABIuCgpSZW1vdmVSZXBvEhIudHlwZXMuU3RyaW5nVmFsdWUaCi52MS5Db25maWciABJEChJHZXRPcGVyYXRpb25FdmVudHMSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaEi52MS5PcGVyYXRpb25FdmVudCIAMAESPgoNR2V0T3BlcmF0aW9ucxIYLnYxLkdldE9wZXJhdGlvbnNSZXF1ZXN0GhEudjEuT3BlcmF0aW9uTGlzdCIAEkMKDUxpc3RTbmFwc2hvdHMSGC52MS5MaXN0U25hcHNob3RzUmVxdWVzdBoWLnYxLlJlc3RpY1NuYXBzaG90TGlzdCIAElIKEUxpc3RTbmFwc2hvdEZpbGVzEhwudjEuTGlzdFNuYXBzaG90RmlsZXNSZXF1ZXN0Gh0udjEuTGlzdFNuYXBzaG90RmlsZXNSZXNwb25zZSIAEjYKBkJhY2t1cBISLnR5cGVzLlN0cmluZ1ZhbHVlGhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASPQoKRG9SZXBvVGFzaxIVLnYxLkRvUmVwb1Rhc2tSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASNQoGRm9yZ2V0EhEudjEuRm9yZ2V0UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIAEj8KB1Jlc3RvcmUSGi52MS5SZXN0b3JlU25hcHNob3RSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASNQoGQ2FuY2VsEhEudHlwZXMuSW50NjRWYWx1ZRoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIAEjQKB0dldExvZ3MSEi52MS5Mb2dEYXRhUmVxdWVzdBoRLnR5cGVzLkJ5dGVzVmFsdWUiADABEjgKClJ1bkNvbW1hbmQSFS52MS5SdW5Db21tYW5kUmVxdWVzdBoRLnR5cGVzLkludDY0VmFsdWUiABI5Cg5HZXREb3dubG9hZFVSTBIRLnR5cGVzLkludDY0VmFsdWUaEi50eXBlcy5TdHJpbmdWYWx1ZSIAEkEKDENsZWFySGlzdG9yeRIXLnYxLkNsZWFySGlzdG9yeVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiABI7ChBQYXRoQXV0b2NvbXBsZXRlEhIudHlwZXMuU3RyaW5nVmFsdWUaES50eXBlcy5TdHJpbmdMaXN0IgASTQoTR2V0U3VtbWFyeURhc2hib2FyZBIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRocLnYxLlN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZSIAQixaKmdpdGh1Yi5jb20vZ2FyZXRoZ2VvcmdlL2JhY2tyZXN0L2dlbi9nby92MWIGcHJvdG8z", [file_v1_config, file_v1_restic, file_v1_operations, file_types_value, file_google_protobuf_empty, file_google_api_annotations]); + fileDesc("ChB2MS9zZXJ2aWNlLnByb3RvEgJ2MSK/AgoKT3BTZWxlY3RvchILCgNpZHMYASADKAMSGAoLaW5zdGFuY2VfaWQYBiABKAlIAIgBARIkChdvcmlnaW5hbF9pbnN0YW5jZV9rZXlpZBgIIAEoCUgBiAEBEhYKCXJlcG9fZ3VpZBgHIAEoCUgCiAEBEhQKB3BsYW5faWQYAyABKAlIA4gBARIYCgtzbmFwc2hvdF9pZBgEIAEoCUgEiAEBEhQKB2Zsb3dfaWQYBSABKANIBYgBARIWCgltb2Rub19ndGUYCSABKANIBogBAUIOCgxfaW5zdGFuY2VfaWRCGgoYX29yaWdpbmFsX2luc3RhbmNlX2tleWlkQgwKCl9yZXBvX2d1aWRCCgoIX3BsYW5faWRCDgoMX3NuYXBzaG90X2lkQgoKCF9mbG93X2lkQgwKCl9tb2Rub19ndGUiwAEKEURvUmVwb1Rhc2tSZXF1ZXN0Eg8KB3JlcG9faWQYASABKAkSKAoEdGFzaxgCIAEoDjIaLnYxLkRvUmVwb1Rhc2tSZXF1ZXN0LlRhc2sicAoEVGFzaxINCglUQVNLX05PTkUQABIYChRUQVNLX0lOREVYX1NOQVBTSE9UUxABEg4KClRBU0tfUFJVTkUQAhIOCgpUQVNLX0NIRUNLEAMSDgoKVEFTS19TVEFUUxAEEg8KC1RBU0tfVU5MT0NLEAUiTAoTQ2xlYXJIaXN0b3J5UmVxdWVzdBIgCghzZWxlY3RvchgBIAEoCzIOLnYxLk9wU2VsZWN0b3ISEwoLb25seV9mYWlsZWQYAiABKAgiRgoNRm9yZ2V0UmVxdWVzdBIPCgdyZXBvX2lkGAEgASgJEg8KB3BsYW5faWQYAiABKAkSEwoLc25hcHNob3RfaWQYAyABKAkiOAoUTGlzdFNuYXBzaG90c1JlcXVlc3QSDwoHcmVwb19pZBgBIAEoCRIPCgdwbGFuX2lkGAIgASgJIkgKFEdldE9wZXJhdGlvbnNSZXF1ZXN0EiAKCHNlbGVjdG9yGAEgASgLMg4udjEuT3BTZWxlY3RvchIOCgZsYXN0X24YAiABKAMibQoWUmVzdG9yZVNuYXBzaG90UmVxdWVzdBIPCgdwbGFuX2lkGAEgASgJEg8KB3JlcG9faWQYBSABKAkSEwoLc25hcHNob3RfaWQYAiABKAkSDAoEcGF0aBgDIAEoCRIOCgZ0YXJnZXQYBCABKAkiUAoYTGlzdFNuYXBzaG90RmlsZXNSZXF1ZXN0EhEKCXJlcG9fZ3VpZBgBIAEoCRITCgtzbmFwc2hvdF9pZBgCIAEoCRIMCgRwYXRoGAMgASgJIkcKGUxpc3RTbmFwc2hvdEZpbGVzUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIcCgdlbnRyaWVzGAIgAygLMgsudjEuTHNFbnRyeSIdCg5Mb2dEYXRhUmVxdWVzdBILCgNyZWYYASABKAkiOQoVR2V0RG93bmxvYWRVUkxSZXF1ZXN0Eg0KBW9wX2lkGAEgASgDEhEKCWZpbGVfcGF0aBgCIAEoCSKWAQoHTHNFbnRyeRIMCgRuYW1lGAEgASgJEgwKBHR5cGUYAiABKAkSDAoEcGF0aBgDIAEoCRILCgN1aWQYBCABKAMSCwoDZ2lkGAUgASgDEgwKBHNpemUYBiABKAMSDAoEbW9kZRgHIAEoAxINCgVtdGltZRgIIAEoCRINCgVhdGltZRgJIAEoCRINCgVjdGltZRgKIAEoCSI1ChFSdW5Db21tYW5kUmVxdWVzdBIPCgdyZXBvX2lkGAEgASgJEg8KB2NvbW1hbmQYAiABKAkitQUKGFN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZRI8Cg5yZXBvX3N1bW1hcmllcxgBIAMoCzIkLnYxLlN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZS5TdW1tYXJ5EjwKDnBsYW5fc3VtbWFyaWVzGAIgAygLMiQudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlLlN1bW1hcnkSEwoLY29uZmlnX3BhdGgYCiABKAkSEQoJZGF0YV9wYXRoGAsgASgJGu4CCgdTdW1tYXJ5EgoKAmlkGAEgASgJEh0KFWJhY2t1cHNfZmFpbGVkXzMwZGF5cxgCIAEoAxIjChtiYWNrdXBzX3dhcm5pbmdfbGFzdF8zMGRheXMYAyABKAMSIwobYmFja3Vwc19zdWNjZXNzX2xhc3RfMzBkYXlzGAQgASgDEiEKGWJ5dGVzX3NjYW5uZWRfbGFzdF8zMGRheXMYBSABKAMSHwoXYnl0ZXNfYWRkZWRfbGFzdF8zMGRheXMYBiABKAMSFwoPdG90YWxfc25hcHNob3RzGAcgASgDEhkKEWJ5dGVzX3NjYW5uZWRfYXZnGAggASgDEhcKD2J5dGVzX2FkZGVkX2F2ZxgJIAEoAxIbChNuZXh0X2JhY2t1cF90aW1lX21zGAogASgDEkAKDnJlY2VudF9iYWNrdXBzGAsgASgLMigudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlLkJhY2t1cENoYXJ0GoMBCgtCYWNrdXBDaGFydBIPCgdmbG93X2lkGAEgAygDEhQKDHRpbWVzdGFtcF9tcxgCIAMoAxITCgtkdXJhdGlvbl9tcxgDIAMoAxIjCgZzdGF0dXMYBCADKA4yEy52MS5PcGVyYXRpb25TdGF0dXMSEwoLYnl0ZXNfYWRkZWQYBSADKAMyrwkKCEJhY2tyZXN0EjEKCUdldENvbmZpZxIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRoKLnYxLkNvbmZpZyIAEiUKCVNldENvbmZpZxIKLnYxLkNvbmZpZxoKLnYxLkNvbmZpZyIAEi8KD0NoZWNrUmVwb0V4aXN0cxIILnYxLlJlcG8aEC50eXBlcy5Cb29sVmFsdWUiABIhCgdBZGRSZXBvEggudjEuUmVwbxoKLnYxLkNvbmZpZyIAEi4KClJlbW92ZVJlcG8SEi50eXBlcy5TdHJpbmdWYWx1ZRoKLnYxLkNvbmZpZyIAEkQKEkdldE9wZXJhdGlvbkV2ZW50cxIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRoSLnYxLk9wZXJhdGlvbkV2ZW50IgAwARI+Cg1HZXRPcGVyYXRpb25zEhgudjEuR2V0T3BlcmF0aW9uc1JlcXVlc3QaES52MS5PcGVyYXRpb25MaXN0IgASQwoNTGlzdFNuYXBzaG90cxIYLnYxLkxpc3RTbmFwc2hvdHNSZXF1ZXN0GhYudjEuUmVzdGljU25hcHNob3RMaXN0IgASUgoRTGlzdFNuYXBzaG90RmlsZXMSHC52MS5MaXN0U25hcHNob3RGaWxlc1JlcXVlc3QaHS52MS5MaXN0U25hcHNob3RGaWxlc1Jlc3BvbnNlIgASNgoGQmFja3VwEhIudHlwZXMuU3RyaW5nVmFsdWUaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiABI9CgpEb1JlcG9UYXNrEhUudjEuRG9SZXBvVGFza1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiABI1CgZGb3JnZXQSES52MS5Gb3JnZXRSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASPwoHUmVzdG9yZRIaLnYxLlJlc3RvcmVTbmFwc2hvdFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiABI1CgZDYW5jZWwSES50eXBlcy5JbnQ2NFZhbHVlGhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASNAoHR2V0TG9ncxISLnYxLkxvZ0RhdGFSZXF1ZXN0GhEudHlwZXMuQnl0ZXNWYWx1ZSIAMAESOAoKUnVuQ29tbWFuZBIVLnYxLlJ1bkNvbW1hbmRSZXF1ZXN0GhEudHlwZXMuSW50NjRWYWx1ZSIAEkEKDkdldERvd25sb2FkVVJMEhkudjEuR2V0RG93bmxvYWRVUkxSZXF1ZXN0GhIudHlwZXMuU3RyaW5nVmFsdWUiABJBCgxDbGVhckhpc3RvcnkSFy52MS5DbGVhckhpc3RvcnlSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASOwoQUGF0aEF1dG9jb21wbGV0ZRISLnR5cGVzLlN0cmluZ1ZhbHVlGhEudHlwZXMuU3RyaW5nTGlzdCIAEk0KE0dldFN1bW1hcnlEYXNoYm9hcmQSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaHC52MS5TdW1tYXJ5RGFzaGJvYXJkUmVzcG9uc2UiAEIsWipnaXRodWIuY29tL2dhcmV0aGdlb3JnZS9iYWNrcmVzdC9nZW4vZ28vdjFiBnByb3RvMw", [file_v1_config, file_v1_restic, file_v1_operations, file_types_value, file_google_protobuf_empty, file_google_api_annotations]); /** * OpSelector is a message that can be used to select operations e.g. by query. @@ -338,6 +338,28 @@ export type LogDataRequest = Message<"v1.LogDataRequest"> & { export const LogDataRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_v1_service, 9); +/** + * @generated from message v1.GetDownloadURLRequest + */ +export type GetDownloadURLRequest = Message<"v1.GetDownloadURLRequest"> & { + /** + * @generated from field: int64 op_id = 1; + */ + opId: bigint; + + /** + * @generated from field: string file_path = 2; + */ + filePath: string; +}; + +/** + * Describes the message v1.GetDownloadURLRequest. + * Use `create(GetDownloadURLRequestSchema)` to create a new message. + */ +export const GetDownloadURLRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_service, 10); + /** * @generated from message v1.LsEntry */ @@ -398,7 +420,7 @@ export type LsEntry = Message<"v1.LsEntry"> & { * Use `create(LsEntrySchema)` to create a new message. */ export const LsEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1_service, 10); + messageDesc(file_v1_service, 11); /** * @generated from message v1.RunCommandRequest @@ -420,7 +442,7 @@ export type RunCommandRequest = Message<"v1.RunCommandRequest"> & { * Use `create(RunCommandRequestSchema)` to create a new message. */ export const RunCommandRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1_service, 11); + messageDesc(file_v1_service, 12); /** * @generated from message v1.SummaryDashboardResponse @@ -452,7 +474,7 @@ export type SummaryDashboardResponse = Message<"v1.SummaryDashboardResponse"> & * Use `create(SummaryDashboardResponseSchema)` to create a new message. */ export const SummaryDashboardResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1_service, 12); + messageDesc(file_v1_service, 13); /** * @generated from message v1.SummaryDashboardResponse.Summary @@ -523,7 +545,7 @@ export type SummaryDashboardResponse_Summary = Message<"v1.SummaryDashboardRespo * Use `create(SummaryDashboardResponse_SummarySchema)` to create a new message. */ export const SummaryDashboardResponse_SummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1_service, 12, 0); + messageDesc(file_v1_service, 13, 0); /** * @generated from message v1.SummaryDashboardResponse.BackupChart @@ -560,7 +582,7 @@ export type SummaryDashboardResponse_BackupChart = Message<"v1.SummaryDashboardR * Use `create(SummaryDashboardResponse_BackupChartSchema)` to create a new message. */ export const SummaryDashboardResponse_BackupChartSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_v1_service, 12, 1); + messageDesc(file_v1_service, 13, 1); /** * @generated from service v1.Backrest @@ -709,13 +731,13 @@ export const Backrest: GenService<{ output: typeof Int64ValueSchema; }, /** - * GetDownloadURL returns a signed download URL given a forget operation ID. + * GetDownloadURL returns a signed download URL given an operation ID and file path. * * @generated from rpc v1.Backrest.GetDownloadURL */ getDownloadURL: { methodKind: "unary"; - input: typeof Int64ValueSchema; + input: typeof GetDownloadURLRequestSchema; output: typeof StringValueSchema; }, /** diff --git a/webui/package-lock.json b/webui/package-lock.json index 0f3ce140..1ec5c6bb 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -151,13 +151,15 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz", "integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)" + "license": "(Apache-2.0 AND BSD-3-Clause)", + "peer": true }, "node_modules/@connectrpc/connect": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.0.tgz", "integrity": "sha512-xhiwnYlJNHzmFsRw+iSPIwXR/xweTvTw8x5HiwWp10sbVtd4OpOXbRgE7V58xs1EC17fzusF1f5uOAy24OkBuA==", "license": "Apache-2.0", + "peer": true, "peerDependencies": { "@bufbuild/protobuf": "^2.7.0" } @@ -515,6 +517,7 @@ "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.16.0.tgz", "integrity": "sha512-erH9GdLe8Boie0mCO8hXn8Qt/pCACsOFlKp8UHNMlPaizUtCDkCOQqwmSi+VyrJ3dMMCOc/qBwTSGAJaJE8/Kw==", "license": "MIT", + "peer": true, "dependencies": { "@mischnic/json-sourcemap": "^0.1.1", "@parcel/cache": "2.16.0", @@ -2487,6 +2490,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2543,6 +2547,7 @@ "resolved": "https://registry.npmjs.org/antd/-/antd-5.27.6.tgz", "integrity": "sha512-70HrjVbzDXvtiUQ5MP1XdNudr/wGAk9Ivaemk6f36yrAeJurJSmZ8KngOIilolLRHdGuNc6/Vk+4T1OZpSjpag==", "license": "MIT", + "peer": true, "dependencies": { "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", @@ -2693,6 +2698,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3094,7 +3100,8 @@ "version": "1.11.18", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/decimal.js-light": { "version": "2.5.1", @@ -4677,6 +4684,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4689,6 +4697,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index 977af0c3..e0db94ab 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -458,7 +458,7 @@ const RestoreOperationStatus = ({ operation }: { operation: Operation }) => { type="link" onClick={() => { backrestService - .getDownloadURL({ value: operation.id }) + .getDownloadURL({ opId: operation.id!, filePath: "" }) .then((resp) => { window.open(resp.value, "_blank"); }) diff --git a/webui/src/components/SnapshotBrowser.tsx b/webui/src/components/SnapshotBrowser.tsx index d7955085..3fd8b0d6 100644 --- a/webui/src/components/SnapshotBrowser.tsx +++ b/webui/src/components/SnapshotBrowser.tsx @@ -133,13 +133,19 @@ export const SnapshotBrowser = ({ path += "/"; } - const resp = await backrestService.listSnapshotFiles( - create(ListSnapshotFilesRequestSchema, { - path, - repoGuid, - snapshotId, - }) - ); + let resp: ListSnapshotFilesResponse; + try { + resp = await backrestService.listSnapshotFiles( + create(ListSnapshotFilesRequestSchema, { + path, + repoGuid, + snapshotId, + }) + ); + } catch (e: any) { + alertApi?.error("Failed to load snapshot files: " + e.message); + return; + } setTreeData((treeData) => { let toUpdate: DataNode | null = null; @@ -234,15 +240,10 @@ const FileNode = ({ label: "Download", onClick: () => { backrestService - .getDownloadURL({ value: snapshotOpId }) + .getDownloadURL({ opId: snapshotOpId!, filePath: entry.path! }) .then((resp) => { - const encodePathKeepSlashes = (p: string) => - p - .split("/") - .map((seg) => encodeURIComponent(seg)) - .join("/"); window.open( - resp.value + encodePathKeepSlashes(entry.path!), + resp.value, "_blank" ); })