feat: add rollback and virtual list

This commit is contained in:
Samuel N Cui
2023-10-12 22:54:07 +08:00
parent 7a2fb83d36
commit 59f626cef5
44 changed files with 3170 additions and 2560 deletions
-4
View File
@@ -16,9 +16,5 @@ func (api *API) JobCreate(ctx context.Context, req *entity.JobCreateRequest) (*e
return nil, err
}
if err := api.exe.Start(ctx, job); err != nil {
return nil, err
}
return &entity.JobCreateReply{Job: convertJobs(job)[0]}, nil
}
+44
View File
@@ -0,0 +1,44 @@
package apis
import (
"context"
"fmt"
"github.com/samuelncui/yatm/entity"
)
func (api *API) JobEditState(ctx context.Context, req *entity.JobEditStateRequest) (*entity.JobEditStateReply, error) {
job, err := api.exe.GetJob(ctx, req.Id)
if err != nil {
return nil, err
}
if job == nil {
return nil, fmt.Errorf("job not found, id= %d", req.Id)
}
if job.Status == entity.JobStatus_PROCESSING {
return nil, fmt.Errorf("job status 'PROCESSING' is unexpected")
}
if req.Status != nil {
if *req.Status == entity.JobStatus_PROCESSING {
return nil, fmt.Errorf("job target status 'PROCESSING' is unexpected")
}
job.Status = *req.Status
}
job.State = req.State
if _, err := api.exe.SaveJob(ctx, job); err != nil {
return nil, fmt.Errorf("save job fail, %w", err)
}
executor, err := api.exe.GetJobExecutor(ctx, job.ID)
if err != nil {
return nil, fmt.Errorf("get job executor fail, %w", err)
}
if err := executor.Close(ctx); err != nil {
return nil, fmt.Errorf("close job executor fail, %w", err)
}
return &entity.JobEditStateReply{}, nil
}
+3 -8
View File
@@ -6,15 +6,10 @@ import (
"github.com/samuelncui/yatm/entity"
)
func (api *API) JobNext(ctx context.Context, req *entity.JobNextRequest) (*entity.JobNextReply, error) {
job, err := api.exe.GetJob(ctx, req.Id)
if err != nil {
func (api *API) JobDispatch(ctx context.Context, req *entity.JobDispatchRequest) (*entity.JobDispatchReply, error) {
if err := api.exe.Dispatch(ctx, req.Id, req.Param); err != nil {
return nil, err
}
if err := api.exe.Submit(ctx, job, req.Param); err != nil {
return nil, err
}
return &entity.JobNextReply{Job: convertJobs(job)[0]}, nil
return &entity.JobDispatchReply{}, nil
}
+84 -83
View File
@@ -328,19 +328,19 @@ func (*JobState_Archive) isJobState_State() {}
func (*JobState_Restore) isJobState_State() {}
type JobNextParam struct {
type JobDispatchParam struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to Param:
// *JobNextParam_Archive
// *JobNextParam_Restore
Param isJobNextParam_Param `protobuf_oneof:"param"`
// *JobDispatchParam_Archive
// *JobDispatchParam_Restore
Param isJobDispatchParam_Param `protobuf_oneof:"param"`
}
func (x *JobNextParam) Reset() {
*x = JobNextParam{}
func (x *JobDispatchParam) Reset() {
*x = JobDispatchParam{}
if protoimpl.UnsafeEnabled {
mi := &file_job_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -348,13 +348,13 @@ func (x *JobNextParam) Reset() {
}
}
func (x *JobNextParam) String() string {
func (x *JobDispatchParam) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*JobNextParam) ProtoMessage() {}
func (*JobDispatchParam) ProtoMessage() {}
func (x *JobNextParam) ProtoReflect() protoreflect.Message {
func (x *JobDispatchParam) ProtoReflect() protoreflect.Message {
mi := &file_job_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -366,47 +366,47 @@ func (x *JobNextParam) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use JobNextParam.ProtoReflect.Descriptor instead.
func (*JobNextParam) Descriptor() ([]byte, []int) {
// Deprecated: Use JobDispatchParam.ProtoReflect.Descriptor instead.
func (*JobDispatchParam) Descriptor() ([]byte, []int) {
return file_job_proto_rawDescGZIP(), []int{3}
}
func (m *JobNextParam) GetParam() isJobNextParam_Param {
func (m *JobDispatchParam) GetParam() isJobDispatchParam_Param {
if m != nil {
return m.Param
}
return nil
}
func (x *JobNextParam) GetArchive() *JobArchiveNextParam {
if x, ok := x.GetParam().(*JobNextParam_Archive); ok {
func (x *JobDispatchParam) GetArchive() *JobArchiveDispatchParam {
if x, ok := x.GetParam().(*JobDispatchParam_Archive); ok {
return x.Archive
}
return nil
}
func (x *JobNextParam) GetRestore() *JobRestoreNextParam {
if x, ok := x.GetParam().(*JobNextParam_Restore); ok {
func (x *JobDispatchParam) GetRestore() *JobRestoreDispatchParam {
if x, ok := x.GetParam().(*JobDispatchParam_Restore); ok {
return x.Restore
}
return nil
}
type isJobNextParam_Param interface {
isJobNextParam_Param()
type isJobDispatchParam_Param interface {
isJobDispatchParam_Param()
}
type JobNextParam_Archive struct {
Archive *JobArchiveNextParam `protobuf:"bytes,1,opt,name=archive,proto3,oneof"`
type JobDispatchParam_Archive struct {
Archive *JobArchiveDispatchParam `protobuf:"bytes,1,opt,name=archive,proto3,oneof"`
}
type JobNextParam_Restore struct {
Restore *JobRestoreNextParam `protobuf:"bytes,2,opt,name=restore,proto3,oneof"`
type JobDispatchParam_Restore struct {
Restore *JobRestoreDispatchParam `protobuf:"bytes,2,opt,name=restore,proto3,oneof"`
}
func (*JobNextParam_Archive) isJobNextParam_Param() {}
func (*JobDispatchParam_Archive) isJobDispatchParam_Param() {}
func (*JobNextParam_Restore) isJobNextParam_Param() {}
func (*JobDispatchParam_Restore) isJobDispatchParam_Param() {}
type CreatableJob struct {
state protoimpl.MessageState
@@ -697,57 +697,58 @@ var file_job_proto_rawDesc = []byte{
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74,
0x6f, 0x72, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x74,
0x61, 0x74, 0x65, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x42, 0x07,
0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x93, 0x01, 0x0a, 0x0c, 0x4a, 0x6f, 0x62, 0x4e,
0x65, 0x78, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x3c, 0x0a, 0x07, 0x61, 0x72, 0x63, 0x68,
0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6a, 0x6f, 0x62, 0x5f,
0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69,
0x76, 0x65, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x07, 0x61,
0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x3c, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x73,
0x74, 0x6f, 0x72, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x4f, 0x0a,
0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, 0x0a,
0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x23, 0x0a, 0x05, 0x70, 0x61, 0x72,
0x61, 0x6d, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6a, 0x6f, 0x62, 0x2e, 0x4a,
0x6f, 0x62, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x52, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x90,
0x01, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x6a,
0x6f, 0x62, 0x2e, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x6c, 0x69, 0x6d,
0x69, 0x74, 0x18, 0x21, 0x20, 0x01, 0x28, 0x03, 0x48, 0x01, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69,
0x74, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x22,
0x20, 0x01, 0x28, 0x03, 0x48, 0x02, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x88, 0x01,
0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x08, 0x0a, 0x06,
0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65,
0x74, 0x22, 0x7f, 0x0a, 0x17, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x63, 0x65, 0x6e, 0x74, 0x6c, 0x79,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x0f,
0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x73, 0x18,
0x01, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53,
0x69, 0x6e, 0x63, 0x65, 0x4e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x6c, 0x69, 0x6d,
0x69, 0x74, 0x18, 0x21, 0x20, 0x01, 0x28, 0x03, 0x48, 0x01, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69,
0x74, 0x88, 0x01, 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f,
0x73, 0x69, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x6c, 0x69, 0x6d,
0x69, 0x74, 0x22, 0x8f, 0x01, 0x0a, 0x0a, 0x4a, 0x6f, 0x62, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61,
0x79, 0x12, 0x3a, 0x0a, 0x07, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65,
0x2e, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c,
0x61, 0x79, 0x48, 0x00, 0x52, 0x07, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x3a, 0x0a,
0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e,
0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x4a, 0x6f, 0x62,
0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x48, 0x00,
0x52, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x64, 0x69, 0x73,
0x70, 0x6c, 0x61, 0x79, 0x2a, 0x6b, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75,
0x73, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x52, 0x41, 0x46, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09,
0x4e, 0x4f, 0x54, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x50,
0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x50, 0x52, 0x4f, 0x43,
0x45, 0x53, 0x53, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4d, 0x50,
0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45,
0x44, 0x10, 0x7f, 0x12, 0x0c, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0xff,
0x01, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
0x73, 0x61, 0x6d, 0x75, 0x65, 0x6c, 0x6e, 0x63, 0x75, 0x69, 0x2f, 0x79, 0x61, 0x74, 0x6d, 0x2f,
0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x10, 0x4a, 0x6f, 0x62, 0x44,
0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x40, 0x0a, 0x07,
0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e,
0x6a, 0x6f, 0x62, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x41,
0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x50, 0x61,
0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x07, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x40,
0x0a, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x24, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x4a, 0x6f,
0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68,
0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
0x42, 0x07, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x4f, 0x0a, 0x0c, 0x43, 0x72, 0x65,
0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69,
0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x72, 0x69,
0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x23, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x18, 0x11,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6a, 0x6f, 0x62, 0x2e, 0x4a, 0x6f, 0x62, 0x50, 0x61,
0x72, 0x61, 0x6d, 0x52, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x90, 0x01, 0x0a, 0x09, 0x4a,
0x6f, 0x62, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x6a, 0x6f, 0x62, 0x2e, 0x4a,
0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74,
0x75, 0x73, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x21,
0x20, 0x01, 0x28, 0x03, 0x48, 0x01, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x88, 0x01, 0x01,
0x12, 0x1b, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x22, 0x20, 0x01, 0x28, 0x03,
0x48, 0x02, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a,
0x07, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x6c, 0x69, 0x6d,
0x69, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x22, 0x7f, 0x0a,
0x17, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x63, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x55, 0x70, 0x64, 0x61,
0x74, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x0f, 0x75, 0x70, 0x64, 0x61,
0x74, 0x65, 0x5f, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
0x03, 0x48, 0x00, 0x52, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65,
0x4e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x21,
0x20, 0x01, 0x28, 0x03, 0x48, 0x01, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x88, 0x01, 0x01,
0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x69, 0x6e, 0x63,
0x65, 0x5f, 0x6e, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x8f,
0x01, 0x0a, 0x0a, 0x4a, 0x6f, 0x62, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x12, 0x3a, 0x0a,
0x07, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e,
0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x4a, 0x6f, 0x62,
0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x48, 0x00,
0x52, 0x07, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x3a, 0x0a, 0x07, 0x72, 0x65, 0x73,
0x74, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6a, 0x6f, 0x62,
0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74,
0x6f, 0x72, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79,
0x2a, 0x6b, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x09, 0x0a,
0x05, 0x44, 0x52, 0x41, 0x46, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x4f, 0x54, 0x5f,
0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, 0x44, 0x49,
0x4e, 0x47, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x49,
0x4e, 0x47, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45,
0x44, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x7f, 0x12,
0x0c, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0xff, 0x01, 0x42, 0x23, 0x5a,
0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x61, 0x6d, 0x75,
0x65, 0x6c, 0x6e, 0x63, 0x75, 0x69, 0x2f, 0x79, 0x61, 0x74, 0x6d, 0x2f, 0x65, 0x6e, 0x74, 0x69,
0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -769,7 +770,7 @@ var file_job_proto_goTypes = []interface{}{
(*Job)(nil), // 1: job.Job
(*JobParam)(nil), // 2: job.JobParam
(*JobState)(nil), // 3: job.JobState
(*JobNextParam)(nil), // 4: job.JobNextParam
(*JobDispatchParam)(nil), // 4: job.JobDispatchParam
(*CreatableJob)(nil), // 5: job.CreatableJob
(*JobFilter)(nil), // 6: job.JobFilter
(*JobRecentlyUpdateFilter)(nil), // 7: job.JobRecentlyUpdateFilter
@@ -778,8 +779,8 @@ var file_job_proto_goTypes = []interface{}{
(*JobRestoreParam)(nil), // 10: job_restore.JobRestoreParam
(*JobArchiveState)(nil), // 11: job_archive.JobArchiveState
(*JobRestoreState)(nil), // 12: job_restore.JobRestoreState
(*JobArchiveNextParam)(nil), // 13: job_archive.JobArchiveNextParam
(*JobRestoreNextParam)(nil), // 14: job_restore.JobRestoreNextParam
(*JobArchiveDispatchParam)(nil), // 13: job_archive.JobArchiveDispatchParam
(*JobRestoreDispatchParam)(nil), // 14: job_restore.JobRestoreDispatchParam
(*JobArchiveDisplay)(nil), // 15: job_archive.JobArchiveDisplay
(*JobRestoreDisplay)(nil), // 16: job_restore.JobRestoreDisplay
}
@@ -790,8 +791,8 @@ var file_job_proto_depIdxs = []int32{
10, // 3: job.JobParam.restore:type_name -> job_restore.JobRestoreParam
11, // 4: job.JobState.archive:type_name -> job_archive.JobArchiveState
12, // 5: job.JobState.restore:type_name -> job_restore.JobRestoreState
13, // 6: job.JobNextParam.archive:type_name -> job_archive.JobArchiveNextParam
14, // 7: job.JobNextParam.restore:type_name -> job_restore.JobRestoreNextParam
13, // 6: job.JobDispatchParam.archive:type_name -> job_archive.JobArchiveDispatchParam
14, // 7: job.JobDispatchParam.restore:type_name -> job_restore.JobRestoreDispatchParam
2, // 8: job.CreatableJob.param:type_name -> job.JobParam
0, // 9: job.JobFilter.status:type_name -> job.JobStatus
15, // 10: job.JobDisplay.archive:type_name -> job_archive.JobArchiveDisplay
@@ -848,7 +849,7 @@ func file_job_proto_init() {
}
}
file_job_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*JobNextParam); i {
switch v := v.(*JobDispatchParam); i {
case 0:
return &v.state
case 1:
@@ -917,8 +918,8 @@ func file_job_proto_init() {
(*JobState_Restore)(nil),
}
file_job_proto_msgTypes[3].OneofWrappers = []interface{}{
(*JobNextParam_Archive)(nil),
(*JobNextParam_Restore)(nil),
(*JobDispatchParam_Archive)(nil),
(*JobDispatchParam_Restore)(nil),
}
file_job_proto_msgTypes[5].OneofWrappers = []interface{}{}
file_job_proto_msgTypes[6].OneofWrappers = []interface{}{}
+3 -3
View File
@@ -41,10 +41,10 @@ message JobState {
}
}
message JobNextParam {
message JobDispatchParam {
oneof param {
job_archive.JobArchiveNextParam archive = 1;
job_restore.JobRestoreNextParam restore = 2;
job_archive.JobArchiveDispatchParam archive = 1;
job_restore.JobRestoreDispatchParam restore = 2;
}
}
+88 -88
View File
@@ -119,20 +119,20 @@ func (x *JobArchiveParam) GetSources() []*Source {
return nil
}
type JobArchiveNextParam struct {
type JobArchiveDispatchParam struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to Param:
// *JobArchiveNextParam_WaitForTape
// *JobArchiveNextParam_Copying
// *JobArchiveNextParam_Finished
Param isJobArchiveNextParam_Param `protobuf_oneof:"param"`
// *JobArchiveDispatchParam_WaitForTape
// *JobArchiveDispatchParam_Copying
// *JobArchiveDispatchParam_Finished
Param isJobArchiveDispatchParam_Param `protobuf_oneof:"param"`
}
func (x *JobArchiveNextParam) Reset() {
*x = JobArchiveNextParam{}
func (x *JobArchiveDispatchParam) Reset() {
*x = JobArchiveDispatchParam{}
if protoimpl.UnsafeEnabled {
mi := &file_job_archive_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -140,13 +140,13 @@ func (x *JobArchiveNextParam) Reset() {
}
}
func (x *JobArchiveNextParam) String() string {
func (x *JobArchiveDispatchParam) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*JobArchiveNextParam) ProtoMessage() {}
func (*JobArchiveDispatchParam) ProtoMessage() {}
func (x *JobArchiveNextParam) ProtoReflect() protoreflect.Message {
func (x *JobArchiveDispatchParam) ProtoReflect() protoreflect.Message {
mi := &file_job_archive_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -158,60 +158,60 @@ func (x *JobArchiveNextParam) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use JobArchiveNextParam.ProtoReflect.Descriptor instead.
func (*JobArchiveNextParam) Descriptor() ([]byte, []int) {
// Deprecated: Use JobArchiveDispatchParam.ProtoReflect.Descriptor instead.
func (*JobArchiveDispatchParam) Descriptor() ([]byte, []int) {
return file_job_archive_proto_rawDescGZIP(), []int{1}
}
func (m *JobArchiveNextParam) GetParam() isJobArchiveNextParam_Param {
func (m *JobArchiveDispatchParam) GetParam() isJobArchiveDispatchParam_Param {
if m != nil {
return m.Param
}
return nil
}
func (x *JobArchiveNextParam) GetWaitForTape() *JobArchiveWaitForTapeParam {
if x, ok := x.GetParam().(*JobArchiveNextParam_WaitForTape); ok {
func (x *JobArchiveDispatchParam) GetWaitForTape() *JobArchiveWaitForTapeParam {
if x, ok := x.GetParam().(*JobArchiveDispatchParam_WaitForTape); ok {
return x.WaitForTape
}
return nil
}
func (x *JobArchiveNextParam) GetCopying() *JobArchiveCopyingParam {
if x, ok := x.GetParam().(*JobArchiveNextParam_Copying); ok {
func (x *JobArchiveDispatchParam) GetCopying() *JobArchiveCopyingParam {
if x, ok := x.GetParam().(*JobArchiveDispatchParam_Copying); ok {
return x.Copying
}
return nil
}
func (x *JobArchiveNextParam) GetFinished() *JobArchiveFinishedParam {
if x, ok := x.GetParam().(*JobArchiveNextParam_Finished); ok {
func (x *JobArchiveDispatchParam) GetFinished() *JobArchiveFinishedParam {
if x, ok := x.GetParam().(*JobArchiveDispatchParam_Finished); ok {
return x.Finished
}
return nil
}
type isJobArchiveNextParam_Param interface {
isJobArchiveNextParam_Param()
type isJobArchiveDispatchParam_Param interface {
isJobArchiveDispatchParam_Param()
}
type JobArchiveNextParam_WaitForTape struct {
type JobArchiveDispatchParam_WaitForTape struct {
WaitForTape *JobArchiveWaitForTapeParam `protobuf:"bytes,1,opt,name=wait_for_tape,json=waitForTape,proto3,oneof"`
}
type JobArchiveNextParam_Copying struct {
type JobArchiveDispatchParam_Copying struct {
Copying *JobArchiveCopyingParam `protobuf:"bytes,2,opt,name=copying,proto3,oneof"`
}
type JobArchiveNextParam_Finished struct {
type JobArchiveDispatchParam_Finished struct {
Finished *JobArchiveFinishedParam `protobuf:"bytes,255,opt,name=finished,proto3,oneof"`
}
func (*JobArchiveNextParam_WaitForTape) isJobArchiveNextParam_Param() {}
func (*JobArchiveDispatchParam_WaitForTape) isJobArchiveDispatchParam_Param() {}
func (*JobArchiveNextParam_Copying) isJobArchiveNextParam_Param() {}
func (*JobArchiveDispatchParam_Copying) isJobArchiveDispatchParam_Param() {}
func (*JobArchiveNextParam_Finished) isJobArchiveNextParam_Param() {}
func (*JobArchiveDispatchParam_Finished) isJobArchiveDispatchParam_Param() {}
type JobArchiveWaitForTapeParam struct {
state protoimpl.MessageState
@@ -503,61 +503,61 @@ var file_job_archive_proto_rawDesc = []byte{
0x0a, 0x0f, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x50, 0x61, 0x72, 0x61,
0x6d, 0x12, 0x28, 0x0a, 0x07, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x52, 0x07, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0xf3, 0x01, 0x0a, 0x13,
0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61,
0x72, 0x61, 0x6d, 0x12, 0x4d, 0x0a, 0x0d, 0x77, 0x61, 0x69, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x5f,
0x74, 0x61, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6a, 0x6f, 0x62,
0x63, 0x65, 0x52, 0x07, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0xf7, 0x01, 0x0a, 0x17,
0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74,
0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x4d, 0x0a, 0x0d, 0x77, 0x61, 0x69, 0x74, 0x5f,
0x66, 0x6f, 0x72, 0x5f, 0x74, 0x61, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27,
0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x4a, 0x6f, 0x62,
0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x54, 0x61,
0x70, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x0b, 0x77, 0x61, 0x69, 0x74, 0x46,
0x6f, 0x72, 0x54, 0x61, 0x70, 0x65, 0x12, 0x3f, 0x0a, 0x07, 0x63, 0x6f, 0x70, 0x79, 0x69, 0x6e,
0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x61, 0x72,
0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65,
0x43, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x07,
0x63, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x12, 0x43, 0x0a, 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73,
0x68, 0x65, 0x64, 0x18, 0xff, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6a, 0x6f, 0x62,
0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68,
0x69, 0x76, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x50, 0x61, 0x72, 0x61, 0x6d,
0x48, 0x00, 0x52, 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05,
0x70, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x1c, 0x0a, 0x1a, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68,
0x69, 0x76, 0x65, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x54, 0x61, 0x70, 0x65, 0x50, 0x61,
0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x0b, 0x77, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x54, 0x61,
0x70, 0x65, 0x12, 0x3f, 0x0a, 0x07, 0x63, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76,
0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x70, 0x79,
0x69, 0x6e, 0x67, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x70, 0x79,
0x69, 0x6e, 0x67, 0x12, 0x43, 0x0a, 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18,
0xff, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x61, 0x72, 0x63,
0x68, 0x69, 0x76, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x46,
0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x08,
0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x61,
0x6d, 0x22, 0x1c, 0x0a, 0x1a, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x57,
0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x54, 0x61, 0x70, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x22,
0x5e, 0x0a, 0x16, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x70,
0x79, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x76,
0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63,
0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22,
0x19, 0x0a, 0x17, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x46, 0x69, 0x6e,
0x69, 0x73, 0x68, 0x65, 0x64, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x71, 0x0a, 0x0f, 0x4a, 0x6f,
0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a,
0x04, 0x73, 0x74, 0x65, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x6a, 0x6f,
0x62, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63,
0x68, 0x69, 0x76, 0x65, 0x53, 0x74, 0x65, 0x70, 0x52, 0x04, 0x73, 0x74, 0x65, 0x70, 0x12, 0x2d,
0x0a, 0x07, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x13, 0x2e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53,
0x74, 0x61, 0x74, 0x65, 0x52, 0x07, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0xdf, 0x01,
0x0a, 0x11, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70,
0x6c, 0x61, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x5f, 0x62, 0x79,
0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x6f, 0x70, 0x69, 0x65,
0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64,
0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x6f,
0x70, 0x69, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74,
0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a,
0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f,
0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x05, 0x73,
0x70, 0x65, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x05, 0x73, 0x70,
0x65, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f,
0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72,
0x74, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x70, 0x65, 0x65, 0x64, 0x2a,
0x4c, 0x0a, 0x0e, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x53, 0x74, 0x65,
0x70, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x11,
0x0a, 0x0d, 0x57, 0x41, 0x49, 0x54, 0x5f, 0x46, 0x4f, 0x52, 0x5f, 0x54, 0x41, 0x50, 0x45, 0x10,
0x01, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x50, 0x59, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0d,
0x0a, 0x08, 0x46, 0x49, 0x4e, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0xff, 0x01, 0x42, 0x23, 0x5a,
0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x61, 0x6d, 0x75,
0x65, 0x6c, 0x6e, 0x63, 0x75, 0x69, 0x2f, 0x79, 0x61, 0x74, 0x6d, 0x2f, 0x65, 0x6e, 0x74, 0x69,
0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x72, 0x61, 0x6d, 0x22, 0x5e, 0x0a, 0x16, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76,
0x65, 0x43, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x16, 0x0a,
0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64,
0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x22, 0x19, 0x0a, 0x17, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76,
0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x71,
0x0a, 0x0f, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x53, 0x74, 0x61, 0x74,
0x65, 0x12, 0x2f, 0x0a, 0x04, 0x73, 0x74, 0x65, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x1b, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x2e, 0x4a, 0x6f,
0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x53, 0x74, 0x65, 0x70, 0x52, 0x04, 0x73, 0x74,
0x65, 0x70, 0x12, 0x2d, 0x0a, 0x07, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x07, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x73, 0x22, 0xdf, 0x01, 0x0a, 0x11, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65,
0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x70, 0x69, 0x65,
0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63,
0x6f, 0x70, 0x69, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f,
0x70, 0x69, 0x65, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0b, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1f, 0x0a,
0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1f,
0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12,
0x19, 0x0a, 0x05, 0x73, 0x70, 0x65, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00,
0x52, 0x05, 0x73, 0x70, 0x65, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74,
0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x70,
0x65, 0x65, 0x64, 0x2a, 0x4c, 0x0a, 0x0e, 0x4a, 0x6f, 0x62, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76,
0x65, 0x53, 0x74, 0x65, 0x70, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47,
0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x57, 0x41, 0x49, 0x54, 0x5f, 0x46, 0x4f, 0x52, 0x5f, 0x54,
0x41, 0x50, 0x45, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x50, 0x59, 0x49, 0x4e, 0x47,
0x10, 0x02, 0x12, 0x0d, 0x0a, 0x08, 0x46, 0x49, 0x4e, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0xff,
0x01, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
0x73, 0x61, 0x6d, 0x75, 0x65, 0x6c, 0x6e, 0x63, 0x75, 0x69, 0x2f, 0x79, 0x61, 0x74, 0x6d, 0x2f,
0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -577,7 +577,7 @@ var file_job_archive_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_job_archive_proto_goTypes = []interface{}{
(JobArchiveStep)(0), // 0: job_archive.JobArchiveStep
(*JobArchiveParam)(nil), // 1: job_archive.JobArchiveParam
(*JobArchiveNextParam)(nil), // 2: job_archive.JobArchiveNextParam
(*JobArchiveDispatchParam)(nil), // 2: job_archive.JobArchiveDispatchParam
(*JobArchiveWaitForTapeParam)(nil), // 3: job_archive.JobArchiveWaitForTapeParam
(*JobArchiveCopyingParam)(nil), // 4: job_archive.JobArchiveCopyingParam
(*JobArchiveFinishedParam)(nil), // 5: job_archive.JobArchiveFinishedParam
@@ -588,9 +588,9 @@ var file_job_archive_proto_goTypes = []interface{}{
}
var file_job_archive_proto_depIdxs = []int32{
8, // 0: job_archive.JobArchiveParam.sources:type_name -> source.Source
3, // 1: job_archive.JobArchiveNextParam.wait_for_tape:type_name -> job_archive.JobArchiveWaitForTapeParam
4, // 2: job_archive.JobArchiveNextParam.copying:type_name -> job_archive.JobArchiveCopyingParam
5, // 3: job_archive.JobArchiveNextParam.finished:type_name -> job_archive.JobArchiveFinishedParam
3, // 1: job_archive.JobArchiveDispatchParam.wait_for_tape:type_name -> job_archive.JobArchiveWaitForTapeParam
4, // 2: job_archive.JobArchiveDispatchParam.copying:type_name -> job_archive.JobArchiveCopyingParam
5, // 3: job_archive.JobArchiveDispatchParam.finished:type_name -> job_archive.JobArchiveFinishedParam
0, // 4: job_archive.JobArchiveState.step:type_name -> job_archive.JobArchiveStep
9, // 5: job_archive.JobArchiveState.sources:type_name -> source.SourceState
6, // [6:6] is the sub-list for method output_type
@@ -620,7 +620,7 @@ func file_job_archive_proto_init() {
}
}
file_job_archive_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*JobArchiveNextParam); i {
switch v := v.(*JobArchiveDispatchParam); i {
case 0:
return &v.state
case 1:
@@ -693,9 +693,9 @@ func file_job_archive_proto_init() {
}
}
file_job_archive_proto_msgTypes[1].OneofWrappers = []interface{}{
(*JobArchiveNextParam_WaitForTape)(nil),
(*JobArchiveNextParam_Copying)(nil),
(*JobArchiveNextParam_Finished)(nil),
(*JobArchiveDispatchParam_WaitForTape)(nil),
(*JobArchiveDispatchParam_Copying)(nil),
(*JobArchiveDispatchParam_Finished)(nil),
}
file_job_archive_proto_msgTypes[6].OneofWrappers = []interface{}{}
type x struct{}
+1 -1
View File
@@ -16,7 +16,7 @@ message JobArchiveParam {
repeated source.Source sources = 1;
}
message JobArchiveNextParam {
message JobArchiveDispatchParam {
oneof param {
JobArchiveWaitForTapeParam wait_for_tape = 1;
JobArchiveCopyingParam copying = 2;
+111 -111
View File
@@ -119,20 +119,20 @@ func (x *JobRestoreParam) GetFileIds() []int64 {
return nil
}
type JobRestoreNextParam struct {
type JobRestoreDispatchParam struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to Param:
// *JobRestoreNextParam_WaitForTape
// *JobRestoreNextParam_Copying
// *JobRestoreNextParam_Finished
Param isJobRestoreNextParam_Param `protobuf_oneof:"param"`
// *JobRestoreDispatchParam_WaitForTape
// *JobRestoreDispatchParam_Copying
// *JobRestoreDispatchParam_Finished
Param isJobRestoreDispatchParam_Param `protobuf_oneof:"param"`
}
func (x *JobRestoreNextParam) Reset() {
*x = JobRestoreNextParam{}
func (x *JobRestoreDispatchParam) Reset() {
*x = JobRestoreDispatchParam{}
if protoimpl.UnsafeEnabled {
mi := &file_job_restore_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -140,13 +140,13 @@ func (x *JobRestoreNextParam) Reset() {
}
}
func (x *JobRestoreNextParam) String() string {
func (x *JobRestoreDispatchParam) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*JobRestoreNextParam) ProtoMessage() {}
func (*JobRestoreDispatchParam) ProtoMessage() {}
func (x *JobRestoreNextParam) ProtoReflect() protoreflect.Message {
func (x *JobRestoreDispatchParam) ProtoReflect() protoreflect.Message {
mi := &file_job_restore_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -158,60 +158,60 @@ func (x *JobRestoreNextParam) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use JobRestoreNextParam.ProtoReflect.Descriptor instead.
func (*JobRestoreNextParam) Descriptor() ([]byte, []int) {
// Deprecated: Use JobRestoreDispatchParam.ProtoReflect.Descriptor instead.
func (*JobRestoreDispatchParam) Descriptor() ([]byte, []int) {
return file_job_restore_proto_rawDescGZIP(), []int{1}
}
func (m *JobRestoreNextParam) GetParam() isJobRestoreNextParam_Param {
func (m *JobRestoreDispatchParam) GetParam() isJobRestoreDispatchParam_Param {
if m != nil {
return m.Param
}
return nil
}
func (x *JobRestoreNextParam) GetWaitForTape() *JobRestoreWaitForTapeParam {
if x, ok := x.GetParam().(*JobRestoreNextParam_WaitForTape); ok {
func (x *JobRestoreDispatchParam) GetWaitForTape() *JobRestoreWaitForTapeParam {
if x, ok := x.GetParam().(*JobRestoreDispatchParam_WaitForTape); ok {
return x.WaitForTape
}
return nil
}
func (x *JobRestoreNextParam) GetCopying() *JobRestoreCopyingParam {
if x, ok := x.GetParam().(*JobRestoreNextParam_Copying); ok {
func (x *JobRestoreDispatchParam) GetCopying() *JobRestoreCopyingParam {
if x, ok := x.GetParam().(*JobRestoreDispatchParam_Copying); ok {
return x.Copying
}
return nil
}
func (x *JobRestoreNextParam) GetFinished() *JobRestoreFinishedParam {
if x, ok := x.GetParam().(*JobRestoreNextParam_Finished); ok {
func (x *JobRestoreDispatchParam) GetFinished() *JobRestoreFinishedParam {
if x, ok := x.GetParam().(*JobRestoreDispatchParam_Finished); ok {
return x.Finished
}
return nil
}
type isJobRestoreNextParam_Param interface {
isJobRestoreNextParam_Param()
type isJobRestoreDispatchParam_Param interface {
isJobRestoreDispatchParam_Param()
}
type JobRestoreNextParam_WaitForTape struct {
type JobRestoreDispatchParam_WaitForTape struct {
WaitForTape *JobRestoreWaitForTapeParam `protobuf:"bytes,1,opt,name=wait_for_tape,json=waitForTape,proto3,oneof"`
}
type JobRestoreNextParam_Copying struct {
type JobRestoreDispatchParam_Copying struct {
Copying *JobRestoreCopyingParam `protobuf:"bytes,2,opt,name=copying,proto3,oneof"`
}
type JobRestoreNextParam_Finished struct {
type JobRestoreDispatchParam_Finished struct {
Finished *JobRestoreFinishedParam `protobuf:"bytes,255,opt,name=finished,proto3,oneof"`
}
func (*JobRestoreNextParam_WaitForTape) isJobRestoreNextParam_Param() {}
func (*JobRestoreDispatchParam_WaitForTape) isJobRestoreDispatchParam_Param() {}
func (*JobRestoreNextParam_Copying) isJobRestoreNextParam_Param() {}
func (*JobRestoreDispatchParam_Copying) isJobRestoreDispatchParam_Param() {}
func (*JobRestoreNextParam_Finished) isJobRestoreNextParam_Param() {}
func (*JobRestoreDispatchParam_Finished) isJobRestoreDispatchParam_Param() {}
type JobRestoreWaitForTapeParam struct {
state protoimpl.MessageState
@@ -661,84 +661,84 @@ var file_job_restore_proto_rawDesc = []byte{
0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x0f, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72,
0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x69,
0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x65, 0x49, 0x64,
0x73, 0x22, 0xf3, 0x01, 0x0a, 0x13, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x4d, 0x0a, 0x0d, 0x77, 0x61, 0x69,
0x74, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x74, 0x61, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x27, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x4a,
0x73, 0x22, 0xf7, 0x01, 0x0a, 0x17, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x4d, 0x0a,
0x0d, 0x77, 0x61, 0x69, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x74, 0x61, 0x70, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x57, 0x61, 0x69,
0x74, 0x46, 0x6f, 0x72, 0x54, 0x61, 0x70, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52,
0x0b, 0x77, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x54, 0x61, 0x70, 0x65, 0x12, 0x3f, 0x0a, 0x07,
0x63, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e,
0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x52,
0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x72,
0x61, 0x6d, 0x48, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x12, 0x43, 0x0a,
0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0xff, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x24, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x4a,
0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65,
0x64, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68,
0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x1c, 0x0a, 0x1a, 0x4a,
0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72,
0x54, 0x61, 0x70, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00, 0x52, 0x0b, 0x77, 0x61, 0x69,
0x74, 0x46, 0x6f, 0x72, 0x54, 0x61, 0x70, 0x65, 0x12, 0x3f, 0x0a, 0x07, 0x63, 0x6f, 0x70, 0x79,
0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6a, 0x6f, 0x62, 0x5f,
0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x43, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x48, 0x00,
0x52, 0x07, 0x63, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x12, 0x43, 0x0a, 0x08, 0x66, 0x69, 0x6e,
0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0xff, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6a,
0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x65,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x50, 0x61, 0x72,
0x61, 0x6d, 0x48, 0x00, 0x52, 0x08, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x42, 0x07,
0x0a, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x1c, 0x0a, 0x1a, 0x4a, 0x6f, 0x62, 0x52, 0x65,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x54, 0x61, 0x70, 0x65,
0x50, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x30, 0x0a, 0x16, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74,
0x6f, 0x72, 0x65, 0x43, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12,
0x16, 0x0a, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x22, 0x19, 0x0a, 0x17, 0x4a, 0x6f, 0x62, 0x52, 0x65,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x50, 0x61, 0x72,
0x61, 0x6d, 0x22, 0xf7, 0x01, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x46, 0x69,
0x6c, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x03, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74,
0x61, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x74, 0x61,
0x70, 0x65, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x70, 0x6f, 0x73, 0x69, 0x74,
0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18,
0x11, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x70, 0x79, 0x5f, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x2e, 0x43, 0x6f, 0x70, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x12,
0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61,
0x73, 0x68, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1b,
0x0a, 0x09, 0x74, 0x61, 0x70, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x21, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x74, 0x61, 0x70, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x22, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x61, 0x74, 0x68, 0x22, 0xa1, 0x01, 0x0a,
0x0b, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x61, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x07,
0x74, 0x61, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x74,
0x61, 0x70, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x12,
0x2f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x17, 0x2e, 0x63, 0x6f, 0x70, 0x79, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x43, 0x6f,
0x70, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x12, 0x2e, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x12, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x18, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73,
0x22, 0x72, 0x0a, 0x0f, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x74,
0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x73, 0x74, 0x65, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0e, 0x32, 0x1b, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e,
0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x74, 0x65, 0x70, 0x52, 0x04,
0x73, 0x74, 0x65, 0x70, 0x12, 0x2e, 0x0a, 0x05, 0x74, 0x61, 0x70, 0x65, 0x73, 0x18, 0x02, 0x20,
0x54, 0x61, 0x70, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x22, 0x30, 0x0a, 0x16, 0x4a, 0x6f, 0x62,
0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x70, 0x79, 0x69, 0x6e, 0x67, 0x50, 0x61,
0x72, 0x61, 0x6d, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x22, 0x19, 0x0a, 0x17, 0x4a,
0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65,
0x64, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x22, 0xf7, 0x01, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x65, 0x49, 0x64, 0x12,
0x17, 0x0a, 0x07, 0x74, 0x61, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
0x52, 0x06, 0x74, 0x61, 0x70, 0x65, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x6f, 0x73, 0x69,
0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x70,
0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x70, 0x79,
0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x43, 0x6f, 0x70, 0x79, 0x53, 0x74, 0x61, 0x74,
0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69,
0x7a, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12,
0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61,
0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x70, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18,
0x21, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x70, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12,
0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x22,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x61, 0x74, 0x68,
0x22, 0xa1, 0x01, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x61, 0x70, 0x65,
0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x03, 0x52, 0x06, 0x74, 0x61, 0x70, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x72,
0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x72, 0x63,
0x6f, 0x64, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x11, 0x20,
0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x70, 0x79, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75,
0x73, 0x2e, 0x43, 0x6f, 0x70, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74,
0x61, 0x74, 0x75, 0x73, 0x12, 0x2e, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x12, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72,
0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x61, 0x70, 0x65, 0x52, 0x05, 0x74,
0x61, 0x70, 0x65, 0x73, 0x22, 0xdf, 0x01, 0x0a, 0x11, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74,
0x6f, 0x72, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f,
0x70, 0x69, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0b, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x21, 0x0a,
0x0c, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73,
0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18,
0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65,
0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73,
0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x12, 0x19, 0x0a, 0x05, 0x73, 0x70, 0x65, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28,
0x03, 0x48, 0x00, 0x52, 0x05, 0x73, 0x70, 0x65, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a,
0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28,
0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x08, 0x0a, 0x06,
0x5f, 0x73, 0x70, 0x65, 0x65, 0x64, 0x2a, 0x4c, 0x0a, 0x0e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73,
0x74, 0x6f, 0x72, 0x65, 0x53, 0x74, 0x65, 0x70, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, 0x44,
0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x57, 0x41, 0x49, 0x54, 0x5f, 0x46, 0x4f,
0x52, 0x5f, 0x54, 0x41, 0x50, 0x45, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x50, 0x59,
0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x08, 0x46, 0x49, 0x4e, 0x49, 0x53, 0x48, 0x45,
0x44, 0x10, 0xff, 0x01, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x73, 0x61, 0x6d, 0x75, 0x65, 0x6c, 0x6e, 0x63, 0x75, 0x69, 0x2f, 0x79, 0x61,
0x74, 0x6d, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66,
0x69, 0x6c, 0x65, 0x73, 0x22, 0x72, 0x0a, 0x0f, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x73, 0x74, 0x65, 0x70, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65, 0x73, 0x74,
0x6f, 0x72, 0x65, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x74,
0x65, 0x70, 0x52, 0x04, 0x73, 0x74, 0x65, 0x70, 0x12, 0x2e, 0x0a, 0x05, 0x74, 0x61, 0x70, 0x65,
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x65,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x54, 0x61, 0x70,
0x65, 0x52, 0x05, 0x74, 0x61, 0x70, 0x65, 0x73, 0x22, 0xdf, 0x01, 0x0a, 0x11, 0x4a, 0x6f, 0x62,
0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x12, 0x21,
0x0a, 0x0c, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65,
0x73, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65,
0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x46,
0x69, 0x6c, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79,
0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c,
0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66,
0x69, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61,
0x6c, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x05, 0x73, 0x70, 0x65, 0x65, 0x64, 0x18,
0x05, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x05, 0x73, 0x70, 0x65, 0x65, 0x64, 0x88, 0x01,
0x01, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65,
0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x70, 0x65, 0x65, 0x64, 0x2a, 0x4c, 0x0a, 0x0e, 0x4a, 0x6f,
0x62, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x74, 0x65, 0x70, 0x12, 0x0b, 0x0a, 0x07,
0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x57, 0x41, 0x49,
0x54, 0x5f, 0x46, 0x4f, 0x52, 0x5f, 0x54, 0x41, 0x50, 0x45, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07,
0x43, 0x4f, 0x50, 0x59, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x08, 0x46, 0x49, 0x4e,
0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0xff, 0x01, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x61, 0x6d, 0x75, 0x65, 0x6c, 0x6e, 0x63, 0x75,
0x69, 0x2f, 0x79, 0x61, 0x74, 0x6d, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -758,7 +758,7 @@ var file_job_restore_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_job_restore_proto_goTypes = []interface{}{
(JobRestoreStep)(0), // 0: job_restore.JobRestoreStep
(*JobRestoreParam)(nil), // 1: job_restore.JobRestoreParam
(*JobRestoreNextParam)(nil), // 2: job_restore.JobRestoreNextParam
(*JobRestoreDispatchParam)(nil), // 2: job_restore.JobRestoreDispatchParam
(*JobRestoreWaitForTapeParam)(nil), // 3: job_restore.JobRestoreWaitForTapeParam
(*JobRestoreCopyingParam)(nil), // 4: job_restore.JobRestoreCopyingParam
(*JobRestoreFinishedParam)(nil), // 5: job_restore.JobRestoreFinishedParam
@@ -769,9 +769,9 @@ var file_job_restore_proto_goTypes = []interface{}{
(CopyStatus)(0), // 10: copy_status.CopyStatus
}
var file_job_restore_proto_depIdxs = []int32{
3, // 0: job_restore.JobRestoreNextParam.wait_for_tape:type_name -> job_restore.JobRestoreWaitForTapeParam
4, // 1: job_restore.JobRestoreNextParam.copying:type_name -> job_restore.JobRestoreCopyingParam
5, // 2: job_restore.JobRestoreNextParam.finished:type_name -> job_restore.JobRestoreFinishedParam
3, // 0: job_restore.JobRestoreDispatchParam.wait_for_tape:type_name -> job_restore.JobRestoreWaitForTapeParam
4, // 1: job_restore.JobRestoreDispatchParam.copying:type_name -> job_restore.JobRestoreCopyingParam
5, // 2: job_restore.JobRestoreDispatchParam.finished:type_name -> job_restore.JobRestoreFinishedParam
10, // 3: job_restore.RestoreFile.status:type_name -> copy_status.CopyStatus
10, // 4: job_restore.RestoreTape.status:type_name -> copy_status.CopyStatus
6, // 5: job_restore.RestoreTape.files:type_name -> job_restore.RestoreFile
@@ -804,7 +804,7 @@ func file_job_restore_proto_init() {
}
}
file_job_restore_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*JobRestoreNextParam); i {
switch v := v.(*JobRestoreDispatchParam); i {
case 0:
return &v.state
case 1:
@@ -901,9 +901,9 @@ func file_job_restore_proto_init() {
}
}
file_job_restore_proto_msgTypes[1].OneofWrappers = []interface{}{
(*JobRestoreNextParam_WaitForTape)(nil),
(*JobRestoreNextParam_Copying)(nil),
(*JobRestoreNextParam_Finished)(nil),
(*JobRestoreDispatchParam_WaitForTape)(nil),
(*JobRestoreDispatchParam_Copying)(nil),
(*JobRestoreDispatchParam_Finished)(nil),
}
file_job_restore_proto_msgTypes[8].OneofWrappers = []interface{}{}
type x struct{}
+1 -1
View File
@@ -16,7 +16,7 @@ message JobRestoreParam {
repeated int64 file_ids = 1;
}
message JobRestoreNextParam {
message JobRestoreDispatchParam {
oneof param {
JobRestoreWaitForTapeParam wait_for_tape = 1;
JobRestoreCopyingParam copying = 2;
+462 -323
View File
File diff suppressed because it is too large Load Diff
+14 -5
View File
@@ -22,8 +22,9 @@ service Service {
rpc JobList(JobListRequest) returns (JobListReply) {}
rpc JobCreate(JobCreateRequest) returns (JobCreateReply) {}
rpc JobEditState(JobEditStateRequest) returns (JobEditStateReply) {}
rpc JobDelete(JobDeleteRequest) returns (JobDeleteReply) {}
rpc JobNext(JobNextRequest) returns (JobNextReply) {}
rpc JobDispatch(JobDispatchRequest) returns (JobDispatchReply) {}
rpc JobDisplay(JobDisplayRequest) returns (JobDisplayReply) {}
rpc JobGetLog(JobGetLogRequest) returns (JobGetLogReply) {}
@@ -133,6 +134,15 @@ message JobCreateReply {
job.Job job = 1;
}
message JobEditStateRequest {
int64 id = 1;
optional job.JobStatus status = 2;
optional job.JobState state = 3;
}
message JobEditStateReply {
}
message JobDeleteRequest {
repeated int64 ids = 1;
}
@@ -140,13 +150,12 @@ message JobDeleteRequest {
message JobDeleteReply {
}
message JobNextRequest {
message JobDispatchRequest {
int64 id = 1;
job.JobNextParam param = 2;
job.JobDispatchParam param = 2;
}
message JobNextReply {
job.Job job = 1;
message JobDispatchReply {
}
message JobDisplayRequest {
+50 -14
View File
@@ -32,8 +32,9 @@ type ServiceClient interface {
TapeGetPositions(ctx context.Context, in *TapeGetPositionsRequest, opts ...grpc.CallOption) (*TapeGetPositionsReply, error)
JobList(ctx context.Context, in *JobListRequest, opts ...grpc.CallOption) (*JobListReply, error)
JobCreate(ctx context.Context, in *JobCreateRequest, opts ...grpc.CallOption) (*JobCreateReply, error)
JobEditState(ctx context.Context, in *JobEditStateRequest, opts ...grpc.CallOption) (*JobEditStateReply, error)
JobDelete(ctx context.Context, in *JobDeleteRequest, opts ...grpc.CallOption) (*JobDeleteReply, error)
JobNext(ctx context.Context, in *JobNextRequest, opts ...grpc.CallOption) (*JobNextReply, error)
JobDispatch(ctx context.Context, in *JobDispatchRequest, opts ...grpc.CallOption) (*JobDispatchReply, error)
JobDisplay(ctx context.Context, in *JobDisplayRequest, opts ...grpc.CallOption) (*JobDisplayReply, error)
JobGetLog(ctx context.Context, in *JobGetLogRequest, opts ...grpc.CallOption) (*JobGetLogReply, error)
SourceList(ctx context.Context, in *SourceListRequest, opts ...grpc.CallOption) (*SourceListReply, error)
@@ -140,6 +141,15 @@ func (c *serviceClient) JobCreate(ctx context.Context, in *JobCreateRequest, opt
return out, nil
}
func (c *serviceClient) JobEditState(ctx context.Context, in *JobEditStateRequest, opts ...grpc.CallOption) (*JobEditStateReply, error) {
out := new(JobEditStateReply)
err := c.cc.Invoke(ctx, "/service.Service/JobEditState", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *serviceClient) JobDelete(ctx context.Context, in *JobDeleteRequest, opts ...grpc.CallOption) (*JobDeleteReply, error) {
out := new(JobDeleteReply)
err := c.cc.Invoke(ctx, "/service.Service/JobDelete", in, out, opts...)
@@ -149,9 +159,9 @@ func (c *serviceClient) JobDelete(ctx context.Context, in *JobDeleteRequest, opt
return out, nil
}
func (c *serviceClient) JobNext(ctx context.Context, in *JobNextRequest, opts ...grpc.CallOption) (*JobNextReply, error) {
out := new(JobNextReply)
err := c.cc.Invoke(ctx, "/service.Service/JobNext", in, out, opts...)
func (c *serviceClient) JobDispatch(ctx context.Context, in *JobDispatchRequest, opts ...grpc.CallOption) (*JobDispatchReply, error) {
out := new(JobDispatchReply)
err := c.cc.Invoke(ctx, "/service.Service/JobDispatch", in, out, opts...)
if err != nil {
return nil, err
}
@@ -226,8 +236,9 @@ type ServiceServer interface {
TapeGetPositions(context.Context, *TapeGetPositionsRequest) (*TapeGetPositionsReply, error)
JobList(context.Context, *JobListRequest) (*JobListReply, error)
JobCreate(context.Context, *JobCreateRequest) (*JobCreateReply, error)
JobEditState(context.Context, *JobEditStateRequest) (*JobEditStateReply, error)
JobDelete(context.Context, *JobDeleteRequest) (*JobDeleteReply, error)
JobNext(context.Context, *JobNextRequest) (*JobNextReply, error)
JobDispatch(context.Context, *JobDispatchRequest) (*JobDispatchReply, error)
JobDisplay(context.Context, *JobDisplayRequest) (*JobDisplayReply, error)
JobGetLog(context.Context, *JobGetLogRequest) (*JobGetLogReply, error)
SourceList(context.Context, *SourceListRequest) (*SourceListReply, error)
@@ -271,11 +282,14 @@ func (UnimplementedServiceServer) JobList(context.Context, *JobListRequest) (*Jo
func (UnimplementedServiceServer) JobCreate(context.Context, *JobCreateRequest) (*JobCreateReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method JobCreate not implemented")
}
func (UnimplementedServiceServer) JobEditState(context.Context, *JobEditStateRequest) (*JobEditStateReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method JobEditState not implemented")
}
func (UnimplementedServiceServer) JobDelete(context.Context, *JobDeleteRequest) (*JobDeleteReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method JobDelete not implemented")
}
func (UnimplementedServiceServer) JobNext(context.Context, *JobNextRequest) (*JobNextReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method JobNext not implemented")
func (UnimplementedServiceServer) JobDispatch(context.Context, *JobDispatchRequest) (*JobDispatchReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method JobDispatch not implemented")
}
func (UnimplementedServiceServer) JobDisplay(context.Context, *JobDisplayRequest) (*JobDisplayReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method JobDisplay not implemented")
@@ -488,6 +502,24 @@ func _Service_JobCreate_Handler(srv interface{}, ctx context.Context, dec func(i
return interceptor(ctx, in, info, handler)
}
func _Service_JobEditState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(JobEditStateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ServiceServer).JobEditState(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/service.Service/JobEditState",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ServiceServer).JobEditState(ctx, req.(*JobEditStateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Service_JobDelete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(JobDeleteRequest)
if err := dec(in); err != nil {
@@ -506,20 +538,20 @@ func _Service_JobDelete_Handler(srv interface{}, ctx context.Context, dec func(i
return interceptor(ctx, in, info, handler)
}
func _Service_JobNext_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(JobNextRequest)
func _Service_JobDispatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(JobDispatchRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ServiceServer).JobNext(ctx, in)
return srv.(ServiceServer).JobDispatch(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/service.Service/JobNext",
FullMethod: "/service.Service/JobDispatch",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ServiceServer).JobNext(ctx, req.(*JobNextRequest))
return srv.(ServiceServer).JobDispatch(ctx, req.(*JobDispatchRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -679,13 +711,17 @@ var Service_ServiceDesc = grpc.ServiceDesc{
MethodName: "JobCreate",
Handler: _Service_JobCreate_Handler,
},
{
MethodName: "JobEditState",
Handler: _Service_JobEditState_Handler,
},
{
MethodName: "JobDelete",
Handler: _Service_JobDelete_Handler,
},
{
MethodName: "JobNext",
Handler: _Service_JobNext_Handler,
MethodName: "JobDispatch",
Handler: _Service_JobDispatch_Handler,
},
{
MethodName: "JobDisplay",
+33
View File
@@ -0,0 +1,33 @@
package executor
import "sort"
func (e *Executor) ListAvailableDevices() []string {
e.devicesLock.Lock()
defer e.devicesLock.Unlock()
devices := e.availableDevices.ToSlice()
sort.Slice(devices, func(i, j int) bool {
return devices[i] < devices[j]
})
return devices
}
func (e *Executor) OccupyDevice(dev string) bool {
e.devicesLock.Lock()
defer e.devicesLock.Unlock()
if !e.availableDevices.Contains(dev) {
return false
}
e.availableDevices.Remove(dev)
return true
}
func (e *Executor) ReleaseDevice(dev string) {
e.devicesLock.Lock()
defer e.devicesLock.Unlock()
e.availableDevices.Add(dev)
}
+62 -79
View File
@@ -3,12 +3,13 @@ package executor
import (
"context"
"fmt"
"sort"
"sync"
mapset "github.com/deckarep/golang-set/v2"
"github.com/modern-go/reflect2"
"github.com/samuelncui/yatm/entity"
"github.com/samuelncui/yatm/library"
"github.com/samuelncui/yatm/tools"
"gorm.io/gorm"
)
@@ -16,13 +17,14 @@ type Executor struct {
db *gorm.DB
lib *library.Library
devices []string
devicesLock sync.Mutex
devices []string
availableDevices mapset.Set[string]
paths Paths
scripts Scripts
jobExecutors *tools.CacheOnce[int64, JobExecutor]
}
type Paths struct {
@@ -43,7 +45,7 @@ func New(
db *gorm.DB, lib *library.Library,
devices []string, paths Paths, scripts Scripts,
) *Executor {
return &Executor{
e := &Executor{
db: db,
lib: lib,
devices: devices,
@@ -51,89 +53,76 @@ func New(
paths: paths,
scripts: scripts,
}
e.jobExecutors = tools.NewCacheOnce(e.newJobExecutor)
return e
}
func (e *Executor) AutoMigrate() error {
return e.db.AutoMigrate(ModelJob)
}
func (e *Executor) ListAvailableDevices() []string {
e.devicesLock.Lock()
defer e.devicesLock.Unlock()
func (e *Executor) CreateJob(ctx context.Context, job *Job, param *entity.JobParam) (*Job, error) {
job, err := e.SaveJob(ctx, job)
if err != nil {
return nil, fmt.Errorf("save job fail, err= %w", err)
}
devices := e.availableDevices.ToSlice()
sort.Slice(devices, func(i, j int) bool {
return devices[i] < devices[j]
})
typ, found := jobTypes[jobParamToTypes[reflect2.RTypeOf(param.GetParam())]]
if !found || typ == nil {
return nil, fmt.Errorf("job type unexpected, state_type= %T", param.GetParam())
}
return devices
executor, err := typ.GetExecutor(ctx, e, job)
if err != nil {
return nil, fmt.Errorf("get job executor fail, job_id= %d, %w", job.ID, err)
}
if err := executor.Initialize(ctx, param); err != nil {
executor.Logger().WithContext(ctx).WithError(err).Errorf("initialize failed, param= %s", param)
return nil, fmt.Errorf("executor initialize fail, job_id= %d param= %s, %w", job.ID, param, err)
}
if err := executor.Close(ctx); err != nil {
executor.Logger().WithContext(ctx).WithError(err).Errorf("close executor failed, param= %s", param)
return nil, fmt.Errorf("close executor failed, job_id= %d param= %s, %w", job.ID, param, err)
}
return job, nil
}
func (e *Executor) occupyDevice(dev string) bool {
e.devicesLock.Lock()
defer e.devicesLock.Unlock()
if !e.availableDevices.Contains(dev) {
return false
}
e.availableDevices.Remove(dev)
return true
func (e *Executor) GetJobExecutor(ctx context.Context, id int64) (JobExecutor, error) {
return e.jobExecutors.Get(ctx, id)
}
func (e *Executor) releaseDevice(dev string) {
e.devicesLock.Lock()
defer e.devicesLock.Unlock()
e.availableDevices.Add(dev)
func (e *Executor) RemoveJobExecutor(ctx context.Context, id int64) {
e.jobExecutors.Remove(id)
}
func (e *Executor) Start(ctx context.Context, job *Job) error {
job.Status = entity.JobStatus_PROCESSING
if _, err := e.SaveJob(ctx, job); err != nil {
return err
func (e *Executor) newJobExecutor(ctx context.Context, id int64) (JobExecutor, error) {
job, err := e.GetJob(ctx, id)
if err != nil {
return nil, fmt.Errorf("get job fail, id= %d, %w", id, err)
}
if state := job.State.GetArchive(); state != nil {
if err := e.startArchive(ctx, job); err != nil {
return err
}
return nil
}
if state := job.State.GetRestore(); state != nil {
if err := e.startRestore(ctx, job); err != nil {
return err
}
return nil
factory, has := jobTypes[reflect2.RTypeOf(job.State.GetState())]
if !has {
return nil, fmt.Errorf("job type unexpected, state_type= %T", job.State.GetState())
}
return fmt.Errorf("unexpected state type, %T", job.State.State)
return factory.GetExecutor(ctx, e, job)
}
func (e *Executor) Submit(ctx context.Context, job *Job, param *entity.JobNextParam) error {
if job.Status != entity.JobStatus_PROCESSING {
return fmt.Errorf("target job is not on processing, status= %s", job.Status)
func (e *Executor) Dispatch(ctx context.Context, jobID int64, param *entity.JobDispatchParam) error {
executor, err := e.GetJobExecutor(ctx, jobID)
if err != nil {
return fmt.Errorf("get job executor fail, job_id= %d, %w", jobID, err)
}
if state := job.State.GetArchive(); state != nil {
exe, err := e.newArchiveExecutor(ctx, job)
if err != nil {
return err
}
exe.submit(ctx, param.GetArchive())
return nil
}
if state := job.State.GetRestore(); state != nil {
exe, err := e.newRestoreExecutor(ctx, job)
if err != nil {
return err
}
exe.submit(ctx, param.GetRestore())
return nil
if err := executor.Dispatch(ctx, param); err != nil {
executor.Logger().WithContext(ctx).WithError(err).Errorf("dispatch request fail, req= %s", param)
return fmt.Errorf("dispatch request fail, job_id= %d, req= %s, %w", jobID, param, err)
}
return fmt.Errorf("unexpected state type, %T", job.State.State)
return nil
}
func (e *Executor) Display(ctx context.Context, job *Job) (*entity.JobDisplay, error) {
@@ -141,22 +130,16 @@ func (e *Executor) Display(ctx context.Context, job *Job) (*entity.JobDisplay, e
return nil, fmt.Errorf("target job is not on processing, status= %s", job.Status)
}
if state := job.State.GetArchive(); state != nil {
display, err := e.getArchiveDisplay(ctx, job)
if err != nil {
return nil, err
}
return &entity.JobDisplay{Display: &entity.JobDisplay_Archive{Archive: display}}, nil
}
if state := job.State.GetRestore(); state != nil {
display, err := e.getRestoreDisplay(ctx, job)
if err != nil {
return nil, err
}
return &entity.JobDisplay{Display: &entity.JobDisplay_Restore{Restore: display}}, nil
executor, err := e.GetJobExecutor(ctx, job.ID)
if err != nil {
return nil, fmt.Errorf("get job executor fail, job_id= %d, %w", job.ID, err)
}
return nil, fmt.Errorf("unexpected state type, %T", job.State.State)
result, err := executor.Display(ctx)
if err != nil {
executor.Logger().WithContext(ctx).WithError(err).Errorf("get display failed")
return nil, err
}
return result, nil
}
-22
View File
@@ -33,28 +33,6 @@ func (j *Job) BeforeUpdate(tx *gorm.DB) error {
return nil
}
func (e *Executor) initJob(ctx context.Context, job *Job, param *entity.JobParam) error {
if p := param.GetArchive(); p != nil {
return e.createArchive(ctx, job, p)
}
if p := param.GetRestore(); p != nil {
return e.createRestore(ctx, job, p)
}
return fmt.Errorf("unexpected param type, %T", param.Param)
}
func (e *Executor) CreateJob(ctx context.Context, job *Job, param *entity.JobParam) (*Job, error) {
if err := e.initJob(ctx, job, param); err != nil {
return nil, err
}
if r := e.db.WithContext(ctx).Create(job); r.Error != nil {
return nil, fmt.Errorf("save job fail, err= %w", r.Error)
}
return job, nil
}
func (e *Executor) DeleteJobs(ctx context.Context, ids ...int64) error {
jobs, err := e.MGetJob(ctx, ids...)
if err != nil {
+3
View File
@@ -0,0 +1,3 @@
package executor
type jobTypeArchive struct{}
-25
View File
@@ -1,25 +0,0 @@
package executor
import (
"context"
"sync/atomic"
"github.com/samuelncui/yatm/entity"
)
func (e *Executor) getArchiveDisplay(ctx context.Context, job *Job) (*entity.JobArchiveDisplay, error) {
display := new(entity.JobArchiveDisplay)
if exe := e.getArchiveExecutor(ctx, job); exe != nil && exe.progress != nil {
display.CopiedBytes = atomic.LoadInt64(&exe.progress.bytes)
display.CopiedFiles = atomic.LoadInt64(&exe.progress.files)
display.TotalBytes = atomic.LoadInt64(&exe.progress.totalBytes)
display.TotalFiles = atomic.LoadInt64(&exe.progress.totalFiles)
display.StartTime = exe.progress.startTime.Unix()
speed := atomic.LoadInt64(&exe.progress.speed)
display.Speed = &speed
}
return display, nil
}
+78 -345
View File
@@ -2,44 +2,30 @@ package executor
import (
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"sort"
"sync"
"sync/atomic"
"time"
mapset "github.com/deckarep/golang-set/v2"
"github.com/samber/lo"
"github.com/samuelncui/acp"
"github.com/samuelncui/yatm/entity"
"github.com/samuelncui/yatm/library"
"github.com/samuelncui/yatm/tools"
"github.com/sirupsen/logrus"
)
var (
runningArchives sync.Map
)
type jobArchiveExecutor struct {
lock sync.Mutex
exe *Executor
job *Job
func (e *Executor) getArchiveExecutor(ctx context.Context, job *Job) *jobArchiveExecutor {
if running, has := runningArchives.Load(job.ID); has {
return running.(*jobArchiveExecutor)
}
return nil
progress *progress
logFile *os.File
logger *logrus.Logger
}
func (e *Executor) newArchiveExecutor(ctx context.Context, job *Job) (*jobArchiveExecutor, error) {
if exe := e.getArchiveExecutor(ctx, job); exe != nil {
return exe, nil
}
logFile, err := e.newLogWriter(job.ID)
func (*jobTypeArchive) GetExecutor(ctx context.Context, exe *Executor, job *Job) (JobExecutor, error) {
logFile, err := exe.newLogWriter(job.ID)
if err != nil {
return nil, fmt.Errorf("get log writer fail, %w", err)
}
@@ -47,39 +33,27 @@ func (e *Executor) newArchiveExecutor(ctx context.Context, job *Job) (*jobArchiv
logger := logrus.New()
logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
exe := &jobArchiveExecutor{
exe: e,
e := &jobArchiveExecutor{
exe: exe,
job: job,
state: job.State.GetArchive(),
logFile: logFile,
logger: logger,
}
runningArchives.Store(job.ID, exe)
return exe, nil
return e, nil
}
type jobArchiveExecutor struct {
exe *Executor
job *Job
stateLock sync.Mutex
state *entity.JobArchiveState
progress *progress
logFile *os.File
logger *logrus.Logger
}
func (a *jobArchiveExecutor) submit(ctx context.Context, param *entity.JobArchiveNextParam) {
if err := a.handle(ctx, param); err != nil {
a.logger.WithContext(ctx).WithError(err).Infof("handler param fail, param= %s", param)
func (a *jobArchiveExecutor) Dispatch(ctx context.Context, next *entity.JobDispatchParam) error {
param := next.GetArchive()
if param == nil {
return fmt.Errorf("unexpected next param type, unexpected= JobArchiveDispatchParam, has= %s", next)
}
return a.dispatch(ctx, param)
}
func (a *jobArchiveExecutor) handle(ctx context.Context, param *entity.JobArchiveNextParam) error {
func (a *jobArchiveExecutor) dispatch(ctx context.Context, param *entity.JobArchiveDispatchParam) error {
if p := param.GetCopying(); p != nil {
if err := a.switchStep(
ctx, entity.JobArchiveStep_COPYING, entity.JobStatus_PROCESSING,
@@ -91,6 +65,7 @@ func (a *jobArchiveExecutor) handle(ctx context.Context, param *entity.JobArchiv
tools.Working()
go tools.WrapWithLogger(ctx, a.logger, func() {
defer tools.Done()
if err := a.makeTape(tools.ShutdownContext, p.Device, p.Barcode, p.Name); err != nil {
a.logger.WithContext(ctx).WithError(err).Errorf("make tape has error, barcode= '%s' name= '%s'", p.Barcode, p.Name)
}
@@ -101,7 +76,7 @@ func (a *jobArchiveExecutor) handle(ctx context.Context, param *entity.JobArchiv
if p := param.GetWaitForTape(); p != nil {
return a.switchStep(
ctx, entity.JobArchiveStep_WAIT_FOR_TAPE, entity.JobStatus_PROCESSING,
ctx, entity.JobArchiveStep_WAIT_FOR_TAPE, entity.JobStatus_PENDING,
mapset.NewThreadUnsafeSet(entity.JobArchiveStep_PENDING, entity.JobArchiveStep_COPYING),
)
}
@@ -114,322 +89,80 @@ func (a *jobArchiveExecutor) handle(ctx context.Context, param *entity.JobArchiv
return err
}
a.logFile.Close()
runningArchives.Delete(a.job.ID)
return nil
return a.Close(ctx)
}
return nil
}
func (a *jobArchiveExecutor) makeTape(ctx context.Context, device, barcode, name string) (rerr error) {
if !a.exe.occupyDevice(device) {
return fmt.Errorf("device is using, device= %s", device)
}
defer a.exe.releaseDevice(device)
defer a.makeTapeFinished(tools.WithoutTimeout(ctx))
encryption, keyPath, keyRecycle, err := a.exe.newKey()
if err != nil {
return err
}
defer keyRecycle()
if err := runCmd(a.logger, a.exe.makeEncryptCmd(ctx, device, keyPath, barcode, name)); err != nil {
return fmt.Errorf("run encrypt script fail, %w", err)
func (a *jobArchiveExecutor) Display(ctx context.Context) (*entity.JobDisplay, error) {
p := a.progress
if p == nil {
return nil, nil
}
mkfsCmd := exec.CommandContext(ctx, a.exe.scripts.Mkfs)
mkfsCmd.Env = append(mkfsCmd.Env, fmt.Sprintf("DEVICE=%s", device), fmt.Sprintf("TAPE_BARCODE=%s", barcode), fmt.Sprintf("TAPE_NAME=%s", name))
if err := runCmd(a.logger, mkfsCmd); err != nil {
return fmt.Errorf("run mkfs script fail, %w", err)
display := new(entity.JobArchiveDisplay)
display.CopiedBytes = atomic.LoadInt64(&p.bytes)
display.CopiedFiles = atomic.LoadInt64(&p.files)
display.TotalBytes = atomic.LoadInt64(&p.totalBytes)
display.TotalFiles = atomic.LoadInt64(&p.totalFiles)
display.StartTime = p.startTime.Unix()
speed := atomic.LoadInt64(&p.speed)
display.Speed = &speed
return &entity.JobDisplay{Display: &entity.JobDisplay_Archive{Archive: display}}, nil
}
func (a *jobArchiveExecutor) Close(ctx context.Context) error {
a.logFile.Close()
a.exe.RemoveJobExecutor(ctx, a.job.ID)
return nil
}
func (a *jobArchiveExecutor) Logger() *logrus.Logger {
return a.logger
}
func (a *jobArchiveExecutor) getState() *entity.JobArchiveState {
a.lock.Lock()
defer a.lock.Unlock()
if a.job.State == nil || a.job.State.GetArchive() == nil {
a.job.State = &entity.JobState{State: &entity.JobState_Archive{Archive: &entity.JobArchiveState{}}}
}
mountPoint, err := os.MkdirTemp("", "*.ltfs")
if err != nil {
return fmt.Errorf("create temp mountpoint, %w", err)
return a.job.State.GetArchive()
}
func (a *jobArchiveExecutor) updateJob(ctx context.Context, change func(*Job, *entity.JobArchiveState) error) error {
a.lock.Lock()
defer a.lock.Unlock()
if a.job.State == nil || a.job.State.GetArchive() == nil {
a.job.State = &entity.JobState{State: &entity.JobState_Archive{Archive: &entity.JobArchiveState{}}}
}
mountCmd := exec.CommandContext(ctx, a.exe.scripts.Mount)
mountCmd.Env = append(mountCmd.Env, fmt.Sprintf("DEVICE=%s", device), fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(a.logger, mountCmd); err != nil {
return fmt.Errorf("run mount script fail, %w", err)
if err := change(a.job, a.job.State.GetArchive()); err != nil {
a.logger.WithContext(ctx).WithError(err).Warnf("update state failed while exec change callback")
return fmt.Errorf("update state failed while exec change callback, %w", err)
}
defer func() {
umountCmd := exec.CommandContext(tools.WithoutTimeout(ctx), a.exe.scripts.Umount)
umountCmd.Env = append(umountCmd.Env, fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(a.logger, umountCmd); err != nil {
a.logger.WithContext(ctx).WithError(err).Errorf("run umount script fail, %s", mountPoint)
return
}
if err := os.Remove(mountPoint); err != nil {
a.logger.WithContext(ctx).WithError(err).Errorf("remove mount point fail, %s", mountPoint)
return
}
}()
wildcardJobOpts := make([]acp.WildcardJobOption, 0, 6)
wildcardJobOpts = append(wildcardJobOpts, acp.Target(mountPoint))
for _, source := range a.state.Sources {
if source.Status == entity.CopyStatus_SUBMITED {
continue
}
wildcardJobOpts = append(wildcardJobOpts, acp.AccurateSource(source.Source.Base, source.Source.Path))
if _, err := a.exe.SaveJob(ctx, a.job); err != nil {
a.logger.WithContext(ctx).WithError(err).Warnf("update state failed while save job")
return fmt.Errorf("update state failed while save job, %w", err)
}
opts := make([]acp.Option, 0, 4)
opts = append(opts, acp.WildcardJob(wildcardJobOpts...))
opts = append(opts, acp.WithHash(true))
opts = append(opts, acp.SetToDevice(acp.LinearDevice(true)))
opts = append(opts, acp.WithLogger(a.logger))
reportHander, reportGetter := acp.NewReportGetter()
opts = append(opts, acp.WithEventHandler(reportHander))
a.progress = newProgress()
defer func() { a.progress = nil }()
var dropToReadonly bool
opts = append(opts, acp.WithEventHandler(func(ev acp.Event) {
switch e := ev.(type) {
case *acp.EventUpdateCount:
atomic.StoreInt64(&a.progress.totalBytes, e.Bytes)
atomic.StoreInt64(&a.progress.totalFiles, e.Files)
return
case *acp.EventUpdateProgress:
a.progress.setBytes(e.Bytes)
atomic.StoreInt64(&a.progress.files, e.Files)
return
case *acp.EventReportError:
a.logger.WithContext(ctx).Errorf("acp report error, src= '%s' dst= '%s' err= '%s'", e.Error.Src, e.Error.Dst, e.Error.Err)
return
case *acp.EventUpdateJob:
job := e.Job
src := entity.NewSourceFromACPJob(job)
var targetStatus entity.CopyStatus
switch job.Status {
case acp.JobStatusPending, acp.JobStatusPreparing:
targetStatus = entity.CopyStatus_PENDING
case acp.JobStatusCopying:
targetStatus = entity.CopyStatus_RUNNING
case acp.JobStatusFinished:
targetStatus = entity.CopyStatus_FAILED
if len(job.SuccessTargets) > 0 {
a.logger.WithContext(ctx).Infof("file '%s' copy success, size= %d", src.RealPath(), job.Size)
targetStatus = entity.CopyStatus_STAGED
break // break from switch
}
for dst, err := range job.FailTargets {
if err == nil {
continue
}
if errors.Is(err, acp.ErrTargetNoSpace) {
continue
}
a.logger.WithContext(ctx).WithError(err).Errorf("file '%s' copy fail, dst= '%s'", src.RealPath(), dst)
if errors.Is(err, acp.ErrTargetDropToReadonly) {
dropToReadonly = true
}
}
default:
return
}
a.stateLock.Lock()
defer a.stateLock.Unlock()
idx := sort.Search(len(a.state.Sources), func(idx int) bool {
return src.Compare(a.state.Sources[idx].Source) <= 0
})
if idx < 0 || idx >= len(a.state.Sources) || src.Compare(a.state.Sources[idx].Source) != 0 {
a.logger.Warnf(
"cannot found target file, real_path= %s found_index= %d tape_file_path= %v", src.RealPath(), idx,
lo.Map(a.state.Sources, func(source *entity.SourceState, _ int) string { return source.Source.RealPath() }))
return
}
target := a.state.Sources[idx]
if target == nil || !src.Equal(target.Source) {
return
}
target.Status = targetStatus
if _, err := a.exe.SaveJob(ctx, a.job); err != nil {
logrus.WithContext(ctx).Infof("save job for update file fail, name= %s", job.Base+path.Join(job.Path...))
}
return
}
}))
defer func() {
ctx := tools.WithoutTimeout(ctx)
// if tape drop to readonly, ltfs cannot write index to partition a.
// rollback sources for next try.
if dropToReadonly {
a.logger.WithContext(ctx).Errorf("tape filesystem had droped to readonly, rollback, barcode= '%s'", barcode)
a.rollbackSources(ctx)
return
}
report := reportGetter()
sort.Slice(report.Jobs, func(i, j int) bool {
return entity.NewSourceFromACPJob(report.Jobs[i]).Compare(entity.NewSourceFromACPJob(report.Jobs[j])) < 0
})
reportFile, err := a.exe.newReportWriter(barcode)
if err != nil {
a.logger.WithContext(ctx).WithError(err).Warnf("open report file fail, barcode= '%s'", barcode)
} else {
defer reportFile.Close()
tools.WrapWithLogger(ctx, a.logger, func() {
reportFile.Write([]byte(report.ToJSONString(false)))
})
}
filteredJobs := make([]*acp.Job, 0, len(report.Jobs))
files := make([]*library.TapeFile, 0, len(report.Jobs))
for _, job := range report.Jobs {
if len(job.SuccessTargets) == 0 {
continue
}
if !job.Mode.IsRegular() {
continue
}
hash, err := hex.DecodeString(job.SHA256)
if err != nil {
a.logger.WithContext(ctx).WithError(err).Warnf("decode sha256 fail, path= '%s'", entity.NewSourceFromACPJob(job).RealPath())
continue
}
files = append(files, &library.TapeFile{
Path: path.Join(job.Path...),
Size: job.Size,
Mode: job.Mode,
ModTime: job.ModTime,
WriteTime: job.WriteTime,
Hash: hash,
})
filteredJobs = append(filteredJobs, job)
}
tape, err := a.exe.lib.CreateTape(ctx, &library.Tape{
Barcode: barcode,
Name: name,
Encryption: encryption,
CreateTime: time.Now(),
}, files)
if err != nil {
rerr = tools.AppendError(rerr, fmt.Errorf("create tape fail, barcode= '%s' name= '%s', %w", barcode, name, err))
return
}
a.logger.Infof("create tape success, tape_id= %d", tape.ID)
if err := a.exe.lib.TrimFiles(ctx); err != nil {
a.logger.WithError(err).Warnf("trim library files fail")
}
if err := a.markSourcesAsSubmited(ctx, filteredJobs); err != nil {
rerr = tools.AppendError(rerr, fmt.Errorf("mark source as submited fail, %w", err))
return
}
}()
copyer, err := acp.New(ctx, opts...)
if err != nil {
rerr = fmt.Errorf("start copy fail, %w", err)
return
}
copyer.Wait()
return
return nil
}
func (a *jobArchiveExecutor) switchStep(ctx context.Context, target entity.JobArchiveStep, status entity.JobStatus, expect mapset.Set[entity.JobArchiveStep]) error {
a.stateLock.Lock()
defer a.stateLock.Unlock()
if !expect.Contains(a.state.Step) {
return fmt.Errorf("unexpected current step, target= '%s' expect= '%s' has= '%s'", target, expect, a.state.Step)
}
a.state.Step = target
a.job.Status = status
if _, err := a.exe.SaveJob(ctx, a.job); err != nil {
return fmt.Errorf("switch to step copying, save job fail, %w", err)
}
return nil
}
func (a *jobArchiveExecutor) markSourcesAsSubmited(ctx context.Context, jobs []*acp.Job) error {
a.stateLock.Lock()
defer a.stateLock.Unlock()
searchableSource := a.state.Sources[:]
for _, job := range jobs {
src := entity.NewSourceFromACPJob(job)
for idx, testSrc := range searchableSource {
if src.Compare(testSrc.Source) <= 0 {
searchableSource = searchableSource[idx:]
break
}
return a.updateJob(ctx, func(job *Job, state *entity.JobArchiveState) error {
if !expect.Contains(state.Step) {
return fmt.Errorf("unexpected current step, target= '%s' expect= '%s' has= '%s'", target, expect, state.Step)
}
target := searchableSource[0]
if target == nil || !src.Equal(target.Source) {
continue
}
target.Status = entity.CopyStatus_SUBMITED
}
if _, err := a.exe.SaveJob(ctx, a.job); err != nil {
return fmt.Errorf("mark sources as submited, save job, %w", err)
}
return nil
}
func (a *jobArchiveExecutor) rollbackSources(ctx context.Context) error {
a.stateLock.Lock()
defer a.stateLock.Unlock()
for _, source := range a.state.Sources {
if source.Status == entity.CopyStatus_SUBMITED {
continue
}
source.Status = entity.CopyStatus_PENDING
}
if _, err := a.exe.SaveJob(ctx, a.job); err != nil {
return fmt.Errorf("mark sources as submited, save job, %w", err)
}
return nil
}
func (a *jobArchiveExecutor) getTodoSources() int {
a.stateLock.Lock()
defer a.stateLock.Unlock()
var todo int
for _, s := range a.state.Sources {
if s.Status == entity.CopyStatus_SUBMITED {
continue
}
todo++
}
return todo
}
func (a *jobArchiveExecutor) makeTapeFinished(ctx context.Context) {
if a.getTodoSources() > 0 {
a.submit(ctx, &entity.JobArchiveNextParam{Param: &entity.JobArchiveNextParam_WaitForTape{WaitForTape: &entity.JobArchiveWaitForTapeParam{}}})
} else {
a.submit(ctx, &entity.JobArchiveNextParam{Param: &entity.JobArchiveNextParam_Finished{Finished: &entity.JobArchiveFinishedParam{}}})
}
state.Step = target
job.Status = status
return nil
})
}
+95
View File
@@ -0,0 +1,95 @@
package executor
import (
"context"
"fmt"
"os"
"path"
"sort"
"strings"
"github.com/samuelncui/acp"
"github.com/samuelncui/yatm/entity"
)
func (a *jobArchiveExecutor) Initialize(ctx context.Context, param *entity.JobParam) error {
if err := a.applyParam(ctx, param.GetArchive()); err != nil {
return err
}
return a.dispatch(ctx, &entity.JobArchiveDispatchParam{Param: &entity.JobArchiveDispatchParam_WaitForTape{
WaitForTape: &entity.JobArchiveWaitForTapeParam{},
}})
}
func (a *jobArchiveExecutor) applyParam(ctx context.Context, param *entity.JobArchiveParam) error {
if param == nil {
return fmt.Errorf("archive param is nil")
}
return a.updateJob(ctx, func(_ *Job, state *entity.JobArchiveState) error {
var err error
sources := make([]*entity.SourceState, 0, len(param.Sources)*8)
for _, src := range param.Sources {
src.Base = strings.TrimSpace(src.Base)
if src.Base[0] != '/' {
src.Base = path.Join(a.exe.paths.Source, src.Base) + "/"
}
sources, err = a.walk(ctx, src, sources)
if err != nil {
return err
}
}
sort.Slice(sources, func(i, j int) bool {
return sources[i].Source.Compare(sources[j].Source) < 0
})
for idx, src := range sources {
if idx > 0 && sources[idx-1].Source.Equal(src.Source) {
return fmt.Errorf("have multi file with same path, path= %s", src.Source.RealPath())
}
}
state.Step = entity.JobArchiveStep_PENDING
state.Sources = sources
return nil
})
}
func (a *jobArchiveExecutor) walk(ctx context.Context, src *entity.Source, sources []*entity.SourceState) ([]*entity.SourceState, error) {
path := src.RealPath()
stat, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("walk get stat, path= '%s', %w", path, err)
}
mode := stat.Mode()
if mode.IsRegular() {
if stat.Name() == ".DS_Store" {
return sources, nil
}
return append(sources, &entity.SourceState{
Source: src,
Size: stat.Size(),
Status: entity.CopyStatus_PENDING,
}), nil
}
if mode&acp.UnexpectFileMode != 0 {
return sources, nil
}
files, err := os.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("walk read dir, path= '%s', %w", path, err)
}
for _, file := range files {
sources, err = a.walk(ctx, src.Append(file.Name()), sources)
if err != nil {
return nil, err
}
}
return sources, nil
}
-81
View File
@@ -1,81 +0,0 @@
package executor
import (
"context"
"fmt"
"os"
"path"
"sort"
"strings"
"github.com/samuelncui/acp"
"github.com/samuelncui/yatm/entity"
)
func (e *Executor) createArchive(ctx context.Context, job *Job, param *entity.JobArchiveParam) error {
var err error
sources := make([]*entity.SourceState, 0, len(param.Sources)*8)
for _, src := range param.Sources {
src.Base = strings.TrimSpace(src.Base)
if src.Base[0] != '/' {
src.Base = path.Join(e.paths.Source, src.Base) + "/"
}
sources, err = walk(ctx, src, sources)
if err != nil {
return err
}
}
sort.Slice(sources, func(i, j int) bool {
return sources[i].Source.Compare(sources[j].Source) < 0
})
for idx, src := range sources {
if idx > 0 && sources[idx-1].Source.Equal(src.Source) {
return fmt.Errorf("have multi file with same path, path= %s", src.Source.RealPath())
}
}
job.State = &entity.JobState{State: &entity.JobState_Archive{Archive: &entity.JobArchiveState{
Step: entity.JobArchiveStep_PENDING,
Sources: sources,
}}}
return nil
}
func walk(ctx context.Context, src *entity.Source, sources []*entity.SourceState) ([]*entity.SourceState, error) {
path := src.RealPath()
stat, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("walk get stat, path= '%s', %w", path, err)
}
mode := stat.Mode()
if mode.IsRegular() {
if stat.Name() == ".DS_Store" {
return sources, nil
}
return append(sources, &entity.SourceState{
Source: src,
Size: stat.Size(),
Status: entity.CopyStatus_PENDING,
}), nil
}
if mode&acp.UnexpectFileMode != 0 {
return sources, nil
}
files, err := os.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("walk read dir, path= '%s', %w", path, err)
}
for _, file := range files {
sources, err = walk(ctx, src.Append(file.Name()), sources)
if err != nil {
return nil, err
}
}
return sources, nil
}
-15
View File
@@ -1,15 +0,0 @@
package executor
import (
"context"
"github.com/samuelncui/yatm/entity"
)
func (e *Executor) startArchive(ctx context.Context, job *Job) error {
return e.Submit(ctx, job, &entity.JobNextParam{Param: &entity.JobNextParam_Archive{
Archive: &entity.JobArchiveNextParam{Param: &entity.JobArchiveNextParam_WaitForTape{
WaitForTape: &entity.JobArchiveWaitForTapeParam{},
}},
}})
}
+313
View File
@@ -0,0 +1,313 @@
package executor
import (
"context"
"encoding/hex"
"errors"
"fmt"
"os"
"os/exec"
"path"
"sort"
"strings"
"sync/atomic"
"time"
"github.com/samber/lo"
"github.com/samuelncui/acp"
"github.com/samuelncui/yatm/entity"
"github.com/samuelncui/yatm/library"
"github.com/samuelncui/yatm/tools"
)
func (a *jobArchiveExecutor) makeTape(ctx context.Context, device, barcode, name string) (rerr error) {
barcode = strings.ToUpper(barcode)
state := a.getState()
if state == nil {
return fmt.Errorf("cannot found archive state, abort")
}
if !a.exe.OccupyDevice(device) {
return fmt.Errorf("device is using, device= %s", device)
}
defer a.exe.ReleaseDevice(device)
defer a.makeTapeFinished(tools.WithoutTimeout(ctx))
encryption, keyPath, keyRecycle, err := a.exe.newKey()
if err != nil {
return err
}
defer keyRecycle()
if err := runCmd(a.logger, a.exe.makeEncryptCmd(ctx, device, keyPath, barcode, name)); err != nil {
return fmt.Errorf("run encrypt script fail, %w", err)
}
mkfsCmd := exec.CommandContext(ctx, a.exe.scripts.Mkfs)
mkfsCmd.Env = append(mkfsCmd.Env, fmt.Sprintf("DEVICE=%s", device), fmt.Sprintf("TAPE_BARCODE=%s", barcode), fmt.Sprintf("TAPE_NAME=%s", name))
if err := runCmd(a.logger, mkfsCmd); err != nil {
return fmt.Errorf("run mkfs script fail, %w", err)
}
mountPoint, err := os.MkdirTemp("", "*.ltfs")
if err != nil {
return fmt.Errorf("create temp mountpoint, %w", err)
}
mountCmd := exec.CommandContext(ctx, a.exe.scripts.Mount)
mountCmd.Env = append(mountCmd.Env, fmt.Sprintf("DEVICE=%s", device), fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(a.logger, mountCmd); err != nil {
return fmt.Errorf("run mount script fail, %w", err)
}
defer func() {
umountCmd := exec.CommandContext(tools.WithoutTimeout(ctx), a.exe.scripts.Umount)
umountCmd.Env = append(umountCmd.Env, fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(a.logger, umountCmd); err != nil {
a.logger.WithContext(ctx).WithError(err).Errorf("run umount script fail, %s", mountPoint)
return
}
if err := os.Remove(mountPoint); err != nil {
a.logger.WithContext(ctx).WithError(err).Errorf("remove mount point fail, %s", mountPoint)
return
}
}()
wildcardJobOpts := make([]acp.WildcardJobOption, 0, 6)
wildcardJobOpts = append(wildcardJobOpts, acp.Target(mountPoint))
for _, source := range state.Sources {
if source.Status == entity.CopyStatus_SUBMITED {
continue
}
wildcardJobOpts = append(wildcardJobOpts, acp.AccurateSource(source.Source.Base, source.Source.Path))
}
opts := make([]acp.Option, 0, 4)
opts = append(opts, acp.WildcardJob(wildcardJobOpts...))
opts = append(opts, acp.WithHash(true))
opts = append(opts, acp.SetToDevice(acp.LinearDevice(true)))
opts = append(opts, acp.WithLogger(a.logger))
reportHander, reportGetter := acp.NewReportGetter()
opts = append(opts, acp.WithEventHandler(reportHander))
a.progress = newProgress()
defer func() { a.progress = nil }()
var dropToReadonly bool
opts = append(opts, acp.WithEventHandler(func(ev acp.Event) {
switch e := ev.(type) {
case *acp.EventUpdateCount:
atomic.StoreInt64(&a.progress.totalBytes, e.Bytes)
atomic.StoreInt64(&a.progress.totalFiles, e.Files)
return
case *acp.EventUpdateProgress:
a.progress.setBytes(e.Bytes)
atomic.StoreInt64(&a.progress.files, e.Files)
return
case *acp.EventReportError:
a.logger.WithContext(ctx).Errorf("acp report error, src= '%s' dst= '%s' err= '%s'", e.Error.Src, e.Error.Dst, e.Error.Err)
return
case *acp.EventUpdateJob:
job := e.Job
src := entity.NewSourceFromACPJob(job)
var targetStatus entity.CopyStatus
switch job.Status {
case acp.JobStatusPending, acp.JobStatusPreparing:
targetStatus = entity.CopyStatus_PENDING
case acp.JobStatusCopying:
targetStatus = entity.CopyStatus_RUNNING
case acp.JobStatusFinished:
targetStatus = entity.CopyStatus_FAILED
if len(job.SuccessTargets) > 0 {
a.logger.WithContext(ctx).Infof("file '%s' copy success, size= %d", src.RealPath(), job.Size)
targetStatus = entity.CopyStatus_STAGED
break // break from switch
}
for dst, err := range job.FailTargets {
if err == nil {
continue
}
if errors.Is(err, acp.ErrTargetNoSpace) {
continue
}
a.logger.WithContext(ctx).WithError(err).Errorf("file '%s' copy fail, dst= '%s'", src.RealPath(), dst)
if errors.Is(err, acp.ErrTargetDropToReadonly) {
dropToReadonly = true
}
}
default:
return
}
a.updateJob(ctx, func(_ *Job, state *entity.JobArchiveState) error {
idx := sort.Search(len(state.Sources), func(idx int) bool {
return src.Compare(state.Sources[idx].Source) <= 0
})
if idx < 0 || idx >= len(state.Sources) || src.Compare(state.Sources[idx].Source) != 0 {
return fmt.Errorf(
"cannot found target file, real_path= %s found_index= %d tape_file_path= %v", src.RealPath(), idx,
lo.Map(state.Sources, func(source *entity.SourceState, _ int) string { return source.Source.RealPath() }),
)
}
founded := state.Sources[idx]
if founded == nil || !src.Equal(founded.Source) {
return fmt.Errorf(
"founded file not match, real_path= %s found_path= %s tape_file_path= %v", src.RealPath(), founded.Source.RealPath(),
lo.Map(state.Sources, func(source *entity.SourceState, _ int) string { return source.Source.RealPath() }),
)
}
founded.Status = targetStatus
return nil
})
}
}))
defer func() {
ctx := tools.WithoutTimeout(ctx)
// if tape drop to readonly, ltfs cannot write index to partition a.
// rollback sources for next try.
if dropToReadonly {
a.logger.WithContext(ctx).Errorf("tape filesystem had droped to readonly, rollback, barcode= '%s'", barcode)
a.rollbackSources(ctx)
return
}
report := reportGetter()
sort.Slice(report.Jobs, func(i, j int) bool {
return entity.NewSourceFromACPJob(report.Jobs[i]).Compare(entity.NewSourceFromACPJob(report.Jobs[j])) < 0
})
reportFile, err := a.exe.newReportWriter(barcode)
if err != nil {
a.logger.WithContext(ctx).WithError(err).Warnf("open report file fail, barcode= '%s'", barcode)
} else {
defer reportFile.Close()
tools.WrapWithLogger(ctx, a.logger, func() {
reportFile.Write([]byte(report.ToJSONString(false)))
})
}
filteredJobs := make([]*acp.Job, 0, len(report.Jobs))
files := make([]*library.TapeFile, 0, len(report.Jobs))
for _, job := range report.Jobs {
if len(job.SuccessTargets) == 0 {
continue
}
if !job.Mode.IsRegular() {
continue
}
hash, err := hex.DecodeString(job.SHA256)
if err != nil {
a.logger.WithContext(ctx).WithError(err).Warnf("decode sha256 fail, path= '%s'", entity.NewSourceFromACPJob(job).RealPath())
continue
}
files = append(files, &library.TapeFile{
Path: path.Join(job.Path...),
Size: job.Size,
Mode: job.Mode,
ModTime: job.ModTime,
WriteTime: job.WriteTime,
Hash: hash,
})
filteredJobs = append(filteredJobs, job)
}
tape, err := a.exe.lib.CreateTape(ctx, &library.Tape{
Barcode: barcode,
Name: name,
Encryption: encryption,
CreateTime: time.Now(),
}, files)
if err != nil {
rerr = tools.AppendError(rerr, fmt.Errorf("create tape fail, barcode= '%s' name= '%s', %w", barcode, name, err))
return
}
a.logger.Infof("create tape success, tape_id= %d", tape.ID)
if err := a.exe.lib.TrimFiles(ctx); err != nil {
a.logger.WithError(err).Warnf("trim library files fail")
}
if err := a.markSourcesAsSubmited(ctx, filteredJobs); err != nil {
rerr = tools.AppendError(rerr, fmt.Errorf("mark source as submited fail, %w", err))
return
}
}()
copyer, err := acp.New(ctx, opts...)
if err != nil {
rerr = fmt.Errorf("start copy fail, %w", err)
return
}
copyer.Wait()
return
}
func (a *jobArchiveExecutor) markSourcesAsSubmited(ctx context.Context, jobs []*acp.Job) error {
return a.updateJob(ctx, func(_ *Job, state *entity.JobArchiveState) error {
searchableSource := state.Sources[:]
for _, job := range jobs {
src := entity.NewSourceFromACPJob(job)
for idx, testSrc := range searchableSource {
if src.Compare(testSrc.Source) <= 0 {
searchableSource = searchableSource[idx:]
break
}
}
target := searchableSource[0]
if target == nil || !src.Equal(target.Source) {
continue
}
target.Status = entity.CopyStatus_SUBMITED
}
return nil
})
}
func (a *jobArchiveExecutor) rollbackSources(ctx context.Context) error {
return a.updateJob(ctx, func(_ *Job, state *entity.JobArchiveState) error {
for _, source := range state.Sources {
if source.Status == entity.CopyStatus_SUBMITED {
continue
}
source.Status = entity.CopyStatus_PENDING
}
return nil
})
}
func (a *jobArchiveExecutor) getTodoSources() int {
state := a.getState()
var todo int
for _, s := range state.Sources {
if s.Status == entity.CopyStatus_SUBMITED {
continue
}
todo++
}
return todo
}
func (a *jobArchiveExecutor) makeTapeFinished(ctx context.Context) {
if a.getTodoSources() > 0 {
a.dispatch(ctx, &entity.JobArchiveDispatchParam{Param: &entity.JobArchiveDispatchParam_WaitForTape{WaitForTape: &entity.JobArchiveWaitForTapeParam{}}})
} else {
a.dispatch(ctx, &entity.JobArchiveDispatchParam{Param: &entity.JobArchiveDispatchParam_Finished{Finished: &entity.JobArchiveFinishedParam{}}})
}
}
+1 -57
View File
@@ -1,59 +1,3 @@
package executor
import (
"context"
"fmt"
"os"
"os/exec"
"time"
"github.com/samuelncui/yatm/library"
"github.com/sirupsen/logrus"
)
func (e *jobRestoreExecutor) loadTape(ctx context.Context, device string, tape *library.Tape) error {
if !e.exe.occupyDevice(device) {
return fmt.Errorf("device is using, device= %s", device)
}
defer e.exe.releaseDevice(device)
keyPath, keyRecycle, err := e.exe.restoreKey(tape.Encryption)
if err != nil {
return err
}
defer func() {
time.Sleep(time.Second)
keyRecycle()
}()
logger := logrus.StandardLogger()
if err := runCmd(logger, e.exe.makeEncryptCmd(ctx, device, keyPath, tape.Barcode, tape.Name)); err != nil {
return fmt.Errorf("run encrypt script fail, %w", err)
}
mountPoint, err := os.MkdirTemp("", "*.ltfs")
if err != nil {
return fmt.Errorf("create temp mountpoint, %w", err)
}
mountCmd := exec.CommandContext(ctx, e.exe.scripts.Mount)
mountCmd.Env = append(mountCmd.Env, fmt.Sprintf("DEVICE=%s", device), fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(logger, mountCmd); err != nil {
return fmt.Errorf("run mount script fail, %w", err)
}
// defer func() {
// umountCmd := exec.CommandContext(ctx, e.umountScript)
// umountCmd.Env = append(umountCmd.Env, fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
// if err := runCmd(logger, umountCmd); err != nil {
// logger.WithContext(ctx).WithError(err).Errorf("run umount script fail, %s", mountPoint)
// return
// }
// if err := os.Remove(mountPoint); err != nil {
// logger.WithContext(ctx).WithError(err).Errorf("remove mount point fail, %s", mountPoint)
// return
// }
// }()
return nil
}
type jobTypeRestore struct{}
-25
View File
@@ -1,25 +0,0 @@
package executor
import (
"context"
"sync/atomic"
"github.com/samuelncui/yatm/entity"
)
func (e *Executor) getRestoreDisplay(ctx context.Context, job *Job) (*entity.JobRestoreDisplay, error) {
display := new(entity.JobRestoreDisplay)
if exe := e.getRestoreExecutor(ctx, job); exe != nil && exe.progress != nil {
display.CopiedBytes = atomic.LoadInt64(&exe.progress.bytes)
display.CopiedFiles = atomic.LoadInt64(&exe.progress.files)
display.TotalBytes = atomic.LoadInt64(&exe.progress.totalBytes)
display.TotalFiles = atomic.LoadInt64(&exe.progress.totalFiles)
display.StartTime = exe.progress.startTime.Unix()
speed := atomic.LoadInt64(&exe.progress.speed)
display.Speed = &speed
}
return display, nil
}
+79 -283
View File
@@ -2,44 +2,30 @@ package executor
import (
"context"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"path"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
mapset "github.com/deckarep/golang-set/v2"
jsoniter "github.com/json-iterator/go"
"github.com/samber/lo"
"github.com/samuelncui/acp"
"github.com/samuelncui/yatm/entity"
"github.com/samuelncui/yatm/tools"
"github.com/sirupsen/logrus"
)
var (
runningRestores sync.Map
)
type jobRestoreExecutor struct {
lock sync.Mutex
exe *Executor
job *Job
func (e *Executor) getRestoreExecutor(ctx context.Context, job *Job) *jobRestoreExecutor {
if running, has := runningRestores.Load(job.ID); has {
return running.(*jobRestoreExecutor)
}
return nil
progress *progress
logFile *os.File
logger *logrus.Logger
}
func (e *Executor) newRestoreExecutor(ctx context.Context, job *Job) (*jobRestoreExecutor, error) {
if exe := e.getRestoreExecutor(ctx, job); exe != nil {
return exe, nil
}
logFile, err := e.newLogWriter(job.ID)
func (*jobTypeRestore) GetExecutor(ctx context.Context, exe *Executor, job *Job) (JobExecutor, error) {
logFile, err := exe.newLogWriter(job.ID)
if err != nil {
return nil, fmt.Errorf("get log writer fail, %w", err)
}
@@ -47,39 +33,27 @@ func (e *Executor) newRestoreExecutor(ctx context.Context, job *Job) (*jobRestor
logger := logrus.New()
logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
exe := &jobRestoreExecutor{
exe: e,
e := &jobRestoreExecutor{
exe: exe,
job: job,
state: job.State.GetRestore(),
logFile: logFile,
logger: logger,
}
runningRestores.Store(job.ID, exe)
return exe, nil
return e, nil
}
type jobRestoreExecutor struct {
exe *Executor
job *Job
stateLock sync.Mutex
state *entity.JobRestoreState
progress *progress
logFile *os.File
logger *logrus.Logger
}
func (a *jobRestoreExecutor) submit(ctx context.Context, param *entity.JobRestoreNextParam) {
if err := a.handle(ctx, param); err != nil {
a.logger.WithContext(ctx).WithError(err).Infof("handler param fail, param= %s", param)
func (a *jobRestoreExecutor) Dispatch(ctx context.Context, next *entity.JobDispatchParam) error {
param := next.GetRestore()
if param == nil {
return fmt.Errorf("unexpected next param type, unexpected= JobRestoreDispatchParam, has= %s", next)
}
return a.dispatch(ctx, param)
}
func (a *jobRestoreExecutor) handle(ctx context.Context, param *entity.JobRestoreNextParam) error {
func (a *jobRestoreExecutor) dispatch(ctx context.Context, param *entity.JobRestoreDispatchParam) error {
if p := param.GetCopying(); p != nil {
if err := a.switchStep(
ctx, entity.JobRestoreStep_COPYING, entity.JobStatus_PROCESSING,
@@ -102,7 +76,7 @@ func (a *jobRestoreExecutor) handle(ctx context.Context, param *entity.JobRestor
if p := param.GetWaitForTape(); p != nil {
return a.switchStep(
ctx, entity.JobRestoreStep_WAIT_FOR_TAPE, entity.JobStatus_PROCESSING,
ctx, entity.JobRestoreStep_WAIT_FOR_TAPE, entity.JobStatus_PENDING,
mapset.NewThreadUnsafeSet(entity.JobRestoreStep_PENDING, entity.JobRestoreStep_COPYING),
)
}
@@ -115,258 +89,80 @@ func (a *jobRestoreExecutor) handle(ctx context.Context, param *entity.JobRestor
return err
}
a.logFile.Close()
runningRestores.Delete(a.job.ID)
return nil
return a.Close(ctx)
}
return nil
}
func (a *jobRestoreExecutor) restoreTape(ctx context.Context, device string) (rerr error) {
if !a.exe.occupyDevice(device) {
return fmt.Errorf("device is using, device= %s", device)
}
defer a.exe.releaseDevice(device)
defer func() {
if _, found := lo.Find(a.state.Tapes, func(item *entity.RestoreTape) bool {
return item.Status != entity.CopyStatus_SUBMITED
}); found {
a.submit(tools.WithoutTimeout(ctx), &entity.JobRestoreNextParam{
Param: &entity.JobRestoreNextParam_WaitForTape{WaitForTape: &entity.JobRestoreWaitForTapeParam{}},
})
return
}
a.submit(tools.WithoutTimeout(ctx), &entity.JobRestoreNextParam{
Param: &entity.JobRestoreNextParam_Finished{Finished: &entity.JobRestoreFinishedParam{}},
})
}()
readInfoCmd := exec.CommandContext(ctx, a.exe.scripts.ReadInfo)
readInfoCmd.Env = append(readInfoCmd.Env, fmt.Sprintf("DEVICE=%s", device))
infoBuf, err := runCmdWithReturn(a.logger, readInfoCmd)
if err != nil {
return fmt.Errorf("run read info script fail, %w", err)
func (a *jobRestoreExecutor) Display(ctx context.Context) (*entity.JobDisplay, error) {
p := a.progress
if p == nil {
return nil, nil
}
barcode := jsoniter.Get(infoBuf, "barcode").ToString()
if len(barcode) > 6 {
barcode = barcode[:6]
display := new(entity.JobRestoreDisplay)
display.CopiedBytes = atomic.LoadInt64(&p.bytes)
display.CopiedFiles = atomic.LoadInt64(&p.files)
display.TotalBytes = atomic.LoadInt64(&p.totalBytes)
display.TotalFiles = atomic.LoadInt64(&p.totalFiles)
display.StartTime = p.startTime.Unix()
speed := atomic.LoadInt64(&p.speed)
display.Speed = &speed
return &entity.JobDisplay{Display: &entity.JobDisplay_Restore{Restore: display}}, nil
}
func (a *jobRestoreExecutor) Close(ctx context.Context) error {
a.logFile.Close()
a.exe.RemoveJobExecutor(ctx, a.job.ID)
return nil
}
func (a *jobRestoreExecutor) Logger() *logrus.Logger {
return a.logger
}
func (a *jobRestoreExecutor) getState() *entity.JobRestoreState {
a.lock.Lock()
defer a.lock.Unlock()
if a.job.State == nil || a.job.State.GetRestore() == nil {
a.job.State = &entity.JobState{State: &entity.JobState_Restore{Restore: &entity.JobRestoreState{}}}
}
restoreTape, found := lo.Find(a.state.Tapes, func(t *entity.RestoreTape) bool {
return t.Barcode == barcode
})
if !found || restoreTape == nil {
expects := lo.Map(a.state.Tapes, func(t *entity.RestoreTape, _ int) string { return t.Barcode })
return fmt.Errorf("unexpected tape barcode in library, has= '%s' expect= %v", barcode, expects)
}
if restoreTape.Status == entity.CopyStatus_SUBMITED {
return fmt.Errorf("unexpected restore tape state status, tape is restored, status= '%s'", restoreTape.Status)
return a.job.State.GetRestore()
}
func (a *jobRestoreExecutor) updateJob(ctx context.Context, change func(*Job, *entity.JobRestoreState) error) error {
a.lock.Lock()
defer a.lock.Unlock()
if a.job.State == nil || a.job.State.GetRestore() == nil {
a.job.State = &entity.JobState{State: &entity.JobState_Restore{Restore: &entity.JobRestoreState{}}}
}
tape, err := a.exe.lib.GetTape(ctx, restoreTape.TapeId)
if err != nil {
return fmt.Errorf("get tape info fail, barcode= '%s' id= %d, %w", restoreTape.Barcode, restoreTape.TapeId, err)
if err := change(a.job, a.job.State.GetRestore()); err != nil {
a.logger.WithContext(ctx).WithError(err).Warnf("update state failed while exec change callback")
return fmt.Errorf("update state failed while exec change callback, %w", err)
}
if _, err := a.exe.SaveJob(ctx, a.job); err != nil {
a.logger.WithContext(ctx).WithError(err).Warnf("update state failed while save job")
return fmt.Errorf("update state failed while save job, %w", err)
}
keyPath, keyRecycle, err := a.exe.restoreKey(tape.Encryption)
if err != nil {
return err
}
defer func() {
time.Sleep(time.Second)
keyRecycle()
}()
if err := runCmd(a.logger, a.exe.makeEncryptCmd(ctx, device, keyPath, barcode, tape.Name)); err != nil {
return fmt.Errorf("run encrypt script fail, %w", err)
}
mountPoint, err := os.MkdirTemp("", "*.ltfs")
if err != nil {
return fmt.Errorf("create temp mountpoint, %w", err)
}
sourcePath := tools.Cache(func(p string) string { return path.Join(mountPoint, p) })
mountCmd := exec.CommandContext(ctx, a.exe.scripts.Mount)
mountCmd.Env = append(mountCmd.Env, fmt.Sprintf("DEVICE=%s", device), fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(a.logger, mountCmd); err != nil {
return fmt.Errorf("run mount script fail, %w", err)
}
defer func() {
umountCmd := exec.CommandContext(tools.WithoutTimeout(ctx), a.exe.scripts.Umount)
umountCmd.Env = append(umountCmd.Env, fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(a.logger, umountCmd); err != nil {
a.logger.WithContext(ctx).WithError(err).Errorf("run umount script fail, %s", mountPoint)
return
}
if err := os.Remove(mountPoint); err != nil {
a.logger.WithContext(ctx).WithError(err).Errorf("remove mount point fail, %s", mountPoint)
return
}
}()
opts := make([]acp.Option, 0, 16)
for _, f := range restoreTape.Files {
if f.Status == entity.CopyStatus_SUBMITED {
continue
}
opts = append(opts, acp.AccurateJob(sourcePath(f.TapePath), []string{path.Join(a.exe.paths.Target, f.TargetPath)}))
}
opts = append(opts, acp.WithHash(true))
opts = append(opts, acp.SetFromDevice(acp.LinearDevice(true)))
opts = append(opts, acp.WithLogger(a.logger))
a.progress = newProgress()
defer func() { a.progress = nil }()
convertPath := tools.Cache(func(p string) string { return strings.ReplaceAll(p, "/", "\x00") })
opts = append(opts, acp.WithEventHandler(func(ev acp.Event) {
switch e := ev.(type) {
case *acp.EventUpdateCount:
atomic.StoreInt64(&a.progress.totalBytes, e.Bytes)
atomic.StoreInt64(&a.progress.totalFiles, e.Files)
return
case *acp.EventUpdateProgress:
a.progress.setBytes(e.Bytes)
atomic.StoreInt64(&a.progress.files, e.Files)
return
case *acp.EventReportError:
a.logger.WithContext(ctx).Errorf("acp report error, src= '%s' dst= '%s' err= '%s'", e.Error.Src, e.Error.Dst, e.Error.Err)
return
case *acp.EventUpdateJob:
job := e.Job
src := entity.NewSourceFromACPJob(job)
var targetStatus entity.CopyStatus
switch job.Status {
case "pending":
targetStatus = entity.CopyStatus_PENDING
case "preparing":
targetStatus = entity.CopyStatus_RUNNING
case "finished":
a.logger.WithContext(ctx).Infof("file '%s' copy finished, size= %d", src.RealPath(), job.Size)
targetStatus = entity.CopyStatus_STAGED
if len(job.FailTargets) > 0 {
targetStatus = entity.CopyStatus_FAILED
}
for dst, err := range job.FailTargets {
if err == nil {
continue
}
a.logger.WithContext(ctx).WithError(err).Errorf("file '%s' copy fail, dst= '%s'", src.RealPath(), dst)
}
default:
return
}
a.stateLock.Lock()
defer a.stateLock.Unlock()
realPath := src.RealPath()
idx := sort.Search(len(restoreTape.Files), func(idx int) bool {
return convertPath(realPath) <= convertPath(sourcePath(restoreTape.Files[idx].TapePath))
})
if idx < 0 || idx >= len(restoreTape.Files) {
a.logger.Warnf(
"cannot found target file, real_path= %s found_index= %d tape_file_path= %v", realPath, idx,
lo.Map(restoreTape.Files, func(file *entity.RestoreFile, _ int) string { return sourcePath(file.TapePath) }),
)
return
}
targetFile := restoreTape.Files[idx]
if targetFile == nil || realPath != sourcePath(targetFile.TapePath) {
a.logger.Warnf(
"cannot match target file, real_path= %s found_index= %d found_file_path= %s",
realPath, idx, sourcePath(targetFile.TapePath),
)
return
}
if targetStatus == entity.CopyStatus_STAGED {
if targetHash := hex.EncodeToString(targetFile.Hash); targetHash != job.SHA256 {
targetStatus = entity.CopyStatus_FAILED
a.logger.Warnf(
"copy checksum do not match target file hash, real_path= %s target_hash= %s copy_hash= %s",
realPath, targetHash, job.SHA256,
)
}
if targetSize := targetFile.Size; targetSize != job.Size {
targetStatus = entity.CopyStatus_FAILED
a.logger.Warnf(
"copy size do not match target file hash, real_path= %s target_size= %d copy_size= %d",
realPath, targetSize, job.Size,
)
}
}
targetFile.Status = targetStatus
if _, err := a.exe.SaveJob(ctx, a.job); err != nil {
a.logger.WithContext(ctx).Infof("save job for update file fail, name= %s", job.Base+path.Join(job.Path...))
}
return
}
}))
restoreTape.Status = entity.CopyStatus_RUNNING
if _, err := a.exe.SaveJob(tools.WithoutTimeout(ctx), a.job); err != nil {
a.logger.WithContext(ctx).Infof("save job for submit tape fail, barcode= %s", restoreTape.Barcode)
}
defer func() {
a.stateLock.Lock()
defer a.stateLock.Unlock()
restoreTape.Status = entity.CopyStatus_SUBMITED
for _, file := range restoreTape.Files {
if file.Status == entity.CopyStatus_STAGED {
file.Status = entity.CopyStatus_SUBMITED
}
if file.Status != entity.CopyStatus_SUBMITED {
restoreTape.Status = entity.CopyStatus_FAILED
}
}
if _, err := a.exe.SaveJob(tools.WithoutTimeout(ctx), a.job); err != nil {
a.logger.WithContext(ctx).Infof("save job for submit tape fail, barcode= %s", restoreTape.Barcode)
}
}()
copyer, err := acp.New(ctx, opts...)
if err != nil {
rerr = fmt.Errorf("start copy fail, %w", err)
return
}
copyer.Wait()
return
return nil
}
func (a *jobRestoreExecutor) switchStep(ctx context.Context, target entity.JobRestoreStep, status entity.JobStatus, expect mapset.Set[entity.JobRestoreStep]) error {
a.stateLock.Lock()
defer a.stateLock.Unlock()
return a.updateJob(ctx, func(job *Job, state *entity.JobRestoreState) error {
if !expect.Contains(state.Step) {
return fmt.Errorf("unexpected current step, target= '%s' expect= '%s' has= '%s'", target, expect, state.Step)
}
if !expect.Contains(a.state.Step) {
return fmt.Errorf("unexpected current step, target= '%s' expect= '%s' has= '%s'", target, expect, a.state.Step)
}
a.state.Step = target
a.job.Status = status
if _, err := a.exe.SaveJob(ctx, a.job); err != nil {
return fmt.Errorf("switch to step copying, save job fail, %w", err)
}
return nil
state.Step = target
job.Status = status
return nil
})
}
+249
View File
@@ -0,0 +1,249 @@
package executor
import (
"context"
"fmt"
"io/fs"
"sort"
"strings"
mapset "github.com/deckarep/golang-set/v2"
"github.com/samber/lo"
"github.com/samuelncui/yatm/entity"
"github.com/samuelncui/yatm/library"
"github.com/samuelncui/yatm/tools"
"github.com/sirupsen/logrus"
)
func (a *jobRestoreExecutor) Initialize(ctx context.Context, param *entity.JobParam) error {
if err := a.applyParam(ctx, param.GetRestore()); err != nil {
return err
}
return a.dispatch(ctx, &entity.JobRestoreDispatchParam{Param: &entity.JobRestoreDispatchParam_WaitForTape{
WaitForTape: &entity.JobRestoreWaitForTapeParam{},
}})
}
type restoreFile struct {
*library.File
target string
}
func (a *jobRestoreExecutor) applyParam(ctx context.Context, param *entity.JobRestoreParam) error {
if param == nil {
return fmt.Errorf("restore param is nil")
}
return a.updateJob(ctx, func(_ *Job, state *entity.JobRestoreState) error {
exe := a.exe
files, err := exe.getRestoreFiles(ctx, param.FileIds...)
if err != nil {
return fmt.Errorf("get restore files fail, ids= %v, %w", param.FileIds, err)
}
fileIDs := make([]int64, 0, len(files))
for _, file := range files {
fileIDs = append(fileIDs, file.ID)
}
positions, err := exe.lib.MGetPositionByFileID(ctx, fileIDs...)
if err != nil {
return err
}
tapeMapping := make(map[int64]mapset.Set[int64], 4)
for _, file := range files {
for _, posi := range positions[file.ID] {
set := tapeMapping[posi.TapeID]
if set == nil {
tapeMapping[posi.TapeID] = mapset.NewThreadUnsafeSet(file.ID)
continue
}
set.Add(file.ID)
}
}
tapes, err := exe.lib.MGetTape(ctx, lo.Keys(tapeMapping)...)
if err != nil {
return err
}
for tapeID := range tapeMapping {
if tape, has := tapes[tapeID]; has && tape != nil {
continue
}
logrus.WithContext(ctx).Infof("tape not found, tape_id= %d", tapeID)
delete(tapeMapping, tapeID)
}
restoreTapes := make([]*entity.RestoreTape, 0, len(tapeMapping))
for len(tapeMapping) > 0 {
var maxTapeID int64
for tapeID, files := range tapeMapping {
if maxTapeID == 0 {
maxTapeID = tapeID
continue
}
diff := files.Cardinality() - tapeMapping[maxTapeID].Cardinality()
if diff > 0 {
maxTapeID = tapeID
continue
}
if diff < 0 {
continue
}
if tapeID < maxTapeID {
maxTapeID = tapeID
continue
}
}
if maxTapeID == 0 {
return fmt.Errorf("max tape not found, tape_ids= %v", lo.Keys(tapeMapping))
}
fileIDs := tapeMapping[maxTapeID]
delete(tapeMapping, maxTapeID)
if fileIDs.Cardinality() == 0 {
continue
}
for i, f := range tapeMapping {
tapeMapping[i] = f.Difference(fileIDs)
}
targets := make([]*entity.RestoreFile, 0, fileIDs.Cardinality())
for _, fileID := range fileIDs.ToSlice() {
file := files[fileID]
if file == nil {
continue
}
posi := positions[fileID]
if len(posi) == 0 {
logrus.WithContext(ctx).Infof("file position not found, file_id= %d", fileID)
continue
}
for _, p := range posi {
if p.TapeID != maxTapeID {
continue
}
targets = append(targets, &entity.RestoreFile{
FileId: file.ID,
TapeId: p.TapeID,
PositionId: p.ID,
Status: entity.CopyStatus_PENDING,
Size: file.Size,
Hash: file.Hash,
TapePath: p.Path,
TargetPath: file.target,
})
break
}
}
convertPath := tools.ThreadUnsafeCache(func(p string) string { return strings.ReplaceAll(p, "/", "\x00") })
sort.Slice(targets, func(i, j int) bool {
return convertPath(targets[i].TapePath) < convertPath(targets[j].TapePath)
})
restoreTapes = append(restoreTapes, &entity.RestoreTape{
TapeId: maxTapeID,
Barcode: tapes[maxTapeID].Barcode,
Status: entity.CopyStatus_PENDING,
Files: targets,
})
}
state.Step = entity.JobRestoreStep_PENDING
state.Tapes = restoreTapes
return nil
})
}
func (e *Executor) getRestoreFiles(ctx context.Context, rootIDs ...int64) (map[int64]*restoreFile, error) {
rootIDSet := mapset.NewThreadUnsafeSet(rootIDs...)
for _, id := range rootIDs {
parents, err := e.lib.ListParents(ctx, id)
if err != nil {
return nil, err
}
if len(parents) <= 1 {
continue
}
for _, parent := range parents[:len(parents)-1] {
if !rootIDSet.Contains(parent.ID) {
continue
}
rootIDSet.Remove(id)
break
}
}
rootIDs = rootIDSet.ToSlice()
mapping, err := e.lib.MGetFile(ctx, rootIDs...)
if err != nil {
return nil, fmt.Errorf("mget file fail, ids= %v, %w", rootIDs, err)
}
files := make([]*restoreFile, 0, len(rootIDs)*8)
visited := mapset.NewThreadUnsafeSet[int64]()
for _, root := range mapping {
if visited.Contains(root.ID) {
continue
}
visited.Add(root.ID)
if !fs.FileMode(root.Mode).IsDir() {
files = append(files, &restoreFile{File: root, target: root.Name})
continue
}
found, err := e.visitFiles(ctx, root.Name, nil, visited, root.ID)
if err != nil {
return nil, err
}
files = append(files, found...)
}
results := make(map[int64]*restoreFile, len(files))
for _, f := range files {
results[f.ID] = f
}
return results, nil
}
func (e *Executor) visitFiles(ctx context.Context, path string, files []*restoreFile, visited mapset.Set[int64], parentID int64) ([]*restoreFile, error) {
children, err := e.lib.List(ctx, parentID)
if err != nil {
return nil, err
}
for _, child := range children {
if visited.Contains(child.ID) {
continue
}
visited.Add(child.ID)
target := path + "/" + child.Name
if !fs.FileMode(child.Mode).IsDir() {
files = append(files, &restoreFile{File: child, target: target})
continue
}
files, err = e.visitFiles(ctx, target, files, visited, child.ID)
if err != nil {
return nil, err
}
}
return files, nil
}
+59
View File
@@ -0,0 +1,59 @@
package executor
import (
"context"
"fmt"
"os"
"os/exec"
"time"
"github.com/samuelncui/yatm/library"
"github.com/sirupsen/logrus"
)
func (e *jobRestoreExecutor) loadTape(ctx context.Context, device string, tape *library.Tape) error {
if !e.exe.OccupyDevice(device) {
return fmt.Errorf("device is using, device= %s", device)
}
defer e.exe.ReleaseDevice(device)
keyPath, keyRecycle, err := e.exe.restoreKey(tape.Encryption)
if err != nil {
return err
}
defer func() {
time.Sleep(time.Second)
keyRecycle()
}()
logger := logrus.StandardLogger()
if err := runCmd(logger, e.exe.makeEncryptCmd(ctx, device, keyPath, tape.Barcode, tape.Name)); err != nil {
return fmt.Errorf("run encrypt script fail, %w", err)
}
mountPoint, err := os.MkdirTemp("", "*.ltfs")
if err != nil {
return fmt.Errorf("create temp mountpoint, %w", err)
}
mountCmd := exec.CommandContext(ctx, e.exe.scripts.Mount)
mountCmd.Env = append(mountCmd.Env, fmt.Sprintf("DEVICE=%s", device), fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(logger, mountCmd); err != nil {
return fmt.Errorf("run mount script fail, %w", err)
}
// defer func() {
// umountCmd := exec.CommandContext(ctx, e.umountScript)
// umountCmd.Env = append(umountCmd.Env, fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
// if err := runCmd(logger, umountCmd); err != nil {
// logger.WithContext(ctx).WithError(err).Errorf("run umount script fail, %s", mountPoint)
// return
// }
// if err := os.Remove(mountPoint); err != nil {
// logger.WithContext(ctx).WithError(err).Errorf("remove mount point fail, %s", mountPoint)
// return
// }
// }()
return nil
}
-233
View File
@@ -1,233 +0,0 @@
package executor
import (
"context"
"fmt"
"io/fs"
"sort"
"strings"
mapset "github.com/deckarep/golang-set/v2"
"github.com/samber/lo"
"github.com/samuelncui/yatm/entity"
"github.com/samuelncui/yatm/library"
"github.com/samuelncui/yatm/tools"
"github.com/sirupsen/logrus"
)
type restoreFile struct {
*library.File
target string
}
func (e *Executor) createRestore(ctx context.Context, job *Job, param *entity.JobRestoreParam) error {
files, err := e.getRestoreFiles(ctx, param.FileIds...)
if err != nil {
return fmt.Errorf("get restore files fail, ids= %v, %w", param.FileIds, err)
}
fileIDs := make([]int64, 0, len(files))
for _, file := range files {
fileIDs = append(fileIDs, file.ID)
}
positions, err := e.lib.MGetPositionByFileID(ctx, fileIDs...)
if err != nil {
return err
}
tapeMapping := make(map[int64]mapset.Set[int64], 4)
for _, file := range files {
for _, posi := range positions[file.ID] {
set := tapeMapping[posi.TapeID]
if set == nil {
tapeMapping[posi.TapeID] = mapset.NewThreadUnsafeSet(file.ID)
continue
}
set.Add(file.ID)
}
}
tapes, err := e.lib.MGetTape(ctx, lo.Keys(tapeMapping)...)
if err != nil {
return err
}
for tapeID := range tapeMapping {
if tape, has := tapes[tapeID]; has && tape != nil {
continue
}
logrus.WithContext(ctx).Infof("tape not found, tape_id= %d", tapeID)
delete(tapeMapping, tapeID)
}
restoreTapes := make([]*entity.RestoreTape, 0, len(tapeMapping))
for len(tapeMapping) > 0 {
var maxTapeID int64
for tapeID, files := range tapeMapping {
if maxTapeID == 0 {
maxTapeID = tapeID
continue
}
diff := files.Cardinality() - tapeMapping[maxTapeID].Cardinality()
if diff > 0 {
maxTapeID = tapeID
continue
}
if diff < 0 {
continue
}
if tapeID < maxTapeID {
maxTapeID = tapeID
continue
}
}
if maxTapeID == 0 {
return fmt.Errorf("max tape not found, tape_ids= %v", lo.Keys(tapeMapping))
}
fileIDs := tapeMapping[maxTapeID]
delete(tapeMapping, maxTapeID)
if fileIDs.Cardinality() == 0 {
continue
}
for i, f := range tapeMapping {
tapeMapping[i] = f.Difference(fileIDs)
}
targets := make([]*entity.RestoreFile, 0, fileIDs.Cardinality())
for _, fileID := range fileIDs.ToSlice() {
file := files[fileID]
if file == nil {
continue
}
posi := positions[fileID]
if len(posi) == 0 {
logrus.WithContext(ctx).Infof("file position not found, file_id= %d", fileID)
continue
}
for _, p := range posi {
if p.TapeID != maxTapeID {
continue
}
targets = append(targets, &entity.RestoreFile{
FileId: file.ID,
TapeId: p.TapeID,
PositionId: p.ID,
Status: entity.CopyStatus_PENDING,
Size: file.Size,
Hash: file.Hash,
TapePath: p.Path,
TargetPath: file.target,
})
break
}
}
convertPath := tools.Cache(func(p string) string { return strings.ReplaceAll(p, "/", "\x00") })
sort.Slice(targets, func(i, j int) bool {
return convertPath(targets[i].TapePath) < convertPath(targets[j].TapePath)
})
restoreTapes = append(restoreTapes, &entity.RestoreTape{
TapeId: maxTapeID,
Barcode: tapes[maxTapeID].Barcode,
Status: entity.CopyStatus_PENDING,
Files: targets,
})
}
job.State = &entity.JobState{State: &entity.JobState_Restore{Restore: &entity.JobRestoreState{
Step: entity.JobRestoreStep_PENDING,
Tapes: restoreTapes,
}}}
return nil
}
func (e *Executor) getRestoreFiles(ctx context.Context, rootIDs ...int64) (map[int64]*restoreFile, error) {
rootIDSet := mapset.NewThreadUnsafeSet(rootIDs...)
for _, id := range rootIDs {
parents, err := e.lib.ListParents(ctx, id)
if err != nil {
return nil, err
}
if len(parents) <= 1 {
continue
}
for _, parent := range parents[:len(parents)-1] {
if !rootIDSet.Contains(parent.ID) {
continue
}
rootIDSet.Remove(id)
break
}
}
rootIDs = rootIDSet.ToSlice()
mapping, err := e.lib.MGetFile(ctx, rootIDs...)
if err != nil {
return nil, fmt.Errorf("mget file fail, ids= %v, %w", rootIDs, err)
}
files := make([]*restoreFile, 0, len(rootIDs)*8)
visited := mapset.NewThreadUnsafeSet[int64]()
for _, root := range mapping {
if visited.Contains(root.ID) {
continue
}
visited.Add(root.ID)
if !fs.FileMode(root.Mode).IsDir() {
files = append(files, &restoreFile{File: root, target: root.Name})
continue
}
found, err := e.visitFiles(ctx, root.Name, nil, visited, root.ID)
if err != nil {
return nil, err
}
files = append(files, found...)
}
results := make(map[int64]*restoreFile, len(files))
for _, f := range files {
results[f.ID] = f
}
return results, nil
}
func (e *Executor) visitFiles(ctx context.Context, path string, files []*restoreFile, visited mapset.Set[int64], parentID int64) ([]*restoreFile, error) {
children, err := e.lib.List(ctx, parentID)
if err != nil {
return nil, err
}
for _, child := range children {
if visited.Contains(child.ID) {
continue
}
visited.Add(child.ID)
target := path + "/" + child.Name
if !fs.FileMode(child.Mode).IsDir() {
files = append(files, &restoreFile{File: child, target: target})
continue
}
files, err = e.visitFiles(ctx, target, files, visited, child.ID)
if err != nil {
return nil, err
}
}
return files, nil
}
-15
View File
@@ -1,15 +0,0 @@
package executor
import (
"context"
"github.com/samuelncui/yatm/entity"
)
func (e *Executor) startRestore(ctx context.Context, job *Job) error {
return e.Submit(ctx, job, &entity.JobNextParam{Param: &entity.JobNextParam_Restore{
Restore: &entity.JobRestoreNextParam{Param: &entity.JobRestoreNextParam_WaitForTape{
WaitForTape: &entity.JobRestoreWaitForTapeParam{},
}},
}})
}
+260
View File
@@ -0,0 +1,260 @@
package executor
import (
"context"
"encoding/hex"
"fmt"
"os"
"os/exec"
"path"
"sort"
"strings"
"sync/atomic"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/samber/lo"
"github.com/samuelncui/acp"
"github.com/samuelncui/yatm/entity"
"github.com/samuelncui/yatm/tools"
)
func (a *jobRestoreExecutor) restoreTape(ctx context.Context, device string) (rerr error) {
if !a.exe.OccupyDevice(device) {
return fmt.Errorf("device is using, device= %s", device)
}
defer a.exe.ReleaseDevice(device)
defer func() {
tapes := a.getState().Tapes
if _, found := lo.Find(tapes, func(item *entity.RestoreTape) bool {
return item.Status != entity.CopyStatus_SUBMITED
}); found {
a.dispatch(tools.WithoutTimeout(ctx), &entity.JobRestoreDispatchParam{
Param: &entity.JobRestoreDispatchParam_WaitForTape{WaitForTape: &entity.JobRestoreWaitForTapeParam{}},
})
return
}
a.dispatch(tools.WithoutTimeout(ctx), &entity.JobRestoreDispatchParam{
Param: &entity.JobRestoreDispatchParam_Finished{Finished: &entity.JobRestoreFinishedParam{}},
})
}()
readInfoCmd := exec.CommandContext(ctx, a.exe.scripts.ReadInfo)
readInfoCmd.Env = append(readInfoCmd.Env, fmt.Sprintf("DEVICE=%s", device))
infoBuf, err := runCmdWithReturn(a.logger, readInfoCmd)
if err != nil {
return fmt.Errorf("run read info script fail, %w", err)
}
barcode := jsoniter.Get(infoBuf, "barcode").ToString()
if len(barcode) > 6 {
barcode = barcode[:6]
}
barcode = strings.ToUpper(barcode)
tapes := a.getState().Tapes
tape, found := lo.Find(tapes, func(t *entity.RestoreTape) bool {
return t.Barcode == barcode
})
if !found || tape == nil {
expects := lo.Map(tapes, func(t *entity.RestoreTape, _ int) string { return t.Barcode })
return fmt.Errorf("unexpected tape barcode in library, has= '%s' expect= %v", barcode, expects)
}
if tape.Status == entity.CopyStatus_SUBMITED {
return fmt.Errorf("unexpected restore tape state status, tape is restored, status= '%s'", tape.Status)
}
libTape, err := a.exe.lib.GetTape(ctx, tape.TapeId)
if err != nil {
return fmt.Errorf("get tape info fail, barcode= '%s' id= %d, %w", tape.Barcode, tape.TapeId, err)
}
keyPath, keyRecycle, err := a.exe.restoreKey(libTape.Encryption)
if err != nil {
return err
}
defer func() {
time.Sleep(time.Second)
keyRecycle()
}()
if err := runCmd(a.logger, a.exe.makeEncryptCmd(ctx, device, keyPath, barcode, libTape.Name)); err != nil {
return fmt.Errorf("run encrypt script fail, %w", err)
}
mountPoint, err := os.MkdirTemp("", "*.ltfs")
if err != nil {
return fmt.Errorf("create temp mountpoint, %w", err)
}
sourcePath := tools.ThreadUnsafeCache(func(p string) string { return path.Join(mountPoint, p) })
mountCmd := exec.CommandContext(ctx, a.exe.scripts.Mount)
mountCmd.Env = append(mountCmd.Env, fmt.Sprintf("DEVICE=%s", device), fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(a.logger, mountCmd); err != nil {
return fmt.Errorf("run mount script fail, %w", err)
}
defer func() {
umountCmd := exec.CommandContext(tools.WithoutTimeout(ctx), a.exe.scripts.Umount)
umountCmd.Env = append(umountCmd.Env, fmt.Sprintf("MOUNT_POINT=%s", mountPoint))
if err := runCmd(a.logger, umountCmd); err != nil {
a.logger.WithContext(ctx).WithError(err).Errorf("run umount script fail, %s", mountPoint)
return
}
if err := os.Remove(mountPoint); err != nil {
a.logger.WithContext(ctx).WithError(err).Errorf("remove mount point fail, %s", mountPoint)
return
}
}()
opts := make([]acp.Option, 0, 16)
for _, f := range tape.Files {
if f.Status == entity.CopyStatus_SUBMITED {
continue
}
opts = append(opts, acp.AccurateJob(sourcePath(f.TapePath), []string{path.Join(a.exe.paths.Target, f.TargetPath)}))
}
opts = append(opts, acp.WithHash(true))
opts = append(opts, acp.SetFromDevice(acp.LinearDevice(true)))
opts = append(opts, acp.WithLogger(a.logger))
a.progress = newProgress()
defer func() { a.progress = nil }()
convertPath := tools.ThreadUnsafeCache(func(p string) string { return strings.ReplaceAll(p, "/", "\x00") })
opts = append(opts, acp.WithEventHandler(func(ev acp.Event) {
switch e := ev.(type) {
case *acp.EventUpdateCount:
atomic.StoreInt64(&a.progress.totalBytes, e.Bytes)
atomic.StoreInt64(&a.progress.totalFiles, e.Files)
return
case *acp.EventUpdateProgress:
a.progress.setBytes(e.Bytes)
atomic.StoreInt64(&a.progress.files, e.Files)
return
case *acp.EventReportError:
a.logger.WithContext(ctx).Errorf("acp report error, src= '%s' dst= '%s' err= '%s'", e.Error.Src, e.Error.Dst, e.Error.Err)
return
case *acp.EventUpdateJob:
job := e.Job
src := entity.NewSourceFromACPJob(job)
var targetStatus entity.CopyStatus
switch job.Status {
case "pending":
targetStatus = entity.CopyStatus_PENDING
case "preparing":
targetStatus = entity.CopyStatus_RUNNING
case "finished":
a.logger.WithContext(ctx).Infof("file '%s' copy finished, size= %d", src.RealPath(), job.Size)
targetStatus = entity.CopyStatus_STAGED
if len(job.FailTargets) > 0 {
targetStatus = entity.CopyStatus_FAILED
}
for dst, err := range job.FailTargets {
if err == nil {
continue
}
a.logger.WithContext(ctx).WithError(err).Errorf("file '%s' copy fail, dst= '%s'", src.RealPath(), dst)
}
default:
return
}
realPath := src.RealPath()
a.updateJob(ctx, func(_ *Job, state *entity.JobRestoreState) error {
tape, has := lo.Find(state.Tapes, func(tape *entity.RestoreTape) bool { return tape.Barcode == barcode })
if !has || tape == nil {
return fmt.Errorf("cannot found tape, barcode= %s", barcode)
}
idx := sort.Search(len(tape.Files), func(idx int) bool {
return convertPath(realPath) <= convertPath(sourcePath(tape.Files[idx].TapePath))
})
if idx < 0 || idx >= len(tape.Files) {
return fmt.Errorf(
"cannot found target file, real_path= %s found_index= %d tape_file_path= %v", realPath, idx,
lo.Map(tape.Files, func(file *entity.RestoreFile, _ int) string { return sourcePath(file.TapePath) }),
)
}
found := tape.Files[idx]
if found == nil || realPath != sourcePath(found.TapePath) {
return fmt.Errorf(
"cannot match found file, real_path= %s found_index= %d found_file_path= %s",
realPath, idx, sourcePath(found.TapePath),
)
}
if targetStatus == entity.CopyStatus_STAGED {
if targetHash := hex.EncodeToString(found.Hash); targetHash != job.SHA256 {
targetStatus = entity.CopyStatus_FAILED
a.logger.Warnf(
"copy checksum do not match target file hash, real_path= %s target_hash= %s copy_hash= %s",
realPath, targetHash, job.SHA256,
)
}
if targetSize := found.Size; targetSize != job.Size {
targetStatus = entity.CopyStatus_FAILED
a.logger.Warnf(
"copy size do not match target file hash, real_path= %s target_size= %d copy_size= %d",
realPath, targetSize, job.Size,
)
}
}
found.Status = targetStatus
return nil
})
}
}))
a.updateJob(ctx, func(_ *Job, state *entity.JobRestoreState) error {
tape, has := lo.Find(state.Tapes, func(tape *entity.RestoreTape) bool { return tape.Barcode == barcode })
if !has || tape == nil {
return fmt.Errorf("cannot found tape, barcode= %s", barcode)
}
tape.Status = entity.CopyStatus_RUNNING
return nil
})
defer func() {
a.updateJob(ctx, func(job *Job, state *entity.JobRestoreState) error {
tape, has := lo.Find(state.Tapes, func(tape *entity.RestoreTape) bool { return tape.Barcode == barcode })
if !has || tape == nil {
return fmt.Errorf("cannot found tape, barcode= %s", barcode)
}
tape.Status = entity.CopyStatus_SUBMITED
for _, file := range tape.Files {
if file.Status == entity.CopyStatus_STAGED {
file.Status = entity.CopyStatus_SUBMITED
}
if file.Status != entity.CopyStatus_SUBMITED {
tape.Status = entity.CopyStatus_FAILED
}
}
return nil
})
}()
copyer, err := acp.New(ctx, opts...)
if err != nil {
rerr = fmt.Errorf("start copy fail, %w", err)
return
}
copyer.Wait()
return
}
+33
View File
@@ -0,0 +1,33 @@
package executor
import (
"context"
"github.com/modern-go/reflect2"
"github.com/samuelncui/yatm/entity"
"github.com/sirupsen/logrus"
)
var (
jobParamToTypes = map[uintptr]uintptr{
reflect2.RTypeOf(&entity.JobParam_Archive{}): reflect2.RTypeOf(&entity.JobState_Archive{}),
reflect2.RTypeOf(&entity.JobParam_Restore{}): reflect2.RTypeOf(&entity.JobState_Restore{}),
}
jobTypes = map[uintptr]JobType{
reflect2.RTypeOf(&entity.JobState_Archive{}): new(jobTypeArchive),
reflect2.RTypeOf(&entity.JobState_Restore{}): new(jobTypeRestore),
}
)
type JobType interface {
GetExecutor(ctx context.Context, exe *Executor, job *Job) (JobExecutor, error)
}
type JobExecutor interface {
Initialize(ctx context.Context, param *entity.JobParam) error
Dispatch(ctx context.Context, param *entity.JobDispatchParam) error
Display(ctx context.Context) (*entity.JobDisplay, error)
Close(ctx context.Context) error
Logger() *logrus.Logger
}
+3 -2
View File
@@ -10,8 +10,6 @@
"gen-proto": "protoc --ts_out ./src/entity --proto_path ../entity/ `ls ../entity/*.proto` && ./src/entity/gen_index.sh"
},
"dependencies": {
"@samuelncui/chonky": "^0.2.7",
"@samuelncui/chonky-icon-fontawesome": "^0.2.7",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fortawesome/fontawesome-svg-core": "^1.2.32",
@@ -25,6 +23,8 @@
"@protobuf-ts/grpcweb-transport": "^2.8.2",
"@protobuf-ts/runtime": "^2.8.2",
"@protobuf-ts/runtime-rpc": "^2.8.2",
"@samuelncui/chonky": "^0.2.7",
"@samuelncui/chonky-icon-fontawesome": "^0.2.7",
"fast-text-encoding": "^1.0.6",
"filesize": "^10.0.5",
"format-duration": "^2.0.0",
@@ -37,6 +37,7 @@
"react-dom": "^18.2.0",
"react-is": "^18.2.0",
"react-router-dom": "^6.4.5",
"react-virtuoso": "^4.6.1",
"sort-by": "^1.2.0"
},
"devDependencies": {
+319
View File
@@ -0,0 +1,319 @@
import { Fragment, ChangeEvent, memo, useState, useMemo, useCallback, useContext } from "react";
import { assert } from "@protobuf-ts/runtime";
import format from "format-duration";
import { Virtuoso } from "react-virtuoso";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import LinearProgress from "@mui/material/LinearProgress";
import { cli } from "../api";
import { Job, JobDispatchRequest, CopyStatus, SourceState, JobStatus } from "../entity";
import { JobArchiveCopyingParam, JobArchiveStep, JobArchiveDisplay, JobArchiveState } from "../entity";
import { formatFilesize } from "../tools";
import { JobCard } from "./job-card";
import { RefreshContext } from "../pages/jobs";
export const ArchiveCard = ({ job, state, display }: { job: Job; state: JobArchiveState; display: JobArchiveDisplay | null }): JSX.Element => {
const [fields, progress] = useMemo(() => {
const totalFiles = state.sources.length;
let submitedFiles = 0,
submitedBytes = 0,
totalBytes = 0;
for (const file of state.sources) {
totalBytes += Number(file.size);
if (file.status !== CopyStatus.SUBMITED) {
continue;
}
submitedFiles++;
submitedBytes += Number(file.size);
}
const copiedFiles = submitedFiles + Number(display?.copiedFiles || 0n);
const copiedBytes = submitedBytes + Number(display?.copiedBytes || 0n);
const avgSpeed = (() => {
if (!display || !display.copiedBytes || !display.startTime) {
return NaN;
}
const duration = Date.now() / 1000 - Number(display.startTime);
if (duration <= 0) {
return NaN;
}
return Number(display.copiedBytes) / duration;
})();
const progress = (totalBytes > 0 ? copiedBytes / totalBytes : 1) * 100;
const fields = [
{ name: "Current Step", value: JobArchiveStep[state.step] },
{ name: "Current Speed", value: display?.speed ? `${formatFilesize(display?.speed)}/s` : "--" },
{ name: "Average Speed", value: !isNaN(avgSpeed) ? `${formatFilesize(avgSpeed)}/s` : "--" },
{ name: "Estimated Time", value: !isNaN(avgSpeed) ? format(((totalBytes - copiedBytes) * 1000) / avgSpeed) : "--" },
{ name: "Copied Files", value: copiedFiles },
{ name: "Copied Bytes", value: formatFilesize(copiedBytes) },
{ name: "Submited Files", value: submitedFiles },
{ name: "Submited Bytes", value: formatFilesize(submitedBytes) },
{ name: "Total Files", value: totalFiles },
{ name: "Total Bytes", value: formatFilesize(totalBytes) },
];
return [fields, progress];
}, [state, display]);
return (
<JobCard
job={job}
detail={
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ paddingTop: "1em" }}>
<LinearProgress variant="determinate" value={progress} />
</Box>
</Grid>
{fields.map((field, idx) => (
<Grid item xs={12} md={3} key={idx}>
<Typography variant="body1">
<b>{field.name}</b>: {field.value}
</Typography>
</Grid>
))}
</Grid>
}
buttons={
<Fragment>
{state.step === JobArchiveStep.WAIT_FOR_TAPE && <NewTapeDialog key="NEW_TAPE" job={job} />}
{job.status !== JobStatus.PROCESSING && <RollbackDialog key="ROLLBACK" jobID={job.id} state={state} />}
<ArchiveViewFilesDialog key="ARCHIVE_VIEW_FILES" sources={state.sources} />
</Fragment>
}
/>
);
};
const NewTapeDialog = ({ job }: { job: Job }) => {
const refresh = useContext(RefreshContext);
const [devices, setDevices] = useState<string[]>([]);
const [param, setParam] = useState<JobArchiveCopyingParam | null>(null);
const handleClickOpen = async () => {
const reply = await cli.deviceList({}).response;
setDevices(reply.devices);
setParam(JobArchiveCopyingParam.create());
};
const handleClose = () => {
setParam(null);
setDevices([]);
};
const handleChange = (key: keyof JobArchiveCopyingParam) => (event: ChangeEvent<HTMLInputElement>) => {
if (param === null) {
return;
}
setParam({ ...param, [key]: event.target.value });
};
const handleSubmit = async () => {
if (!param) {
return;
}
const trimedParam: JobArchiveCopyingParam = {
device: param.device,
barcode: param.barcode.toUpperCase(),
name: param.name,
};
assert(trimedParam.barcode.length === 6);
const req = makeArchiveCopyingParam(job.id, trimedParam);
console.log("job dispatch start, request= ", req);
const reply = await cli.jobDispatch(req).response;
console.log("job dispatch success, reply= ", reply);
await refresh();
handleClose();
};
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
Load Tape
</Button>
{param && (
<Dialog open={true} onClose={handleClose} maxWidth={"sm"} fullWidth>
<DialogTitle>Load Tape</DialogTitle>
<DialogContent>
<DialogContentText>After load tape into tape drive, click 'Submit'</DialogContentText>
<TextField select required margin="dense" label="Drive Device" fullWidth variant="standard" value={param.device} onChange={handleChange("device")}>
{devices.map((device) => (
<MenuItem key={device} value={device}>
{device}
</MenuItem>
))}
</TextField>
<TextField required margin="dense" label="Tape Barcode" fullWidth variant="standard" value={param.barcode} onChange={handleChange("barcode")} />
<TextField required margin="dense" label="Tape Name" fullWidth variant="standard" value={param.name} onChange={handleChange("name")} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleSubmit}>Submit</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
const ArchiveViewFilesDialog = ({ sources }: { sources: SourceState[] }) => {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
View Files
</Button>
{open && (
<Dialog open={true} onClose={handleClose} maxWidth={"lg"} fullWidth scroll="paper" sx={{ height: "100%" }} className="view-log-dialog">
<DialogTitle>View Files</DialogTitle>
<DialogContent dividers style={{ padding: 0 }}>
<Virtuoso
style={{ width: "100%", height: "100%" }}
totalCount={sources.length}
itemContent={(idx) => {
const src = sources[idx];
if (!src || !src.source) {
return null;
}
return (
<ListItem key={idx} component="div" disablePadding>
<ListItemText
primary={src.source.base + src.source.path.join("/")}
secondary={`Size: ${formatFilesize(src.size)} Status: ${CopyStatus[src.status]}`}
style={{ padding: 0, margin: 5 }}
/>
</ListItem>
);
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
const RollbackDialog = ({ jobID, state }: { jobID: bigint; state: JobArchiveState }) => {
const refresh = useContext(RefreshContext);
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleClickItem = useCallback(
async (idx: number) => {
const found = state.sources[idx];
if (!found || !found.source) {
return;
}
const path = found.source.base + found.source.path.join("/");
if (!confirm(`Rollback to file '${path}', all files after this file (included) will be set to 'PENDING'.`)) {
return;
}
const sources = Array.from(state.sources);
for (let i = idx; i < sources.length; i++) {
sources[i].status = CopyStatus.PENDING;
}
await cli.jobEditState({ id: jobID, state: { state: { oneofKind: "archive", archive: { ...state, sources } } } });
alert(`Rollback to file '${path}' success!`);
await refresh();
},
[state, refresh],
);
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
Rollback
</Button>
{open && (
<Dialog open={true} onClose={handleClose} maxWidth={"lg"} fullWidth scroll="paper" sx={{ height: "100%" }} className="view-log-dialog">
<DialogTitle>Click File to Rollback</DialogTitle>
<DialogContent dividers style={{ padding: 0 }}>
<Virtuoso
style={{ width: "100%", height: "100%" }}
totalCount={state.sources.length}
itemContent={(idx) => {
const src = state.sources[idx];
if (!src || !src.source) {
return null;
}
return (
<ListItem key={idx} component="div" disablePadding>
<ListItemButton style={{ padding: 0 }} onClick={() => handleClickItem(idx)}>
<ListItemText
primary={src.source.base + src.source.path.join("/")}
secondary={`Size: ${formatFilesize(src.size)} Status: ${CopyStatus[src.status]}`}
style={{ padding: 0, margin: 5 }}
/>
</ListItemButton>
</ListItem>
);
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
function makeArchiveCopyingParam(jobID: bigint, param: JobArchiveCopyingParam): JobDispatchRequest {
return {
id: jobID,
param: {
param: {
oneofKind: "archive",
archive: {
param: {
oneofKind: "copying",
copying: param,
},
},
},
},
};
}
+50
View File
@@ -0,0 +1,50 @@
import { useCallback, useContext } from "react";
import Typography from "@mui/material/Typography";
import Card from "@mui/material/Card";
import CardActions from "@mui/material/CardActions";
import CardContent from "@mui/material/CardContent";
import Button from "@mui/material/Button";
import Divider from "@mui/material/Divider";
import { cli } from "../api";
import { Job, JobStatus, JobDeleteRequest } from "../entity";
import { ViewLogDialog } from "./job-log";
import { RefreshContext } from "../pages/jobs";
const DeleteJobButton = ({ jobID }: { jobID: bigint }) => {
const refresh = useContext(RefreshContext);
const deleteJob = useCallback(async () => {
await cli.jobDelete(JobDeleteRequest.create({ ids: [jobID] }));
await refresh();
}, [jobID]);
return (
<Button size="small" onClick={deleteJob}>
Delete Job
</Button>
);
};
export const JobCard = ({ job, detail, buttons }: { job: Job; detail?: JSX.Element; buttons?: JSX.Element }) => {
return (
<Card sx={{ textAlign: "left" }} className="job-detail">
<CardContent>
<Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom>
{`${JobStatus[job.status]}`}
</Typography>
<Typography variant="h5" component="div">{`Job ${job.id} - ${job.state?.state.oneofKind?.toUpperCase()}`}</Typography>
{detail}
</CardContent>
<Divider />
<CardActions>
<div>{buttons}</div>
<div style={{ marginLeft: "auto", marginRight: 0 }}>
<ViewLogDialog key="VIEW_LOG" jobID={job.id} />
<DeleteJobButton key="DELETE_JOB" jobID={job.id} />
</div>
</CardActions>
</Card>
);
};
+84
View File
@@ -0,0 +1,84 @@
import { Fragment, useState, useRef, useEffect, useCallback } from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import { cli } from "../api";
import { sleep } from "../tools";
export const ViewLogDialog = ({ jobID }: { jobID: bigint }) => {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
View Log
</Button>
{open && (
<Dialog open={true} onClose={handleClose} maxWidth={"lg"} fullWidth scroll="paper" sx={{ height: "100%" }} className="view-log-dialog">
<DialogTitle>View Log</DialogTitle>
<DialogContent dividers>
<LogConsole jobId={jobID} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
const LogConsole = ({ jobId }: { jobId: bigint }) => {
const [log, setLog] = useState<string>("");
const [offset, setOffset] = useState(0n);
const bottom = useRef(null);
const refresh = useCallback(async () => {
const reply = await cli.jobGetLog({ jobId, offset: offset }).response;
setLog(log + new TextDecoder().decode(reply.logs));
setOffset(reply.offset);
}, [log, setLog, offset, setOffset, bottom]);
const refreshRef = useRef(refresh);
refreshRef.current = refresh;
useEffect(() => {
var timer: NodeJS.Timeout;
(async () => {
await refreshRef.current();
if (bottom.current) {
const bottomElem = bottom.current as HTMLElement;
await sleep(10);
bottomElem.scrollIntoView(true);
await sleep(10);
bottomElem.parentElement?.scrollBy(0, 100);
}
timer = setInterval(() => refreshRef.current(), 2000);
})();
return () => {
if (!timer) {
return;
}
clearInterval(timer);
};
}, [refresh]);
return (
<Fragment>
<pre>{log || "loading..."}</pre>
<div ref={bottom} />
</Fragment>
);
};
+271
View File
@@ -0,0 +1,271 @@
import { Fragment, ChangeEvent, useState, useMemo, useContext } from "react";
import format from "format-duration";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import Chip, { ChipProps } from "@mui/material/Chip";
import Stack from "@mui/material/Stack";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import LinearProgress from "@mui/material/LinearProgress";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { TreeView, TreeItem } from "@mui/x-tree-view";
import { cli } from "../api";
import { Job, JobDispatchRequest, CopyStatus, JobArchiveStep } from "../entity";
import { JobRestoreCopyingParam, JobRestoreStep, JobRestoreDisplay, JobRestoreState } from "../entity";
import { RestoreTape } from "../entity";
import { formatFilesize } from "../tools";
import { JobCard } from "./job-card";
import { RefreshContext } from "../pages/jobs";
const tapeStatusToColor = (status: CopyStatus): ChipProps["color"] => {
switch (status) {
case CopyStatus.DRAFT:
return "primary";
case CopyStatus.PENDING:
return "primary";
case CopyStatus.RUNNING:
return "secondary";
case CopyStatus.STAGED:
return "warning";
case CopyStatus.SUBMITED:
return "success";
case CopyStatus.FAILED:
return "error";
default:
return "default";
}
};
export const RestoreCard = ({ job, state, display }: { job: Job; state: JobRestoreState; display: JobRestoreDisplay | null }): JSX.Element => {
const [fields, progress] = useMemo(() => {
const totalFiles = state.tapes.reduce((count, tape) => count + tape.files.length, 0);
let successFiles = 0,
successBytes = 0,
copiedFiles = Number(display?.copiedFiles || 0n),
copiedBytes = Number(display?.copiedBytes || 0n),
totalBytes = 0;
for (const tape of state.tapes) {
for (const file of tape.files) {
totalBytes += Number(file.size);
if (file.status === CopyStatus.SUBMITED || file.status === CopyStatus.STAGED) {
successFiles++;
successBytes += Number(file.size);
}
if (file.status === CopyStatus.SUBMITED) {
copiedFiles++;
copiedBytes += Number(file.size);
}
}
}
const avgSpeed = (() => {
if (!display || !display.copiedBytes || !display.startTime) {
return NaN;
}
const duration = Date.now() / 1000 - Number(display.startTime);
if (duration <= 0) {
return NaN;
}
return Number(display.copiedBytes) / duration;
})();
const progress = (totalBytes > 0 ? copiedBytes / totalBytes : 1) * 100;
const fields = [
{ name: "Current Step", value: JobArchiveStep[state.step] },
{ name: "Current Speed", value: display?.speed ? `${formatFilesize(display?.speed)}/s` : "--" },
{ name: "Average Speed", value: !isNaN(avgSpeed) ? `${formatFilesize(avgSpeed)}/s` : "--" },
{ name: "Estimated Time", value: !isNaN(avgSpeed) ? format(((totalBytes - copiedBytes) * 1000) / avgSpeed) : "--" },
{ name: "Copied Files", value: copiedFiles },
{ name: "Copied Bytes", value: formatFilesize(copiedBytes) },
{ name: "Success Files", value: successFiles },
{ name: "Success Bytes", value: formatFilesize(successBytes) },
{ name: "Total Files", value: totalFiles },
{ name: "Total Bytes", value: formatFilesize(totalBytes) },
];
return [fields, progress];
}, [state, display]);
return (
<JobCard
job={job}
detail={
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ paddingTop: "1em" }}>
<LinearProgress variant="determinate" value={progress} />
</Box>
</Grid>
{fields.map((field, idx) => (
<Grid item xs={12} md={3} key={idx}>
<Typography variant="body1">
<b>{field.name}</b>: {field.value}
</Typography>
</Grid>
))}
<Grid item xs={12} md={12}>
<Stack direction="row" spacing={1} style={{ flexWrap: "wrap" }}>
{state.tapes.map((tape) => (
<Chip label={`${tape.barcode}: ${CopyStatus[tape.status]}`} color={tapeStatusToColor(tape.status)} variant="outlined" key={`${tape.tapeId}`} />
))}
</Stack>
</Grid>
</Grid>
}
buttons={
<Fragment>
{state.step === JobRestoreStep.WAIT_FOR_TAPE && <LoadTapeDialog key="LOAD_TAPE" job={job} />}
<RestoreViewFilesDialog key="RESTORE_VIEW_FILES" tapes={state.tapes} />
</Fragment>
}
/>
);
};
const LoadTapeDialog = ({ job }: { job: Job }) => {
const refresh = useContext(RefreshContext);
const [devices, setDevices] = useState<string[] | null>(null);
const [device, setDevice] = useState<string | null>(null);
const handleClickOpen = async () => {
const reply = await cli.deviceList({}).response;
setDevices(reply.devices);
};
const handleClose = () => {
setDevices(null);
setDevice(null);
};
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDevice(event.target.value);
};
const handleSubmit = async () => {
if (!device) {
return;
}
const trimedParam: JobRestoreCopyingParam = {
device: device,
};
const req = makeRestoreCopyingParam(job.id, trimedParam);
console.log("job dispatch start, request= ", req);
const reply = await cli.jobDispatch(req).response;
console.log("job dispatch success, reply= ", reply);
await refresh();
handleClose();
};
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
Load Tape
</Button>
{devices && (
<Dialog open={true} onClose={handleClose} maxWidth={"sm"} fullWidth>
<DialogTitle>Load Tape</DialogTitle>
<DialogContent>
<DialogContentText>After load tape into tape drive, click 'Submit'</DialogContentText>
<TextField select required margin="dense" label="Drive Device" fullWidth variant="standard" value={device} onChange={handleChange}>
{devices.map((device) => (
<MenuItem key={device} value={device}>
{device}
</MenuItem>
))}
</TextField>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleSubmit}>Submit</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
const RestoreViewFilesDialog = ({ tapes }: { tapes: RestoreTape[] }) => {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const counts = useMemo(() => tapes.map((tape) => tape.files.length), [tapes]);
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
View Files
</Button>
{open && (
<Dialog open={true} onClose={handleClose} maxWidth={"lg"} fullWidth scroll="paper" sx={{ height: "100%" }} className="view-log-dialog">
<DialogTitle>View Files</DialogTitle>
<DialogContent dividers>
<TreeView defaultCollapseIcon={<ExpandMoreIcon />} defaultExpandIcon={<ChevronRightIcon />}>
{tapes.map((tape) => {
if (!tape.files) {
return null;
}
return (
<TreeItem label={tape.barcode} nodeId={`tape-${tape.tapeId}`}>
{tape.files.map((file) => (
<TreeItem
label={
<pre style={{ margin: 0 }}>
{file.tapePath} <b>{CopyStatus[file.status]}</b>
</pre>
}
nodeId={`file-${file.positionId}`}
/>
))}
</TreeItem>
);
})}
</TreeView>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
function makeRestoreCopyingParam(jobID: bigint, param: JobRestoreCopyingParam): JobDispatchRequest {
return {
id: jobID,
param: {
param: {
oneofKind: "restore",
restore: {
param: {
oneofKind: "copying",
copying: param,
},
},
},
},
};
}
+26 -26
View File
@@ -13,8 +13,8 @@ import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
import { JobRestoreDisplay } from "./job_restore";
import { JobArchiveDisplay } from "./job_archive";
import { JobRestoreNextParam } from "./job_restore";
import { JobArchiveNextParam } from "./job_archive";
import { JobRestoreDispatchParam } from "./job_restore";
import { JobArchiveDispatchParam } from "./job_archive";
import { JobRestoreState } from "./job_restore";
import { JobArchiveState } from "./job_archive";
import { JobRestoreParam } from "./job_restore";
@@ -95,24 +95,24 @@ export interface JobState {
};
}
/**
* @generated from protobuf message job.JobNextParam
* @generated from protobuf message job.JobDispatchParam
*/
export interface JobNextParam {
export interface JobDispatchParam {
/**
* @generated from protobuf oneof: param
*/
param: {
oneofKind: "archive";
/**
* @generated from protobuf field: job_archive.JobArchiveNextParam archive = 1;
* @generated from protobuf field: job_archive.JobArchiveDispatchParam archive = 1;
*/
archive: JobArchiveNextParam;
archive: JobArchiveDispatchParam;
} | {
oneofKind: "restore";
/**
* @generated from protobuf field: job_restore.JobRestoreNextParam restore = 2;
* @generated from protobuf field: job_restore.JobRestoreDispatchParam restore = 2;
*/
restore: JobRestoreNextParam;
restore: JobRestoreDispatchParam;
} | {
oneofKind: undefined;
};
@@ -423,35 +423,35 @@ class JobState$Type extends MessageType<JobState> {
*/
export const JobState = new JobState$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobNextParam$Type extends MessageType<JobNextParam> {
class JobDispatchParam$Type extends MessageType<JobDispatchParam> {
constructor() {
super("job.JobNextParam", [
{ no: 1, name: "archive", kind: "message", oneof: "param", T: () => JobArchiveNextParam },
{ no: 2, name: "restore", kind: "message", oneof: "param", T: () => JobRestoreNextParam }
super("job.JobDispatchParam", [
{ no: 1, name: "archive", kind: "message", oneof: "param", T: () => JobArchiveDispatchParam },
{ no: 2, name: "restore", kind: "message", oneof: "param", T: () => JobRestoreDispatchParam }
]);
}
create(value?: PartialMessage<JobNextParam>): JobNextParam {
create(value?: PartialMessage<JobDispatchParam>): JobDispatchParam {
const message = { param: { oneofKind: undefined } };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<JobNextParam>(this, message, value);
reflectionMergePartial<JobDispatchParam>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobNextParam): JobNextParam {
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobDispatchParam): JobDispatchParam {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* job_archive.JobArchiveNextParam archive */ 1:
case /* job_archive.JobArchiveDispatchParam archive */ 1:
message.param = {
oneofKind: "archive",
archive: JobArchiveNextParam.internalBinaryRead(reader, reader.uint32(), options, (message.param as any).archive)
archive: JobArchiveDispatchParam.internalBinaryRead(reader, reader.uint32(), options, (message.param as any).archive)
};
break;
case /* job_restore.JobRestoreNextParam restore */ 2:
case /* job_restore.JobRestoreDispatchParam restore */ 2:
message.param = {
oneofKind: "restore",
restore: JobRestoreNextParam.internalBinaryRead(reader, reader.uint32(), options, (message.param as any).restore)
restore: JobRestoreDispatchParam.internalBinaryRead(reader, reader.uint32(), options, (message.param as any).restore)
};
break;
default:
@@ -465,13 +465,13 @@ class JobNextParam$Type extends MessageType<JobNextParam> {
}
return message;
}
internalBinaryWrite(message: JobNextParam, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* job_archive.JobArchiveNextParam archive = 1; */
internalBinaryWrite(message: JobDispatchParam, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* job_archive.JobArchiveDispatchParam archive = 1; */
if (message.param.oneofKind === "archive")
JobArchiveNextParam.internalBinaryWrite(message.param.archive, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
/* job_restore.JobRestoreNextParam restore = 2; */
JobArchiveDispatchParam.internalBinaryWrite(message.param.archive, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
/* job_restore.JobRestoreDispatchParam restore = 2; */
if (message.param.oneofKind === "restore")
JobRestoreNextParam.internalBinaryWrite(message.param.restore, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
JobRestoreDispatchParam.internalBinaryWrite(message.param.restore, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -479,9 +479,9 @@ class JobNextParam$Type extends MessageType<JobNextParam> {
}
}
/**
* @generated MessageType for protobuf message job.JobNextParam
* @generated MessageType for protobuf message job.JobDispatchParam
*/
export const JobNextParam = new JobNextParam$Type();
export const JobDispatchParam = new JobDispatchParam$Type();
// @generated message type with reflection information, may provide speed optimized methods
class CreatableJob$Type extends MessageType<CreatableJob> {
constructor() {
+10 -10
View File
@@ -23,9 +23,9 @@ export interface JobArchiveParam {
sources: Source[];
}
/**
* @generated from protobuf message job_archive.JobArchiveNextParam
* @generated from protobuf message job_archive.JobArchiveDispatchParam
*/
export interface JobArchiveNextParam {
export interface JobArchiveDispatchParam {
/**
* @generated from protobuf oneof: param
*/
@@ -189,22 +189,22 @@ class JobArchiveParam$Type extends MessageType<JobArchiveParam> {
*/
export const JobArchiveParam = new JobArchiveParam$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobArchiveNextParam$Type extends MessageType<JobArchiveNextParam> {
class JobArchiveDispatchParam$Type extends MessageType<JobArchiveDispatchParam> {
constructor() {
super("job_archive.JobArchiveNextParam", [
super("job_archive.JobArchiveDispatchParam", [
{ no: 1, name: "wait_for_tape", kind: "message", oneof: "param", T: () => JobArchiveWaitForTapeParam },
{ no: 2, name: "copying", kind: "message", oneof: "param", T: () => JobArchiveCopyingParam },
{ no: 255, name: "finished", kind: "message", oneof: "param", T: () => JobArchiveFinishedParam }
]);
}
create(value?: PartialMessage<JobArchiveNextParam>): JobArchiveNextParam {
create(value?: PartialMessage<JobArchiveDispatchParam>): JobArchiveDispatchParam {
const message = { param: { oneofKind: undefined } };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<JobArchiveNextParam>(this, message, value);
reflectionMergePartial<JobArchiveDispatchParam>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobArchiveNextParam): JobArchiveNextParam {
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobArchiveDispatchParam): JobArchiveDispatchParam {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
@@ -238,7 +238,7 @@ class JobArchiveNextParam$Type extends MessageType<JobArchiveNextParam> {
}
return message;
}
internalBinaryWrite(message: JobArchiveNextParam, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
internalBinaryWrite(message: JobArchiveDispatchParam, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* job_archive.JobArchiveWaitForTapeParam wait_for_tape = 1; */
if (message.param.oneofKind === "waitForTape")
JobArchiveWaitForTapeParam.internalBinaryWrite(message.param.waitForTape, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
@@ -255,9 +255,9 @@ class JobArchiveNextParam$Type extends MessageType<JobArchiveNextParam> {
}
}
/**
* @generated MessageType for protobuf message job_archive.JobArchiveNextParam
* @generated MessageType for protobuf message job_archive.JobArchiveDispatchParam
*/
export const JobArchiveNextParam = new JobArchiveNextParam$Type();
export const JobArchiveDispatchParam = new JobArchiveDispatchParam$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobArchiveWaitForTapeParam$Type extends MessageType<JobArchiveWaitForTapeParam> {
constructor() {
+10 -10
View File
@@ -22,9 +22,9 @@ export interface JobRestoreParam {
fileIds: bigint[];
}
/**
* @generated from protobuf message job_restore.JobRestoreNextParam
* @generated from protobuf message job_restore.JobRestoreDispatchParam
*/
export interface JobRestoreNextParam {
export interface JobRestoreDispatchParam {
/**
* @generated from protobuf oneof: param
*/
@@ -246,22 +246,22 @@ class JobRestoreParam$Type extends MessageType<JobRestoreParam> {
*/
export const JobRestoreParam = new JobRestoreParam$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobRestoreNextParam$Type extends MessageType<JobRestoreNextParam> {
class JobRestoreDispatchParam$Type extends MessageType<JobRestoreDispatchParam> {
constructor() {
super("job_restore.JobRestoreNextParam", [
super("job_restore.JobRestoreDispatchParam", [
{ no: 1, name: "wait_for_tape", kind: "message", oneof: "param", T: () => JobRestoreWaitForTapeParam },
{ no: 2, name: "copying", kind: "message", oneof: "param", T: () => JobRestoreCopyingParam },
{ no: 255, name: "finished", kind: "message", oneof: "param", T: () => JobRestoreFinishedParam }
]);
}
create(value?: PartialMessage<JobRestoreNextParam>): JobRestoreNextParam {
create(value?: PartialMessage<JobRestoreDispatchParam>): JobRestoreDispatchParam {
const message = { param: { oneofKind: undefined } };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<JobRestoreNextParam>(this, message, value);
reflectionMergePartial<JobRestoreDispatchParam>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobRestoreNextParam): JobRestoreNextParam {
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobRestoreDispatchParam): JobRestoreDispatchParam {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
@@ -295,7 +295,7 @@ class JobRestoreNextParam$Type extends MessageType<JobRestoreNextParam> {
}
return message;
}
internalBinaryWrite(message: JobRestoreNextParam, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
internalBinaryWrite(message: JobRestoreDispatchParam, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* job_restore.JobRestoreWaitForTapeParam wait_for_tape = 1; */
if (message.param.oneofKind === "waitForTape")
JobRestoreWaitForTapeParam.internalBinaryWrite(message.param.waitForTape, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
@@ -312,9 +312,9 @@ class JobRestoreNextParam$Type extends MessageType<JobRestoreNextParam> {
}
}
/**
* @generated MessageType for protobuf message job_restore.JobRestoreNextParam
* @generated MessageType for protobuf message job_restore.JobRestoreDispatchParam
*/
export const JobRestoreNextParam = new JobRestoreNextParam$Type();
export const JobRestoreDispatchParam = new JobRestoreDispatchParam$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobRestoreWaitForTapeParam$Type extends MessageType<JobRestoreWaitForTapeParam> {
constructor() {
+28 -15
View File
@@ -16,10 +16,12 @@ import type { JobGetLogReply } from "./service";
import type { JobGetLogRequest } from "./service";
import type { JobDisplayReply } from "./service";
import type { JobDisplayRequest } from "./service";
import type { JobNextReply } from "./service";
import type { JobNextRequest } from "./service";
import type { JobDispatchReply } from "./service";
import type { JobDispatchRequest } from "./service";
import type { JobDeleteReply } from "./service";
import type { JobDeleteRequest } from "./service";
import type { JobEditStateReply } from "./service";
import type { JobEditStateRequest } from "./service";
import type { JobCreateReply } from "./service";
import type { JobCreateRequest } from "./service";
import type { JobListReply } from "./service";
@@ -87,14 +89,18 @@ export interface IServiceClient {
* @generated from protobuf rpc: JobCreate(service.JobCreateRequest) returns (service.JobCreateReply);
*/
jobCreate(input: JobCreateRequest, options?: RpcOptions): UnaryCall<JobCreateRequest, JobCreateReply>;
/**
* @generated from protobuf rpc: JobEditState(service.JobEditStateRequest) returns (service.JobEditStateReply);
*/
jobEditState(input: JobEditStateRequest, options?: RpcOptions): UnaryCall<JobEditStateRequest, JobEditStateReply>;
/**
* @generated from protobuf rpc: JobDelete(service.JobDeleteRequest) returns (service.JobDeleteReply);
*/
jobDelete(input: JobDeleteRequest, options?: RpcOptions): UnaryCall<JobDeleteRequest, JobDeleteReply>;
/**
* @generated from protobuf rpc: JobNext(service.JobNextRequest) returns (service.JobNextReply);
* @generated from protobuf rpc: JobDispatch(service.JobDispatchRequest) returns (service.JobDispatchReply);
*/
jobNext(input: JobNextRequest, options?: RpcOptions): UnaryCall<JobNextRequest, JobNextReply>;
jobDispatch(input: JobDispatchRequest, options?: RpcOptions): UnaryCall<JobDispatchRequest, JobDispatchReply>;
/**
* @generated from protobuf rpc: JobDisplay(service.JobDisplayRequest) returns (service.JobDisplayReply);
*/
@@ -199,60 +205,67 @@ export class ServiceClient implements IServiceClient, ServiceInfo {
const method = this.methods[9], opt = this._transport.mergeOptions(options);
return stackIntercept<JobCreateRequest, JobCreateReply>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: JobEditState(service.JobEditStateRequest) returns (service.JobEditStateReply);
*/
jobEditState(input: JobEditStateRequest, options?: RpcOptions): UnaryCall<JobEditStateRequest, JobEditStateReply> {
const method = this.methods[10], opt = this._transport.mergeOptions(options);
return stackIntercept<JobEditStateRequest, JobEditStateReply>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: JobDelete(service.JobDeleteRequest) returns (service.JobDeleteReply);
*/
jobDelete(input: JobDeleteRequest, options?: RpcOptions): UnaryCall<JobDeleteRequest, JobDeleteReply> {
const method = this.methods[10], opt = this._transport.mergeOptions(options);
const method = this.methods[11], opt = this._transport.mergeOptions(options);
return stackIntercept<JobDeleteRequest, JobDeleteReply>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: JobNext(service.JobNextRequest) returns (service.JobNextReply);
* @generated from protobuf rpc: JobDispatch(service.JobDispatchRequest) returns (service.JobDispatchReply);
*/
jobNext(input: JobNextRequest, options?: RpcOptions): UnaryCall<JobNextRequest, JobNextReply> {
const method = this.methods[11], opt = this._transport.mergeOptions(options);
return stackIntercept<JobNextRequest, JobNextReply>("unary", this._transport, method, opt, input);
jobDispatch(input: JobDispatchRequest, options?: RpcOptions): UnaryCall<JobDispatchRequest, JobDispatchReply> {
const method = this.methods[12], opt = this._transport.mergeOptions(options);
return stackIntercept<JobDispatchRequest, JobDispatchReply>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: JobDisplay(service.JobDisplayRequest) returns (service.JobDisplayReply);
*/
jobDisplay(input: JobDisplayRequest, options?: RpcOptions): UnaryCall<JobDisplayRequest, JobDisplayReply> {
const method = this.methods[12], opt = this._transport.mergeOptions(options);
const method = this.methods[13], opt = this._transport.mergeOptions(options);
return stackIntercept<JobDisplayRequest, JobDisplayReply>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: JobGetLog(service.JobGetLogRequest) returns (service.JobGetLogReply);
*/
jobGetLog(input: JobGetLogRequest, options?: RpcOptions): UnaryCall<JobGetLogRequest, JobGetLogReply> {
const method = this.methods[13], opt = this._transport.mergeOptions(options);
const method = this.methods[14], opt = this._transport.mergeOptions(options);
return stackIntercept<JobGetLogRequest, JobGetLogReply>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: SourceList(service.SourceListRequest) returns (service.SourceListReply);
*/
sourceList(input: SourceListRequest, options?: RpcOptions): UnaryCall<SourceListRequest, SourceListReply> {
const method = this.methods[14], opt = this._transport.mergeOptions(options);
const method = this.methods[15], opt = this._transport.mergeOptions(options);
return stackIntercept<SourceListRequest, SourceListReply>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: DeviceList(service.DeviceListRequest) returns (service.DeviceListReply);
*/
deviceList(input: DeviceListRequest, options?: RpcOptions): UnaryCall<DeviceListRequest, DeviceListReply> {
const method = this.methods[15], opt = this._transport.mergeOptions(options);
const method = this.methods[16], opt = this._transport.mergeOptions(options);
return stackIntercept<DeviceListRequest, DeviceListReply>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: LibraryExport(service.LibraryExportRequest) returns (service.LibraryExportReply);
*/
libraryExport(input: LibraryExportRequest, options?: RpcOptions): UnaryCall<LibraryExportRequest, LibraryExportReply> {
const method = this.methods[16], opt = this._transport.mergeOptions(options);
const method = this.methods[17], opt = this._transport.mergeOptions(options);
return stackIntercept<LibraryExportRequest, LibraryExportReply>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: LibraryTrim(service.LibraryTrimRequest) returns (service.LibraryTrimReply);
*/
libraryTrim(input: LibraryTrimRequest, options?: RpcOptions): UnaryCall<LibraryTrimRequest, LibraryTrimReply> {
const method = this.methods[17], opt = this._transport.mergeOptions(options);
const method = this.methods[18], opt = this._transport.mergeOptions(options);
return stackIntercept<LibraryTrimRequest, LibraryTrimReply>("unary", this._transport, method, opt, input);
}
}
+142 -55
View File
@@ -15,7 +15,9 @@ import { MessageType } from "@protobuf-ts/runtime";
import { LibraryEntityType } from "./library_entity_type";
import { SourceFile } from "./source";
import { JobDisplay } from "./job";
import { JobNextParam } from "./job";
import { JobDispatchParam } from "./job";
import { JobState } from "./job";
import { JobStatus } from "./job";
import { CreatableJob } from "./job";
import { Job } from "./job";
import { JobRecentlyUpdateFilter } from "./job";
@@ -269,6 +271,28 @@ export interface JobCreateReply {
*/
job?: Job;
}
/**
* @generated from protobuf message service.JobEditStateRequest
*/
export interface JobEditStateRequest {
/**
* @generated from protobuf field: int64 id = 1;
*/
id: bigint;
/**
* @generated from protobuf field: optional job.JobStatus status = 2;
*/
status?: JobStatus;
/**
* @generated from protobuf field: optional job.JobState state = 3;
*/
state?: JobState;
}
/**
* @generated from protobuf message service.JobEditStateReply
*/
export interface JobEditStateReply {
}
/**
* @generated from protobuf message service.JobDeleteRequest
*/
@@ -284,26 +308,22 @@ export interface JobDeleteRequest {
export interface JobDeleteReply {
}
/**
* @generated from protobuf message service.JobNextRequest
* @generated from protobuf message service.JobDispatchRequest
*/
export interface JobNextRequest {
export interface JobDispatchRequest {
/**
* @generated from protobuf field: int64 id = 1;
*/
id: bigint;
/**
* @generated from protobuf field: job.JobNextParam param = 2;
* @generated from protobuf field: job.JobDispatchParam param = 2;
*/
param?: JobNextParam;
param?: JobDispatchParam;
}
/**
* @generated from protobuf message service.JobNextReply
* @generated from protobuf message service.JobDispatchReply
*/
export interface JobNextReply {
/**
* @generated from protobuf field: job.Job job = 1;
*/
job?: Job;
export interface JobDispatchReply {
}
/**
* @generated from protobuf message service.JobDisplayRequest
@@ -1521,6 +1541,93 @@ class JobCreateReply$Type extends MessageType<JobCreateReply> {
*/
export const JobCreateReply = new JobCreateReply$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobEditStateRequest$Type extends MessageType<JobEditStateRequest> {
constructor() {
super("service.JobEditStateRequest", [
{ no: 1, name: "id", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 2, name: "status", kind: "enum", opt: true, T: () => ["job.JobStatus", JobStatus] },
{ no: 3, name: "state", kind: "message", T: () => JobState }
]);
}
create(value?: PartialMessage<JobEditStateRequest>): JobEditStateRequest {
const message = { id: 0n };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<JobEditStateRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobEditStateRequest): JobEditStateRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int64 id */ 1:
message.id = reader.int64().toBigInt();
break;
case /* optional job.JobStatus status */ 2:
message.status = reader.int32();
break;
case /* optional job.JobState state */ 3:
message.state = JobState.internalBinaryRead(reader, reader.uint32(), options, message.state);
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: JobEditStateRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int64 id = 1; */
if (message.id !== 0n)
writer.tag(1, WireType.Varint).int64(message.id);
/* optional job.JobStatus status = 2; */
if (message.status !== undefined)
writer.tag(2, WireType.Varint).int32(message.status);
/* optional job.JobState state = 3; */
if (message.state)
JobState.internalBinaryWrite(message.state, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message service.JobEditStateRequest
*/
export const JobEditStateRequest = new JobEditStateRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobEditStateReply$Type extends MessageType<JobEditStateReply> {
constructor() {
super("service.JobEditStateReply", []);
}
create(value?: PartialMessage<JobEditStateReply>): JobEditStateReply {
const message = {};
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<JobEditStateReply>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobEditStateReply): JobEditStateReply {
return target ?? this.create();
}
internalBinaryWrite(message: JobEditStateReply, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message service.JobEditStateReply
*/
export const JobEditStateReply = new JobEditStateReply$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobDeleteRequest$Type extends MessageType<JobDeleteRequest> {
constructor() {
super("service.JobDeleteRequest", [
@@ -1602,21 +1709,21 @@ class JobDeleteReply$Type extends MessageType<JobDeleteReply> {
*/
export const JobDeleteReply = new JobDeleteReply$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobNextRequest$Type extends MessageType<JobNextRequest> {
class JobDispatchRequest$Type extends MessageType<JobDispatchRequest> {
constructor() {
super("service.JobNextRequest", [
super("service.JobDispatchRequest", [
{ no: 1, name: "id", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 2, name: "param", kind: "message", T: () => JobNextParam }
{ no: 2, name: "param", kind: "message", T: () => JobDispatchParam }
]);
}
create(value?: PartialMessage<JobNextRequest>): JobNextRequest {
create(value?: PartialMessage<JobDispatchRequest>): JobDispatchRequest {
const message = { id: 0n };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<JobNextRequest>(this, message, value);
reflectionMergePartial<JobDispatchRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobNextRequest): JobNextRequest {
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobDispatchRequest): JobDispatchRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
@@ -1624,8 +1731,8 @@ class JobNextRequest$Type extends MessageType<JobNextRequest> {
case /* int64 id */ 1:
message.id = reader.int64().toBigInt();
break;
case /* job.JobNextParam param */ 2:
message.param = JobNextParam.internalBinaryRead(reader, reader.uint32(), options, message.param);
case /* job.JobDispatchParam param */ 2:
message.param = JobDispatchParam.internalBinaryRead(reader, reader.uint32(), options, message.param);
break;
default:
let u = options.readUnknownField;
@@ -1638,13 +1745,13 @@ class JobNextRequest$Type extends MessageType<JobNextRequest> {
}
return message;
}
internalBinaryWrite(message: JobNextRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
internalBinaryWrite(message: JobDispatchRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int64 id = 1; */
if (message.id !== 0n)
writer.tag(1, WireType.Varint).int64(message.id);
/* job.JobNextParam param = 2; */
/* job.JobDispatchParam param = 2; */
if (message.param)
JobNextParam.internalBinaryWrite(message.param, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
JobDispatchParam.internalBinaryWrite(message.param, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -1652,46 +1759,25 @@ class JobNextRequest$Type extends MessageType<JobNextRequest> {
}
}
/**
* @generated MessageType for protobuf message service.JobNextRequest
* @generated MessageType for protobuf message service.JobDispatchRequest
*/
export const JobNextRequest = new JobNextRequest$Type();
export const JobDispatchRequest = new JobDispatchRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobNextReply$Type extends MessageType<JobNextReply> {
class JobDispatchReply$Type extends MessageType<JobDispatchReply> {
constructor() {
super("service.JobNextReply", [
{ no: 1, name: "job", kind: "message", T: () => Job }
]);
super("service.JobDispatchReply", []);
}
create(value?: PartialMessage<JobNextReply>): JobNextReply {
create(value?: PartialMessage<JobDispatchReply>): JobDispatchReply {
const message = {};
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<JobNextReply>(this, message, value);
reflectionMergePartial<JobDispatchReply>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobNextReply): JobNextReply {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* job.Job job */ 1:
message.job = Job.internalBinaryRead(reader, reader.uint32(), options, message.job);
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobDispatchReply): JobDispatchReply {
return target ?? this.create();
}
internalBinaryWrite(message: JobNextReply, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* job.Job job = 1; */
if (message.job)
Job.internalBinaryWrite(message.job, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
internalBinaryWrite(message: JobDispatchReply, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -1699,9 +1785,9 @@ class JobNextReply$Type extends MessageType<JobNextReply> {
}
}
/**
* @generated MessageType for protobuf message service.JobNextReply
* @generated MessageType for protobuf message service.JobDispatchReply
*/
export const JobNextReply = new JobNextReply$Type();
export const JobDispatchReply = new JobDispatchReply$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobDisplayRequest$Type extends MessageType<JobDisplayRequest> {
constructor() {
@@ -2281,8 +2367,9 @@ export const Service = new ServiceType("service.Service", [
{ name: "TapeGetPositions", options: {}, I: TapeGetPositionsRequest, O: TapeGetPositionsReply },
{ name: "JobList", options: {}, I: JobListRequest, O: JobListReply },
{ name: "JobCreate", options: {}, I: JobCreateRequest, O: JobCreateReply },
{ name: "JobEditState", options: {}, I: JobEditStateRequest, O: JobEditStateReply },
{ name: "JobDelete", options: {}, I: JobDeleteRequest, O: JobDeleteReply },
{ name: "JobNext", options: {}, I: JobNextRequest, O: JobNextReply },
{ name: "JobDispatch", options: {}, I: JobDispatchRequest, O: JobDispatchReply },
{ name: "JobDisplay", options: {}, I: JobDisplayRequest, O: JobDisplayReply },
{ name: "JobGetLog", options: {}, I: JobGetLogRequest, O: JobGetLogReply },
{ name: "SourceList", options: {}, I: SourceListRequest, O: SourceListReply },
+33 -619
View File
@@ -1,56 +1,32 @@
import { Fragment, ChangeEvent } from "react";
import { useState, useRef, useEffect, useMemo, useCallback, FC } from "react";
import { createContext, useContext } from "react";
import { assert } from "@protobuf-ts/runtime";
import format from "format-duration";
import { useState, useRef, useEffect, useCallback } from "react";
import { createContext } from "react";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText";
import Typography from "@mui/material/Typography";
import Card from "@mui/material/Card";
import CardActions from "@mui/material/CardActions";
import CardContent from "@mui/material/CardContent";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import Chip, { ChipProps } from "@mui/material/Chip";
import Stack from "@mui/material/Stack";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import LinearProgress from "@mui/material/LinearProgress";
import Divider from "@mui/material/Divider";
import { TreeView, TreeItem } from "@mui/x-tree-view";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { cli, fileBase, JOB_STATUS_VISIBLE } from "../api";
import { sleep } from "../tools";
import { Job, JobDisplay, JobListRequest, JobNextRequest, JobStatus, CopyStatus, LibraryEntityType, JobDeleteRequest } from "../entity";
import { Job, JobDisplay, JobListRequest, JobStatus, LibraryEntityType } from "../entity";
import { JobArchiveCopyingParam, JobArchiveStep, JobArchiveDisplay, JobArchiveState } from "../entity";
import { SourceState } from "../entity";
import { download } from "../tools";
import { JobRestoreCopyingParam, JobRestoreStep, JobRestoreDisplay, JobRestoreState } from "../entity";
import { RestoreTape } from "../entity";
import { formatFilesize, download } from "../tools";
import { Nullable } from "tsdef";
import { JobCard } from "../components/job-card";
import { ArchiveCard } from "../components/job-archive";
import { RestoreCard } from "../components/job-restore";
export const JobsType = "jobs";
type DisplayableJob = Job & Partial<JobDisplay>;
export const RefreshContext = createContext<() => Promise<void>>(async () => {});
const RefreshContext = createContext<() => Promise<void>>(async () => {});
type DisplayableJob = Job & Partial<JobDisplay>;
export const JobsBrowser = () => {
const [jobs, setJobs] = useState<DisplayableJob[] | null>(null);
@@ -58,7 +34,7 @@ export const JobsBrowser = () => {
const refresh = useCallback(
async (refresh?: boolean) => {
const results = await (async () => {
const [results, updated] = await (async () => {
const req: JobListRequest = refresh
? { param: { oneofKind: "list", list: {} } }
: { param: { oneofKind: "recentlyUpdate", recentlyUpdate: { updateSinceNs: latestUpdateTimeNs } } };
@@ -66,9 +42,9 @@ export const JobsBrowser = () => {
const reply = await cli.jobList(req).response;
if (reply.jobs.length === 0) {
if (refresh) {
return [];
return [[], true];
}
return Array.from(jobs || []);
return [Array.from(jobs || []), false];
}
const latest = reply.jobs.reduce((latest, job) => {
@@ -93,25 +69,28 @@ export const JobsBrowser = () => {
results.push(job);
}
return results.filter((job) => job && job.status < JOB_STATUS_VISIBLE).sort((a, b) => Number(b.createTimeNs - a.createTimeNs));
return [results.filter((job) => job && job.status < JOB_STATUS_VISIBLE).sort((a, b) => Number(b.createTimeNs - a.createTimeNs)), true];
})();
const displays = new Map<BigInt, JobDisplay>();
const processingJobs = results.filter((job) => job.status === JobStatus.PROCESSING);
if (processingJobs.length === 0) {
if (updated) {
setJobs(results);
}
return;
}
for (const reply of await Promise.all(
results
.filter((job) => job.status === JobStatus.PROCESSING)
.map((job) => cli.jobDisplay({ id: job.id }).response.then((reply) => ({ ...reply, jobID: job.id }))),
processingJobs.map((job) => cli.jobDisplay({ id: job.id }).response.then((reply) => ({ ...reply, jobID: job.id }))),
)) {
if (!reply.display) {
continue;
}
displays.set(reply.jobID, reply.display);
}
const targets = results.map((job) => ({ ...job, ...displays.get(job.id) }));
console.log("refresh jobs list, set jobs, jobs=", targets);
setJobs(targets);
setJobs(results.map((job) => ({ ...job, ...displays.get(job.id) })));
},
[jobs, setJobs, latestUpdateTimeNs, setLatestUpdateTimeNs],
);
@@ -133,6 +112,7 @@ export const JobsBrowser = () => {
clearInterval(timer);
};
}, []);
useEffect(() => console.log("jobs changed,", jobs), [jobs]);
return (
<RefreshContext.Provider value={refresh}>
@@ -153,20 +133,19 @@ export const JobsBrowser = () => {
// </ListSubheader>
// }
>
{/* <NewArchiveDialog refresh={refresh} /> */}
<ListItemButton
onClick={async () => {
const resp = await cli.libraryExport({ types: [LibraryEntityType.FILE, LibraryEntityType.TAPE, LibraryEntityType.POSITION] }).response;
download(resp.json, "database.json", "application/json");
download(resp.json, "library.json", "application/json");
}}
>
<ListItemText primary="Export Database" />
<ListItemText primary="Export Library" />
</ListItemButton>
<ImportDatabaseDialog />
</List>
</Grid>
<Grid className="browser" item xs={10}>
<div className="job-list">{jobs ? jobs.map((job) => <GetJobCard job={job} key={job.id.toString()} refresh={refresh} />) : <LinearProgress />}</div>
<div className="job-list">{jobs ? jobs.map((job) => <GetJobCard job={job} key={job.id.toString()} />) : <LinearProgress />}</div>
</Grid>
</Grid>
</Box>
@@ -211,11 +190,11 @@ const ImportDatabaseDialog = () => {
return (
<Fragment>
<ListItemButton onClick={handleClickOpen}>
<ListItemText primary="Import Database" />
<ListItemText primary="Import Library" />
</ListItemButton>
{open && (
<Dialog open={true} onClose={handleClose} maxWidth={"sm"} fullWidth>
<DialogTitle>Load Tape</DialogTitle>
<DialogTitle>Upload Library JSON</DialogTitle>
<DialogContent>
<Button variant="contained" component="label">
Upload File
@@ -233,7 +212,7 @@ const ImportDatabaseDialog = () => {
);
};
const GetJobCard = ({ job, refresh }: { job: DisplayableJob; refresh: () => Promise<void> }): JSX.Element => {
const GetJobCard = ({ job }: { job: DisplayableJob }): JSX.Element => {
if (!job.state) {
return <JobCard job={job} />;
}
@@ -241,575 +220,10 @@ const GetJobCard = ({ job, refresh }: { job: DisplayableJob; refresh: () => Prom
const type = job.state?.state.oneofKind;
switch (type) {
case "archive":
return (
<ArchiveCard job={job} refresh={refresh} state={job.state.state.archive} display={job.display?.oneofKind === "archive" ? job.display.archive : null} />
);
return <ArchiveCard job={job} state={job.state.state.archive} display={job.display?.oneofKind === "archive" ? job.display.archive : null} />;
case "restore":
return (
<RestoreCard job={job} refresh={refresh} state={job.state.state.restore} display={job.display?.oneofKind === "restore" ? job.display.restore : null} />
);
return <RestoreCard job={job} state={job.state.state.restore} display={job.display?.oneofKind === "restore" ? job.display.restore : null} />;
default:
return <JobCard job={job} />;
}
};
const ArchiveViewFilesDialog = ({ sources }: { sources: SourceState[] }) => {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
View Files
</Button>
{open && (
<Dialog open={true} onClose={handleClose} maxWidth={"lg"} fullWidth scroll="paper" sx={{ height: "100%" }} className="view-log-dialog">
<DialogTitle>View Files</DialogTitle>
<DialogContent dividers>
{sources.map((src) => {
if (!src.source) {
return null;
}
return (
<ListItemText
primary={src.source.base + src.source.path.join("/")}
secondary={`Size: ${formatFilesize(src.size)} Status: ${CopyStatus[src.status]}`}
/>
);
})}
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
const ArchiveCard = ({
job,
state,
display,
refresh,
}: {
job: Job;
state: JobArchiveState;
display: JobArchiveDisplay | null;
refresh: () => Promise<void>;
}): JSX.Element => {
const [fields, progress] = useMemo(() => {
const totalFiles = state.sources.length;
let submitedFiles = 0,
submitedBytes = 0,
totalBytes = 0;
for (const file of state.sources) {
totalBytes += Number(file.size);
if (file.status !== CopyStatus.SUBMITED) {
continue;
}
submitedFiles++;
submitedBytes += Number(file.size);
}
const copiedFiles = submitedFiles + Number(display?.copiedFiles || 0n);
const copiedBytes = submitedBytes + Number(display?.copiedBytes || 0n);
const avgSpeed = (() => {
if (!display || !display.copiedBytes || !display.startTime) {
return NaN;
}
const duration = Date.now() / 1000 - Number(display.startTime);
if (duration <= 0) {
return NaN;
}
return Number(display.copiedBytes) / duration;
})();
const progress = (totalBytes > 0 ? copiedBytes / totalBytes : 1) * 100;
const fields = [
{ name: "Current Step", value: JobArchiveStep[state.step] },
{ name: "Current Speed", value: display?.speed ? `${formatFilesize(display?.speed)}/s` : "--" },
{ name: "Average Speed", value: !isNaN(avgSpeed) ? `${formatFilesize(avgSpeed)}/s` : "--" },
{ name: "Estimated Time", value: !isNaN(avgSpeed) ? format(((totalBytes - copiedBytes) * 1000) / avgSpeed) : "--" },
{ name: "Copied Files", value: copiedFiles },
{ name: "Copied Bytes", value: formatFilesize(copiedBytes) },
{ name: "Submited Files", value: submitedFiles },
{ name: "Submited Bytes", value: formatFilesize(submitedBytes) },
{ name: "Total Files", value: totalFiles },
{ name: "Total Bytes", value: formatFilesize(totalBytes) },
];
return [fields, progress];
}, [state, display]);
return (
<JobCard
job={job}
detail={
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ paddingTop: "1em" }}>
<LinearProgress variant="determinate" value={progress} />
</Box>
</Grid>
{fields.map((field, idx) => (
<Grid item xs={12} md={3} key={idx}>
<Typography variant="body1">
<b>{field.name}</b>: {field.value}
</Typography>
</Grid>
))}
</Grid>
}
buttons={
<Fragment>
{state.step === JobArchiveStep.WAIT_FOR_TAPE && <NewTapeDialog job={job} refresh={refresh} />}
<ViewLogDialog jobID={job.id} />
<ArchiveViewFilesDialog sources={state.sources} />
</Fragment>
}
/>
);
};
const RestoreViewFilesDialog = ({ tapes }: { tapes: RestoreTape[] }) => {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
View Files
</Button>
{open && (
<Dialog open={true} onClose={handleClose} maxWidth={"lg"} fullWidth scroll="paper" sx={{ height: "100%" }} className="view-log-dialog">
<DialogTitle>View Files</DialogTitle>
<DialogContent dividers>
<TreeView defaultCollapseIcon={<ExpandMoreIcon />} defaultExpandIcon={<ChevronRightIcon />}>
{tapes.map((tape) => {
if (!tape.files) {
return null;
}
return (
<TreeItem label={tape.barcode} nodeId={`tape-${tape.tapeId}`}>
{tape.files.map((file) => (
<TreeItem
label={
<pre style={{ margin: 0 }}>
{file.tapePath} <b>{CopyStatus[file.status]}</b>
</pre>
}
nodeId={`file-${file.positionId}`}
/>
))}
</TreeItem>
);
})}
</TreeView>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
const tapeStatusToColor = (status: CopyStatus): ChipProps["color"] => {
switch (status) {
case CopyStatus.DRAFT:
return "primary";
case CopyStatus.PENDING:
return "primary";
case CopyStatus.RUNNING:
return "secondary";
case CopyStatus.STAGED:
return "warning";
case CopyStatus.SUBMITED:
return "success";
case CopyStatus.FAILED:
return "error";
default:
return "default";
}
};
const RestoreCard = ({
job,
state,
display,
refresh,
}: {
job: Job;
state: JobRestoreState;
display: JobRestoreDisplay | null;
refresh: () => Promise<void>;
}): JSX.Element => {
const [fields, progress] = useMemo(() => {
const totalFiles = state.tapes.reduce((count, tape) => count + tape.files.length, 0);
let successFiles = 0,
successBytes = 0,
copiedFiles = Number(display?.copiedFiles || 0n),
copiedBytes = Number(display?.copiedBytes || 0n),
totalBytes = 0;
for (const tape of state.tapes) {
for (const file of tape.files) {
totalBytes += Number(file.size);
if (file.status === CopyStatus.SUBMITED || file.status === CopyStatus.STAGED) {
successFiles++;
successBytes += Number(file.size);
}
if (file.status === CopyStatus.SUBMITED) {
copiedFiles++;
copiedBytes += Number(file.size);
}
}
}
const avgSpeed = (() => {
if (!display || !display.copiedBytes || !display.startTime) {
return NaN;
}
const duration = Date.now() / 1000 - Number(display.startTime);
if (duration <= 0) {
return NaN;
}
return Number(display.copiedBytes) / duration;
})();
const progress = (totalBytes > 0 ? copiedBytes / totalBytes : 1) * 100;
const fields = [
{ name: "Current Step", value: JobArchiveStep[state.step] },
{ name: "Current Speed", value: display?.speed ? `${formatFilesize(display?.speed)}/s` : "--" },
{ name: "Average Speed", value: !isNaN(avgSpeed) ? `${formatFilesize(avgSpeed)}/s` : "--" },
{ name: "Estimated Time", value: !isNaN(avgSpeed) ? format(((totalBytes - copiedBytes) * 1000) / avgSpeed) : "--" },
{ name: "Copied Files", value: copiedFiles },
{ name: "Copied Bytes", value: formatFilesize(copiedBytes) },
{ name: "Success Files", value: successFiles },
{ name: "Success Bytes", value: formatFilesize(successBytes) },
{ name: "Total Files", value: totalFiles },
{ name: "Total Bytes", value: formatFilesize(totalBytes) },
];
return [fields, progress];
}, [state, display]);
return (
<JobCard
job={job}
detail={
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ paddingTop: "1em" }}>
<LinearProgress variant="determinate" value={progress} />
</Box>
</Grid>
{fields.map((field, idx) => (
<Grid item xs={12} md={3} key={idx}>
<Typography variant="body1">
<b>{field.name}</b>: {field.value}
</Typography>
</Grid>
))}
<Grid item xs={12} md={12}>
<Stack direction="row" spacing={1}>
{state.tapes.map((tape) => (
<Chip label={`${tape.barcode}: ${CopyStatus[tape.status]}`} color={tapeStatusToColor(tape.status)} variant="outlined" key={`${tape.tapeId}`} />
))}
</Stack>
</Grid>
</Grid>
}
buttons={
<Fragment>
{state.step === JobRestoreStep.WAIT_FOR_TAPE && <LoadTapeDialog job={job} refresh={refresh} />}
<ViewLogDialog jobID={job.id} />
<RestoreViewFilesDialog tapes={state.tapes} />
</Fragment>
}
/>
);
};
const NewTapeDialog = ({ job, refresh }: { job: Job; refresh: () => Promise<void> }) => {
const [devices, setDevices] = useState<string[]>([]);
const [param, setParam] = useState<JobArchiveCopyingParam | null>(null);
const handleClickOpen = async () => {
const reply = await cli.deviceList({}).response;
setDevices(reply.devices);
setParam(JobArchiveCopyingParam.create());
};
const handleClose = () => {
setParam(null);
setDevices([]);
};
const handleChange = (key: keyof JobArchiveCopyingParam) => (event: ChangeEvent<HTMLInputElement>) => {
if (param === null) {
return;
}
setParam({ ...param, [key]: event.target.value });
};
const handleSubmit = async () => {
if (!param) {
return;
}
const trimedParam: JobArchiveCopyingParam = {
device: param.device,
barcode: param.barcode.toUpperCase(),
name: param.name,
};
assert(trimedParam.barcode.length === 6);
const reply = await cli.jobNext(makeArchiveCopyingParam(job.id, trimedParam)).response;
console.log("job next reply= ", reply);
await refresh();
handleClose();
};
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
Load Tape
</Button>
{param && (
<Dialog open={true} onClose={handleClose} maxWidth={"sm"} fullWidth>
<DialogTitle>Load Tape</DialogTitle>
<DialogContent>
<DialogContentText>After load tape into tape drive, click 'Submit'</DialogContentText>
<TextField select required margin="dense" label="Drive Device" fullWidth variant="standard" value={param.device} onChange={handleChange("device")}>
{devices.map((device) => (
<MenuItem key={device} value={device}>
{device}
</MenuItem>
))}
</TextField>
<TextField required margin="dense" label="Tape Barcode" fullWidth variant="standard" value={param.barcode} onChange={handleChange("barcode")} />
<TextField required margin="dense" label="Tape Name" fullWidth variant="standard" value={param.name} onChange={handleChange("name")} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleSubmit}>Submit</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
const LoadTapeDialog = ({ job, refresh }: { job: Job; refresh: () => Promise<void> }) => {
const [devices, setDevices] = useState<string[] | null>(null);
const [device, setDevice] = useState<string | null>(null);
const handleClickOpen = async () => {
const reply = await cli.deviceList({}).response;
setDevices(reply.devices);
};
const handleClose = () => {
setDevices(null);
setDevice(null);
};
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDevice(event.target.value);
};
const handleSubmit = async () => {
if (!device) {
return;
}
const trimedParam: JobRestoreCopyingParam = {
device: device,
};
const reply = await cli.jobNext(makeRestoreCopyingParam(job.id, trimedParam)).response;
console.log("job next reply= ", reply);
await refresh();
handleClose();
};
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
Load Tape
</Button>
{devices && (
<Dialog open={true} onClose={handleClose} maxWidth={"sm"} fullWidth>
<DialogTitle>Load Tape</DialogTitle>
<DialogContent>
<DialogContentText>After load tape into tape drive, click 'Submit'</DialogContentText>
<TextField select required margin="dense" label="Drive Device" fullWidth variant="standard" value={device} onChange={handleChange}>
{devices.map((device) => (
<MenuItem key={device} value={device}>
{device}
</MenuItem>
))}
</TextField>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleSubmit}>Submit</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
const ViewLogDialog = ({ jobID }: { jobID: bigint }) => {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<Fragment>
<Button size="small" onClick={handleClickOpen}>
View Log
</Button>
{open && (
<Dialog open={true} onClose={handleClose} maxWidth={"lg"} fullWidth scroll="paper" sx={{ height: "100%" }} className="view-log-dialog">
<DialogTitle>View Log</DialogTitle>
<DialogContent dividers>
<LogConsole jobId={jobID} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
)}
</Fragment>
);
};
const LogConsole = ({ jobId }: { jobId: bigint }) => {
const [log, setLog] = useState<string>("");
const [offset, setOffset] = useState(0n);
const bottom = useRef(null);
const refresh = useCallback(async () => {
const reply = await cli.jobGetLog({ jobId, offset: offset }).response;
setLog(log + new TextDecoder().decode(reply.logs));
setOffset(reply.offset);
}, [log, setLog, offset, setOffset, bottom]);
const refreshRef = useRef(refresh);
refreshRef.current = refresh;
useEffect(() => {
var timer: NodeJS.Timeout;
(async () => {
await refreshRef.current();
if (bottom.current) {
const bottomElem = bottom.current as HTMLElement;
await sleep(10);
bottomElem.scrollIntoView(true);
await sleep(10);
bottomElem.parentElement?.scrollBy(0, 100);
}
timer = setInterval(() => refreshRef.current(), 2000);
})();
return () => {
if (!timer) {
return;
}
clearInterval(timer);
};
}, [refresh]);
return (
<Fragment>
<pre>{log || "loading..."}</pre>
<div ref={bottom} />
</Fragment>
);
};
const DeleteJobButton = ({ jobID }: { jobID: bigint }) => {
const refresh = useContext(RefreshContext);
const deleteJob = useCallback(async () => {
await cli.jobDelete(JobDeleteRequest.create({ ids: [jobID] }));
await refresh();
}, [jobID]);
return (
<Button size="small" onClick={deleteJob} style={{ marginLeft: "auto", marginRight: 0 }}>
Delete Job
</Button>
);
};
const JobCard = ({ job, detail, buttons }: { job: Job; detail?: JSX.Element; buttons?: JSX.Element }) => {
return (
<Card sx={{ textAlign: "left" }} className="job-detail">
<CardContent>
<Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom>
{`${JobStatus[job.status]}`}
</Typography>
<Typography variant="h5" component="div">{`Job ${job.id} - ${job.state?.state.oneofKind?.toUpperCase()}`}</Typography>
{detail}
</CardContent>
<Divider />
<CardActions>
{buttons}
<DeleteJobButton jobID={job.id} />
</CardActions>
</Card>
);
};
function makeArchiveCopyingParam(jobID: bigint, param: JobArchiveCopyingParam): JobNextRequest {
return {
id: jobID,
param: {
param: {
oneofKind: "archive",
archive: {
param: {
oneofKind: "copying",
copying: param,
},
},
},
},
};
}
function makeRestoreCopyingParam(jobID: bigint, param: JobRestoreCopyingParam): JobNextRequest {
return {
id: jobID,
param: {
param: {
oneofKind: "restore",
restore: {
param: {
oneofKind: "copying",
copying: param,
},
},
},
},
};
}
+1 -1
View File
@@ -65,7 +65,7 @@ func (l *Library) ListPositions(ctx context.Context, tapeID int64, prefix string
return nil, fmt.Errorf("find position by file id fail, %w", r.Error)
}
convertPath := tools.Cache(func(p string) string { return strings.ReplaceAll(p, "/", "\x00") })
convertPath := tools.ThreadUnsafeCache(func(p string) string { return strings.ReplaceAll(p, "/", "\x00") })
sort.Slice(positions, func(i int, j int) bool {
return convertPath(positions[i].Path) < convertPath(positions[j].Path)
})
+67 -1
View File
@@ -1,6 +1,9 @@
package tools
import "sync"
import (
"context"
"sync"
)
func Cache[K comparable, V any](f func(in K) V) func(in K) V {
cache := new(sync.Map)
@@ -15,3 +18,66 @@ func Cache[K comparable, V any](f func(in K) V) func(in K) V {
return out
}
}
func ThreadUnsafeCache[K comparable, V any](f func(in K) V) func(in K) V {
cache := make(map[K]V, 32)
return func(in K) V {
cached, has := cache[in]
if has {
return cached
}
out := f(in)
cache[in] = out
return out
}
}
type CacheOnce[K comparable, V any] struct {
cached sync.Map
getter func(context.Context, K) (V, error)
}
type cacheOnceItem[V any] struct {
once sync.Once
value V
}
func NewCacheOnce[K comparable, V any](getter func(context.Context, K) (V, error)) *CacheOnce[K, V] {
return &CacheOnce[K, V]{getter: getter}
}
func (c *CacheOnce[K, V]) Get(ctx context.Context, req K) (V, error) {
if v, have := c.cached.Load(req); have {
item, ok := v.(*cacheOnceItem[V])
if ok {
return c.get(ctx, item, req)
}
}
v, _ := c.cached.LoadOrStore(req, &cacheOnceItem[V]{})
return c.get(ctx, v.(*cacheOnceItem[V]), req)
}
func (c *CacheOnce[K, V]) Remove(req K) {
c.cached.Delete(req)
}
func (c *CacheOnce[K, V]) get(ctx context.Context, item *cacheOnceItem[V], key K) (V, error) {
var err error
item.once.Do(func() {
v, e := c.getter(ctx, key)
if e != nil {
err = e
return
}
item.value = v
})
if err != nil {
var v V
return v, err
}
return item.value, nil
}