From 8ffffa05e41ca31e2d38fde5427dae34ac4a1abb Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Thu, 16 Nov 2023 19:38:37 -0800 Subject: [PATCH] feat: implement snapshot browsing --- gen/go/v1/service.pb.go | 466 +++++++++++++++++++---- gen/go/v1/service.pb.gw.go | 85 +++++ gen/go/v1/service_grpc.pb.go | 37 ++ internal/api/server.go | 17 + internal/orchestrator/repo.go | 19 +- internal/orchestrator/tasks.go | 3 +- pkg/restic/outputs.go | 15 + pkg/restic/restic.go | 1 - proto/v1/service.proto | 31 ++ webui/gen/ts/v1/service.pb.ts | 27 ++ webui/src/components/OperationList.tsx | 119 +++++- webui/src/components/SnapshotBrowser.tsx | 159 ++++++++ 12 files changed, 882 insertions(+), 97 deletions(-) create mode 100644 webui/src/components/SnapshotBrowser.tsx diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index 25e7f91f1..a288305da 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -141,6 +141,243 @@ func (x *GetOperationsRequest) GetLastN() int64 { return 0 } +type ListSnapshotFilesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + SnapshotId string `protobuf:"bytes,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` +} + +func (x *ListSnapshotFilesRequest) Reset() { + *x = ListSnapshotFilesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListSnapshotFilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSnapshotFilesRequest) ProtoMessage() {} + +func (x *ListSnapshotFilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSnapshotFilesRequest.ProtoReflect.Descriptor instead. +func (*ListSnapshotFilesRequest) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ListSnapshotFilesRequest) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *ListSnapshotFilesRequest) GetSnapshotId() string { + if x != nil { + return x.SnapshotId + } + return "" +} + +func (x *ListSnapshotFilesRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type ListSnapshotFilesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Entries []*LsEntry `protobuf:"bytes,2,rep,name=entries,proto3" json:"entries,omitempty"` +} + +func (x *ListSnapshotFilesResponse) Reset() { + *x = ListSnapshotFilesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListSnapshotFilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSnapshotFilesResponse) ProtoMessage() {} + +func (x *ListSnapshotFilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSnapshotFilesResponse.ProtoReflect.Descriptor instead. +func (*ListSnapshotFilesResponse) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{3} +} + +func (x *ListSnapshotFilesResponse) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ListSnapshotFilesResponse) GetEntries() []*LsEntry { + if x != nil { + return x.Entries + } + return nil +} + +type LsEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + Uid int64 `protobuf:"varint,4,opt,name=uid,proto3" json:"uid,omitempty"` + Gid int64 `protobuf:"varint,5,opt,name=gid,proto3" json:"gid,omitempty"` + Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"` + Mode int64 `protobuf:"varint,7,opt,name=mode,proto3" json:"mode,omitempty"` + Mtime string `protobuf:"bytes,8,opt,name=mtime,proto3" json:"mtime,omitempty"` + Atime string `protobuf:"bytes,9,opt,name=atime,proto3" json:"atime,omitempty"` + Ctime string `protobuf:"bytes,10,opt,name=ctime,proto3" json:"ctime,omitempty"` +} + +func (x *LsEntry) Reset() { + *x = LsEntry{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LsEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LsEntry) ProtoMessage() {} + +func (x *LsEntry) ProtoReflect() protoreflect.Message { + mi := &file_v1_service_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LsEntry.ProtoReflect.Descriptor instead. +func (*LsEntry) Descriptor() ([]byte, []int) { + return file_v1_service_proto_rawDescGZIP(), []int{4} +} + +func (x *LsEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *LsEntry) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *LsEntry) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *LsEntry) GetUid() int64 { + if x != nil { + return x.Uid + } + return 0 +} + +func (x *LsEntry) GetGid() int64 { + if x != nil { + return x.Gid + } + return 0 +} + +func (x *LsEntry) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *LsEntry) GetMode() int64 { + if x != nil { + return x.Mode + } + return 0 +} + +func (x *LsEntry) GetMtime() string { + if x != nil { + return x.Mtime + } + return "" +} + +func (x *LsEntry) GetAtime() string { + if x != nil { + return x.Atime + } + return "" +} + +func (x *LsEntry) GetCtime() string { + if x != nil { + return x.Ctime + } + return "" +} + var File_v1_service_proto protoreflect.FileDescriptor var file_v1_service_proto_rawDesc = []byte{ @@ -164,51 +401,84 @@ var file_v1_service_proto_rawDesc = []byte{ 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x32, 0x8f, 0x05, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, - 0x63, 0x55, 0x49, 0x12, 0x43, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, - 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, 0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, - 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, - 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, - 0x6f, 0x12, 0x61, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, - 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x30, 0x01, 0x12, 0x57, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, - 0x73, 0x74, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, - 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, - 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, - 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, - 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, - 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, - 0x63, 0x6b, 0x75, 0x70, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x22, 0x56, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, + 0x68, 0x12, 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x01, 0x0a, 0x07, 0x4c, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, + 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x74, 0x69, + 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0x81, + 0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12, 0x43, 0x0a, 0x09, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, - 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, - 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, - 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, - 0x2a, 0x22, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x2f, 0x70, 0x61, 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, - 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x12, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0a, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, + 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, 0x0a, 0x07, + 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x1a, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x61, 0x0a, 0x12, 0x47, 0x65, 0x74, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x1d, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, 0x12, 0x57, 0x0a, 0x0d, + 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, + 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x73, 0x12, 0x70, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01, 0x2a, 0x22, + 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x2f, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x12, + 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x62, + 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, + 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, + 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, + 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x76, 0x31, + 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2f, 0x70, 0x61, + 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, + 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -223,41 +493,47 @@ func file_v1_service_proto_rawDescGZIP() []byte { return file_v1_service_proto_rawDescData } -var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_v1_service_proto_goTypes = []interface{}{ - (*ListSnapshotsRequest)(nil), // 0: v1.ListSnapshotsRequest - (*GetOperationsRequest)(nil), // 1: v1.GetOperationsRequest - (*emptypb.Empty)(nil), // 2: google.protobuf.Empty - (*Config)(nil), // 3: v1.Config - (*Repo)(nil), // 4: v1.Repo - (*types.StringValue)(nil), // 5: types.StringValue - (*OperationEvent)(nil), // 6: v1.OperationEvent - (*OperationList)(nil), // 7: v1.OperationList - (*ResticSnapshotList)(nil), // 8: v1.ResticSnapshotList - (*types.StringList)(nil), // 9: types.StringList + (*ListSnapshotsRequest)(nil), // 0: v1.ListSnapshotsRequest + (*GetOperationsRequest)(nil), // 1: v1.GetOperationsRequest + (*ListSnapshotFilesRequest)(nil), // 2: v1.ListSnapshotFilesRequest + (*ListSnapshotFilesResponse)(nil), // 3: v1.ListSnapshotFilesResponse + (*LsEntry)(nil), // 4: v1.LsEntry + (*emptypb.Empty)(nil), // 5: google.protobuf.Empty + (*Config)(nil), // 6: v1.Config + (*Repo)(nil), // 7: v1.Repo + (*types.StringValue)(nil), // 8: types.StringValue + (*OperationEvent)(nil), // 9: v1.OperationEvent + (*OperationList)(nil), // 10: v1.OperationList + (*ResticSnapshotList)(nil), // 11: v1.ResticSnapshotList + (*types.StringList)(nil), // 12: types.StringList } var file_v1_service_proto_depIdxs = []int32{ - 2, // 0: v1.ResticUI.GetConfig:input_type -> google.protobuf.Empty - 3, // 1: v1.ResticUI.SetConfig:input_type -> v1.Config - 4, // 2: v1.ResticUI.AddRepo:input_type -> v1.Repo - 2, // 3: v1.ResticUI.GetOperationEvents:input_type -> google.protobuf.Empty - 1, // 4: v1.ResticUI.GetOperations:input_type -> v1.GetOperationsRequest - 0, // 5: v1.ResticUI.ListSnapshots:input_type -> v1.ListSnapshotsRequest - 5, // 6: v1.ResticUI.Backup:input_type -> types.StringValue - 5, // 7: v1.ResticUI.PathAutocomplete:input_type -> types.StringValue - 3, // 8: v1.ResticUI.GetConfig:output_type -> v1.Config - 3, // 9: v1.ResticUI.SetConfig:output_type -> v1.Config - 3, // 10: v1.ResticUI.AddRepo:output_type -> v1.Config - 6, // 11: v1.ResticUI.GetOperationEvents:output_type -> v1.OperationEvent - 7, // 12: v1.ResticUI.GetOperations:output_type -> v1.OperationList - 8, // 13: v1.ResticUI.ListSnapshots:output_type -> v1.ResticSnapshotList - 2, // 14: v1.ResticUI.Backup:output_type -> google.protobuf.Empty - 9, // 15: v1.ResticUI.PathAutocomplete:output_type -> types.StringList - 8, // [8:16] is the sub-list for method output_type - 0, // [0:8] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 4, // 0: v1.ListSnapshotFilesResponse.entries:type_name -> v1.LsEntry + 5, // 1: v1.ResticUI.GetConfig:input_type -> google.protobuf.Empty + 6, // 2: v1.ResticUI.SetConfig:input_type -> v1.Config + 7, // 3: v1.ResticUI.AddRepo:input_type -> v1.Repo + 5, // 4: v1.ResticUI.GetOperationEvents:input_type -> google.protobuf.Empty + 1, // 5: v1.ResticUI.GetOperations:input_type -> v1.GetOperationsRequest + 0, // 6: v1.ResticUI.ListSnapshots:input_type -> v1.ListSnapshotsRequest + 2, // 7: v1.ResticUI.ListSnapshotFiles:input_type -> v1.ListSnapshotFilesRequest + 8, // 8: v1.ResticUI.Backup:input_type -> types.StringValue + 8, // 9: v1.ResticUI.PathAutocomplete:input_type -> types.StringValue + 6, // 10: v1.ResticUI.GetConfig:output_type -> v1.Config + 6, // 11: v1.ResticUI.SetConfig:output_type -> v1.Config + 6, // 12: v1.ResticUI.AddRepo:output_type -> v1.Config + 9, // 13: v1.ResticUI.GetOperationEvents:output_type -> v1.OperationEvent + 10, // 14: v1.ResticUI.GetOperations:output_type -> v1.OperationList + 11, // 15: v1.ResticUI.ListSnapshots:output_type -> v1.ResticSnapshotList + 3, // 16: v1.ResticUI.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse + 5, // 17: v1.ResticUI.Backup:output_type -> google.protobuf.Empty + 12, // 18: v1.ResticUI.PathAutocomplete:output_type -> types.StringList + 10, // [10:19] is the sub-list for method output_type + 1, // [1:10] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_v1_service_proto_init() } @@ -293,6 +569,42 @@ func file_v1_service_proto_init() { return nil } } + file_v1_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListSnapshotFilesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v1_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListSnapshotFilesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v1_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LsEntry); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -300,7 +612,7 @@ func file_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v1_service_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/go/v1/service.pb.gw.go b/gen/go/v1/service.pb.gw.go index 9df2ef9ec..61ab461c6 100644 --- a/gen/go/v1/service.pb.gw.go +++ b/gen/go/v1/service.pb.gw.go @@ -204,6 +204,40 @@ func local_request_ResticUI_ListSnapshots_0(ctx context.Context, marshaler runti } +func request_ResticUI_ListSnapshotFiles_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListSnapshotFilesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.ListSnapshotFiles(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_ResticUI_ListSnapshotFiles_0(ctx context.Context, marshaler runtime.Marshaler, server ResticUIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListSnapshotFilesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.ListSnapshotFiles(ctx, &protoReq) + return msg, metadata, err + +} + func request_ResticUI_Backup_0(ctx context.Context, marshaler runtime.Marshaler, client ResticUIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq types.StringValue var metadata runtime.ServerMetadata @@ -410,6 +444,31 @@ func RegisterResticUIHandlerServer(ctx context.Context, mux *runtime.ServeMux, s }) + mux.Handle("POST", pattern_ResticUI_ListSnapshotFiles_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/v1.ResticUI/ListSnapshotFiles", runtime.WithHTTPPathPattern("/v1/snapshots/files")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ResticUI_ListSnapshotFiles_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ResticUI_ListSnapshotFiles_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_ResticUI_Backup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -633,6 +692,28 @@ func RegisterResticUIHandlerClient(ctx context.Context, mux *runtime.ServeMux, c }) + mux.Handle("POST", pattern_ResticUI_ListSnapshotFiles_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/v1.ResticUI/ListSnapshotFiles", runtime.WithHTTPPathPattern("/v1/snapshots/files")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ResticUI_ListSnapshotFiles_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_ResticUI_ListSnapshotFiles_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_ResticUI_Backup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -693,6 +774,8 @@ var ( pattern_ResticUI_ListSnapshots_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "snapshots"}, "")) + pattern_ResticUI_ListSnapshotFiles_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "snapshots", "files"}, "")) + pattern_ResticUI_Backup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "cmd", "backup"}, "")) pattern_ResticUI_PathAutocomplete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "autocomplete", "path"}, "")) @@ -711,6 +794,8 @@ var ( forward_ResticUI_ListSnapshots_0 = runtime.ForwardResponseMessage + forward_ResticUI_ListSnapshotFiles_0 = runtime.ForwardResponseMessage + forward_ResticUI_Backup_0 = runtime.ForwardResponseMessage forward_ResticUI_PathAutocomplete_0 = runtime.ForwardResponseMessage diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index 4daf3f431..e28d4f2a0 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -27,6 +27,7 @@ const ( ResticUI_GetOperationEvents_FullMethodName = "/v1.ResticUI/GetOperationEvents" ResticUI_GetOperations_FullMethodName = "/v1.ResticUI/GetOperations" ResticUI_ListSnapshots_FullMethodName = "/v1.ResticUI/ListSnapshots" + ResticUI_ListSnapshotFiles_FullMethodName = "/v1.ResticUI/ListSnapshotFiles" ResticUI_Backup_FullMethodName = "/v1.ResticUI/Backup" ResticUI_PathAutocomplete_FullMethodName = "/v1.ResticUI/PathAutocomplete" ) @@ -41,6 +42,7 @@ type ResticUIClient interface { GetOperationEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ResticUI_GetOperationEventsClient, error) GetOperations(ctx context.Context, in *GetOperationsRequest, opts ...grpc.CallOption) (*OperationList, error) ListSnapshots(ctx context.Context, in *ListSnapshotsRequest, opts ...grpc.CallOption) (*ResticSnapshotList, error) + ListSnapshotFiles(ctx context.Context, in *ListSnapshotFilesRequest, opts ...grpc.CallOption) (*ListSnapshotFilesResponse, error) // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. Backup(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) // PathAutocomplete provides path autocompletion options for a given filesystem path. @@ -132,6 +134,15 @@ func (c *resticUIClient) ListSnapshots(ctx context.Context, in *ListSnapshotsReq return out, nil } +func (c *resticUIClient) ListSnapshotFiles(ctx context.Context, in *ListSnapshotFilesRequest, opts ...grpc.CallOption) (*ListSnapshotFilesResponse, error) { + out := new(ListSnapshotFilesResponse) + err := c.cc.Invoke(ctx, ResticUI_ListSnapshotFiles_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *resticUIClient) Backup(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, ResticUI_Backup_FullMethodName, in, out, opts...) @@ -160,6 +171,7 @@ type ResticUIServer interface { GetOperationEvents(*emptypb.Empty, ResticUI_GetOperationEventsServer) error GetOperations(context.Context, *GetOperationsRequest) (*OperationList, error) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ResticSnapshotList, error) + ListSnapshotFiles(context.Context, *ListSnapshotFilesRequest) (*ListSnapshotFilesResponse, error) // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. Backup(context.Context, *types.StringValue) (*emptypb.Empty, error) // PathAutocomplete provides path autocompletion options for a given filesystem path. @@ -189,6 +201,9 @@ func (UnimplementedResticUIServer) GetOperations(context.Context, *GetOperations func (UnimplementedResticUIServer) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ResticSnapshotList, error) { return nil, status.Errorf(codes.Unimplemented, "method ListSnapshots not implemented") } +func (UnimplementedResticUIServer) ListSnapshotFiles(context.Context, *ListSnapshotFilesRequest) (*ListSnapshotFilesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListSnapshotFiles not implemented") +} func (UnimplementedResticUIServer) Backup(context.Context, *types.StringValue) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Backup not implemented") } @@ -319,6 +334,24 @@ func _ResticUI_ListSnapshots_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _ResticUI_ListSnapshotFiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSnapshotFilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResticUIServer).ListSnapshotFiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResticUI_ListSnapshotFiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResticUIServer).ListSnapshotFiles(ctx, req.(*ListSnapshotFilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ResticUI_Backup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(types.StringValue) if err := dec(in); err != nil { @@ -382,6 +415,10 @@ var ResticUI_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListSnapshots", Handler: _ResticUI_ListSnapshots_Handler, }, + { + MethodName: "ListSnapshotFiles", + Handler: _ResticUI_ListSnapshotFiles_Handler, + }, { MethodName: "Backup", Handler: _ResticUI_Backup_Handler, diff --git a/internal/api/server.go b/internal/api/server.go index e88091a39..c093f215a 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -130,6 +130,23 @@ func (s *Server) ListSnapshots(ctx context.Context, query *v1.ListSnapshotsReque }, nil } +func (s *Server) ListSnapshotFiles(ctx context.Context, query *v1.ListSnapshotFilesRequest) (*v1.ListSnapshotFilesResponse, error) { + repo, err := s.orchestrator.GetRepo(query.RepoId) + if err != nil { + return nil, fmt.Errorf("failed to get repo: %w", err) + } + + entries, err := repo.ListSnapshotFiles(ctx, query.SnapshotId, query.Path) + if err != nil { + return nil, fmt.Errorf("failed to list snapshot files: %w", err) + } + + return &v1.ListSnapshotFilesResponse{ + Path: query.Path, + Entries: entries, + }, nil +} + // GetOperationEvents implements GET /v1/events/operations func (s *Server) GetOperationEvents(_ *emptypb.Empty, stream v1.ResticUI_GetOperationEventsServer) error { errorChan := make(chan error) diff --git a/internal/orchestrator/repo.go b/internal/orchestrator/repo.go index 84a9dede9..99c27c998 100644 --- a/internal/orchestrator/repo.go +++ b/internal/orchestrator/repo.go @@ -29,7 +29,7 @@ func newRepoOrchestrator(repoConfig *v1.Repo, repo *restic.Repo) *RepoOrchestrat } func (r *RepoOrchestrator) Snapshots(ctx context.Context) ([]*restic.Snapshot, error) { - snapshots, err := r.repo.Snapshots(ctx, restic.WithPropagatedEnvVars(restic.EnvToPropagate...), restic.WithFlags("--latest", "1000")) + snapshots, err := r.repo.Snapshots(ctx, restic.WithFlags("--latest", "1000")) if err != nil { return nil, fmt.Errorf("restic.Snapshots: %w", err) } @@ -83,6 +83,23 @@ func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCa return summary, nil } +func (r *RepoOrchestrator) ListSnapshotFiles(ctx context.Context, snapshotId string, path string) ([]*v1.LsEntry, error) { + r.mu.Lock() + defer r.mu.Unlock() + + _, entries, err := r.repo.ListDirectory(ctx, snapshotId, path) + if err != nil { + return nil, fmt.Errorf("failed to list snapshot files: %w", err) + } + + lsEnts := make([]*v1.LsEntry, 0, len(entries)) + for _, entry := range entries { + lsEnts = append(lsEnts, entry.ToProto()) + } + + return lsEnts, nil +} + func filterSnapshotsForPlan(snapshots []*restic.Snapshot, plan *v1.Plan) []*restic.Snapshot { wantTag := tagForPlan(plan) var filtered []*restic.Snapshot diff --git a/internal/orchestrator/tasks.go b/internal/orchestrator/tasks.go index 2adeb51b2..3c6934015 100644 --- a/internal/orchestrator/tasks.go +++ b/internal/orchestrator/tasks.go @@ -30,7 +30,7 @@ type ScheduledBackupTask struct { var _ Task = &ScheduledBackupTask{} func NewScheduledBackupTask(orchestrator *Orchestrator, plan *v1.Plan) (*ScheduledBackupTask, error) { - sched, err := cronexpr.Parse(plan.Cron) + sched, err := cronexpr.ParseInLocation(plan.Cron, time.Now().Location().String()) if err != nil { return nil, fmt.Errorf("failed to parse schedule %q: %w", plan.Cron, err) } @@ -160,7 +160,6 @@ func indexSnapshotsHelper(ctx context.Context, orchestrator *Orchestrator, plan opTime := curTimeMillis() var indexOps []*v1.Operation for _, snapshot := range snapshots { - zap.L().Debug("checking if snapshot has been indexed", zap.String("snapshot", snapshot.Id)) opid, err := orchestrator.oplog.HasIndexedSnapshot(snapshot.Id) if err != nil { return fmt.Errorf("HasIndexSnapshot for snapshot %q: %w", snapshot.Id, err) diff --git a/pkg/restic/outputs.go b/pkg/restic/outputs.go index c2df020bc..53e2ba873 100644 --- a/pkg/restic/outputs.go +++ b/pkg/restic/outputs.go @@ -26,6 +26,21 @@ type LsEntry struct { Ctime string `json:"ctime"` } +func (e *LsEntry) ToProto() *v1.LsEntry { + return &v1.LsEntry{ + Name: e.Name, + Type: e.Type, + Path: e.Path, + Uid: int64(e.Uid), + Gid: int64(e.Gid), + Size: int64(e.Size), + Mode: int64(e.Mode), + Mtime: e.Mtime, + Atime: e.Atime, + Ctime: e.Ctime, + } +} + type Snapshot struct { Id string `json:"id"` Time string `json:"time"` diff --git a/pkg/restic/restic.go b/pkg/restic/restic.go index 3e297c44e..5f8633f0d 100644 --- a/pkg/restic/restic.go +++ b/pkg/restic/restic.go @@ -208,7 +208,6 @@ func (r *Repo) ListDirectory(ctx context.Context, snapshot string, path string, return nil, nil, NewCmdError(cmd, output, err) } - snapshots, entries, err := readLs(bytes.NewBuffer(output)) if err != nil { return nil, nil, NewCmdError(cmd, output, err) diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 0c39b0410..180f3a1e5 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -51,6 +51,13 @@ service ResticUI { body: "*" }; } + + rpc ListSnapshotFiles(ListSnapshotFilesRequest) returns (ListSnapshotFilesResponse) { + option (google.api.http) = { + post: "/v1/snapshots/files" + body: "*" + }; + } // Backup schedules a backup operation. It accepts a plan id and returns empty if the task is enqueued. rpc Backup(types.StringValue) returns (google.protobuf.Empty) { @@ -79,3 +86,27 @@ message GetOperationsRequest { string plan_id = 2; int64 last_n = 3; // limit to the last n operations } + +message ListSnapshotFilesRequest { + string repo_id = 1; + string snapshot_id = 2; + string path = 3; +} + +message ListSnapshotFilesResponse { + string path = 1; + repeated LsEntry entries = 2; +} + +message LsEntry { + string name = 1; + string type = 2; + string path = 3; + int64 uid = 4; + int64 gid = 5; + int64 size = 6; + int64 mode = 7; + string mtime = 8; + string atime = 9; + string ctime = 10; +} \ No newline at end of file diff --git a/webui/gen/ts/v1/service.pb.ts b/webui/gen/ts/v1/service.pb.ts index 06cfdab3a..8dc10d966 100644 --- a/webui/gen/ts/v1/service.pb.ts +++ b/webui/gen/ts/v1/service.pb.ts @@ -21,6 +21,30 @@ export type GetOperationsRequest = { lastN?: string } +export type ListSnapshotFilesRequest = { + repoId?: string + snapshotId?: string + path?: string +} + +export type ListSnapshotFilesResponse = { + path?: string + entries?: LsEntry[] +} + +export type LsEntry = { + name?: string + type?: string + path?: string + uid?: string + gid?: string + size?: string + mode?: string + mtime?: string + atime?: string + ctime?: string +} + export class ResticUI { static GetConfig(req: GoogleProtobufEmpty.Empty, initReq?: fm.InitReq): Promise { return fm.fetchReq(`/v1/config?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"}) @@ -40,6 +64,9 @@ export class ResticUI { static ListSnapshots(req: ListSnapshotsRequest, initReq?: fm.InitReq): Promise { return fm.fetchReq(`/v1/snapshots`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) } + static ListSnapshotFiles(req: ListSnapshotFilesRequest, initReq?: fm.InitReq): Promise { + return fm.fetchReq(`/v1/snapshots/files`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) + } static Backup(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise { return fm.fetchReq(`/v1/cmd/backup`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) } diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index f55e89148..09789af88 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -1,16 +1,23 @@ import React from "react"; import { Operation, OperationStatus } from "../../gen/ts/v1/operations.pb"; -import { Col, Collapse, Empty, List, Progress, Row, Typography } from "antd"; import { - AlertOutlined, - DatabaseOutlined, + Card, + Col, + Collapse, + Empty, + List, + Progress, + Row, + Typography, +} from "antd"; +import { ExclamationCircleOutlined, - ExclamationOutlined, PaperClipOutlined, SaveOutlined, } from "@ant-design/icons"; import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb"; import { EOperation } from "../state/oplog"; +import { SnapshotBrowser } from "./SnapshotBrowser"; export const OperationList = ({ operations, @@ -26,14 +33,60 @@ export const OperationList = ({ ); } + const groupBy = (ops: EOperation[], keyFunc: (op: EOperation) => string) => { + const groups: { [key: string]: EOperation[] } = {}; + + ops.forEach((op) => { + const key = keyFunc(op); + if (!(key in groups)) { + groups[key] = []; + } + groups[key].push(op); + }); + + return Object.values(groups); + }; + + // snapshotKey is a heuristic that tries to find a snapshot ID to group the operation by, + // if one can not be found the operation ID is the key. + const snapshotKey = (op: EOperation) => { + if ( + op.operationBackup && + op.operationBackup.lastStatus && + op.operationBackup.lastStatus.summary + ) { + return normalizeSnapshotId( + op.operationBackup.lastStatus.summary.snapshotId! + ); + } else if (op.operationIndexSnapshot) { + return normalizeSnapshotId(op.operationIndexSnapshot.snapshot!.id!); + } + return op.id!; + }; + + const groupedItems = groupBy(operations, snapshotKey); + groupedItems.sort((a, b) => { + return b[0].parsedTime - a[0].parsedTime; + }); + return ( ( - - )} + dataSource={groupedItems} + renderItem={(group, index) => { + if (group.length === 1) { + return ; + } + + return ( + + {group.map((op) => ( + + ))} + + ); + }} pagination={ operations.length > 50 ? { position: "both", align: "center", defaultPageSize: 50 } @@ -55,11 +108,25 @@ export const OperationRow = ({ color = "blue"; } - if (operation.displayMessage) { + if ( + operation.displayMessage && + operation.status === OperationStatus.STATUS_ERROR + ) { + let opType = "Message"; + if (operation.operationBackup) { + opType = "Backup"; + } else if (operation.operationIndexSnapshot) { + opType = "Snapshot"; + } + return ( Message} + title={ + <> + {opType} Error at {formatTime(operation.unixTimeStartMs!)} + + } avatar={} description={operation.displayMessage} /> @@ -120,14 +187,25 @@ export const OperationRow = ({ <>Snapshot at {formatTime(snapshotOp.snapshot!.unixTimeMs!)} } avatar={} - description={} + description={ + + } /> ); } }; -const SnapshotInfo = ({ snapshot }: { snapshot: ResticSnapshot }) => { +const SnapshotInfo = ({ + snapshot, + repoId, +}: { + snapshot: ResticSnapshot; + repoId: string; +}) => { return ( { <> Snapshot ID: - {snapshot.id?.substring(0, 8)} + {normalizeSnapshotId(snapshot.id!)} @@ -164,7 +242,9 @@ const SnapshotInfo = ({ snapshot }: { snapshot: ResticSnapshot }) => { { key: 2, label: "Browse", - children: null, + children: ( + + ), }, ]} /> @@ -277,7 +357,10 @@ const formatTime = (time: number | string) => { } const d = new Date(); d.setTime(time); - return d.toISOString(); + return d.toLocaleString(undefined, { + dateStyle: "short", + timeStyle: "long", + }); }; const formatDuration = (ms: number) => { @@ -290,4 +373,8 @@ const formatDuration = (ms: number) => { return `${minutes}m${seconds % 60}s`; } return `${hours}h${minutes % 60}m${seconds % 60}s`; -}; \ No newline at end of file +}; + +const normalizeSnapshotId = (id: string) => { + return id.substring(0, 8); +}; diff --git a/webui/src/components/SnapshotBrowser.tsx b/webui/src/components/SnapshotBrowser.tsx new file mode 100644 index 000000000..9be49c98d --- /dev/null +++ b/webui/src/components/SnapshotBrowser.tsx @@ -0,0 +1,159 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Input, Tree } from "antd"; +import type { DataNode, EventDataNode } from "antd/es/tree"; +import { + ListSnapshotFilesResponse, + LsEntry, + ResticUI, +} from "../../gen/ts/v1/service.pb"; +import { useAlertApi } from "./Alerts"; +import { FileOutlined, FolderOutlined } from "@ant-design/icons"; + +type ELsEntry = LsEntry & { children?: ELsEntry[] }; + +// replaceKeyInTree returns a value only if changes are made. +const replaceKeyInTree = ( + curNode: DataNode, + setKey: string, + setValue: DataNode +): DataNode | null => { + if (curNode.key === setKey) { + return setValue; + } + if (!curNode.children) { + return null; + } + for (const idx in curNode.children!) { + const child = curNode.children![idx]; + const newChild = replaceKeyInTree(child, setKey, setValue); + if (newChild) { + const curNodeCopy = { ...curNode }; + curNodeCopy.children = [...curNode.children!]; + curNodeCopy.children[idx] = newChild; + return curNodeCopy; + } + } + return null; +}; +const findInTree = (curNode: DataNode, key: string): DataNode | null => { + if (curNode.key === key) { + return curNode; + } + + if (!curNode.children) { + return null; + } + + for (const child of curNode.children) { + const found = findInTree(child, key); + if (found) { + return found; + } + } + return null; +}; + +export const SnapshotBrowser = ({ + repoId, + snapshotId, +}: React.PropsWithoutRef<{ snapshotId: string; repoId: string }>) => { + const alertApi = useAlertApi(); + const [treeData, setTreeData] = useState([]); + + useEffect(() => { + (async () => { + try { + const resp = await ResticUI.ListSnapshotFiles( + { + path: "/", + repoId, + snapshotId, + }, + { pathPrefix: "/api" } + ); + setTreeData(respToNodes(resp)); + } catch (e: any) { + alertApi?.error("Failed to list snapshot files: " + e.message); + } + })(); + }, [repoId, snapshotId]); + + const onLoadData = async ({ key, children }: EventDataNode) => { + if (children) { + return; + } + + console.log("Loading data for key: " + key); + + const resp = await ResticUI.ListSnapshotFiles( + { + path: (key + "/") as string, + repoId, + snapshotId, + }, + { pathPrefix: "/api" } + ); + + let toUpdate: DataNode | null = null; + for (const node of treeData) { + toUpdate = findInTree(node, key as string); + if (toUpdate) { + break; + } + } + + if (!toUpdate) { + console.log("No node to update found!"); + return; + } + + const toUpdateCopy = { ...toUpdate }; + toUpdateCopy.children = respToNodes(resp); + + console.log( + "Replacing key: " + + key + + " with: " + + JSON.stringify(toUpdateCopy, null, 2) + ); + console.log("In tree: " + JSON.stringify(treeData, null, 2)); + + const newTree = treeData.map((node) => { + console.log("trying replace in tree..."); + const didUpdate = replaceKeyInTree(node, key as string, toUpdateCopy); + if (didUpdate) { + console.log("Replaced in tree successfully!"); + return didUpdate; + } + return node; + }); + + console.log("New tree: ", JSON.stringify(newTree, null, 2)); + + setTreeData(newTree); + }; + + return loadData={onLoadData} treeData={treeData} />; +}; + +const respToNodes = (resp: ListSnapshotFilesResponse): DataNode[] => { + const nodes = resp + .entries!.filter((entry) => entry.path!.length > resp.path!.length) + .map((entry) => { + const lastSlash = entry.path!.lastIndexOf("/"); + const title = + lastSlash === -1 ? entry.path : entry.path!.slice(lastSlash + 1); + + const node: DataNode = { + key: entry.path!, + title: title, + isLeaf: entry.type === "file", + icon: entry.type === "file" ? : , + }; + return node; + }); + + console.log(JSON.stringify(nodes, null, 2)); + + return nodes; +};