chore: use JWTs for signed download URLs instead of bespoke signatures

This commit is contained in:
Gareth
2025-12-21 14:14:22 -08:00
parent 2ca200a845
commit 99dc2c8297
13 changed files with 306 additions and 182 deletions
+1 -1
View File
@@ -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()
+130 -74
View File
@@ -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,
},
+8 -8
View File
@@ -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)
}
+8 -8
View File
@@ -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"))
}
+14 -8
View File
@@ -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
}
+8 -43
View File
@@ -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()
+43 -13
View File
@@ -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
}
+30
View File
@@ -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)
}
+7 -2
View File
@@ -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;
+30 -8
View File
@@ -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<LogDataRequest> = /*@__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<GetDownloadURLRequest> = /*@__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<LsEntry> = /*@__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<RunCommandRequest> = /*@__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<SummaryDashboardResponse> = /*@__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<SummaryDashboardResponse_Summary> = /*@__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<SummaryDashboardResponse_BackupChart> = /*@__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;
},
/**
+11 -2
View File
@@ -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"
+1 -1
View File
@@ -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");
})
+15 -14
View File
@@ -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"
);
})