From 33518f1642c5bfeb897f61a92fc3f5edc3ed2c63 Mon Sep 17 00:00:00 2001 From: Gareth George Date: Wed, 22 Apr 2026 01:05:10 -0700 Subject: [PATCH] misc bug fixes, add lock hooks, dashboard improvements --- gen/go/v1/config.pb.go | 120 +++++- gen/go/v1sync/syncservice.pb.go | 381 ++++++++++++++++-- internal/api/syncapi/authmiddleware.go | 13 +- internal/api/syncapi/cmdstreamutil.go | 18 +- internal/api/syncapi/lockmanager.go | 69 ++++ internal/api/syncapi/syncclient.go | 144 ++++++- internal/api/syncapi/synccommon.go | 45 +++ internal/api/syncapi/syncmanager.go | 108 ++++- internal/api/syncapi/syncserver.go | 55 ++- internal/hook/types/synclock.go | 160 ++++++++ internal/orchestrator/orchestrator.go | 21 +- internal/orchestrator/tasks/hookvars.go | 4 + internal/protoutil/conditions.go | 1 + proto/v1/config.proto | 10 + proto/v1sync/syncservice.proto | 26 ++ webui/gen/ts/v1/config_pb.ts | 50 ++- webui/gen/ts/v1sync/syncservice_pb.ts | 132 +++++- webui/src/app/App.tsx | 11 + .../features/dashboard/SummaryDashboard.tsx | 49 ++- 19 files changed, 1312 insertions(+), 105 deletions(-) create mode 100644 internal/api/syncapi/lockmanager.go create mode 100644 internal/hook/types/synclock.go diff --git a/gen/go/v1/config.pb.go b/gen/go/v1/config.pb.go index cb87d846..e96c5eed 100644 --- a/gen/go/v1/config.pb.go +++ b/gen/go/v1/config.pb.go @@ -260,6 +260,9 @@ const ( Hook_CONDITION_FORGET_START Hook_Condition = 300 // forget started. Hook_CONDITION_FORGET_ERROR Hook_Condition = 301 // forget failed. Hook_CONDITION_FORGET_SUCCESS Hook_Condition = 302 // forget succeeded. + // any operation conditions + Hook_CONDITION_ANY_START Hook_Condition = 400 // before any operation starts. + Hook_CONDITION_ANY_END Hook_Condition = 401 // after any operation ends (success or fail). ) // Enum value maps for Hook_Condition. @@ -282,6 +285,8 @@ var ( 300: "CONDITION_FORGET_START", 301: "CONDITION_FORGET_ERROR", 302: "CONDITION_FORGET_SUCCESS", + 400: "CONDITION_ANY_START", + 401: "CONDITION_ANY_END", } Hook_Condition_value = map[string]int32{ "CONDITION_UNKNOWN": 0, @@ -301,6 +306,8 @@ var ( "CONDITION_FORGET_START": 300, "CONDITION_FORGET_ERROR": 301, "CONDITION_FORGET_SUCCESS": 302, + "CONDITION_ANY_START": 400, + "CONDITION_ANY_END": 401, } ) @@ -1302,6 +1309,7 @@ type Hook struct { // *Hook_ActionShoutrrr // *Hook_ActionHealthchecks // *Hook_ActionTelegram + // *Hook_ActionSyncLock Action isHook_Action `protobuf_oneof:"action"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1430,6 +1438,15 @@ func (x *Hook) GetActionTelegram() *Hook_Telegram { return nil } +func (x *Hook) GetActionSyncLock() *Hook_SyncLock { + if x != nil { + if x, ok := x.Action.(*Hook_ActionSyncLock); ok { + return x.ActionSyncLock + } + } + return nil +} + type isHook_Action interface { isHook_Action() } @@ -1466,6 +1483,10 @@ type Hook_ActionTelegram struct { ActionTelegram *Hook_Telegram `protobuf:"bytes,107,opt,name=action_telegram,json=actionTelegram,proto3,oneof"` } +type Hook_ActionSyncLock struct { + ActionSyncLock *Hook_SyncLock `protobuf:"bytes,109,opt,name=action_sync_lock,json=actionSyncLock,proto3,oneof"` +} + func (*Hook_ActionCommand) isHook_Action() {} func (*Hook_ActionWebhook) isHook_Action() {} @@ -1482,6 +1503,8 @@ func (*Hook_ActionHealthchecks) isHook_Action() {} func (*Hook_ActionTelegram) isHook_Action() {} +func (*Hook_ActionSyncLock) isHook_Action() {} + type Auth struct { state protoimpl.MessageState `protogen:"open.v1"` Disabled bool `protobuf:"varint,1,opt,name=disabled,proto3" json:"disabled,omitempty"` // disable authentication. @@ -2364,6 +2387,58 @@ func (x *Hook_Telegram) GetTemplate() string { return "" } +type Hook_SyncLock struct { + state protoimpl.MessageState `protogen:"open.v1"` + TargetInstanceId string `protobuf:"bytes,1,opt,name=target_instance_id,json=targetInstanceId,proto3" json:"target_instance_id,omitempty"` // the instance ID of the peer to acquire the lock on. + LockKey string `protobuf:"bytes,2,opt,name=lock_key,json=lockKey,proto3" json:"lock_key,omitempty"` // the lock key, typically the repo ID. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Hook_SyncLock) Reset() { + *x = Hook_SyncLock{} + mi := &file_v1_config_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook_SyncLock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hook_SyncLock) ProtoMessage() {} + +func (x *Hook_SyncLock) ProtoReflect() protoreflect.Message { + mi := &file_v1_config_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hook_SyncLock.ProtoReflect.Descriptor instead. +func (*Hook_SyncLock) Descriptor() ([]byte, []int) { + return file_v1_config_proto_rawDescGZIP(), []int{9, 8} +} + +func (x *Hook_SyncLock) GetTargetInstanceId() string { + if x != nil { + return x.TargetInstanceId + } + return "" +} + +func (x *Hook_SyncLock) GetLockKey() string { + if x != nil { + return x.LockKey + } + return "" +} + var File_v1_config_proto protoreflect.FileDescriptor const file_v1_config_proto_rawDesc = "" + @@ -2484,7 +2559,7 @@ const file_v1_config_proto_rawDesc = "" + "\tCLOCK_UTC\x10\x02\x12\x17\n" + "\x13CLOCK_LAST_RUN_TIME\x10\x03B\n" + "\n" + - "\bschedule\"\xe1\x0f\n" + + "\bschedule\"\xa7\x11\n" + "\x04Hook\x122\n" + "\n" + "conditions\x18\x01 \x03(\x0e2\x12.v1.Hook.ConditionR\n" + @@ -2497,7 +2572,8 @@ const file_v1_config_proto_rawDesc = "" + "\faction_slack\x18h \x01(\v2\x0e.v1.Hook.SlackH\x00R\vactionSlack\x12<\n" + "\x0faction_shoutrrr\x18i \x01(\v2\x11.v1.Hook.ShoutrrrH\x00R\x0eactionShoutrrr\x12H\n" + "\x13action_healthchecks\x18j \x01(\v2\x15.v1.Hook.HealthchecksH\x00R\x12actionHealthchecks\x12<\n" + - "\x0faction_telegram\x18k \x01(\v2\x11.v1.Hook.TelegramH\x00R\x0eactionTelegram\x1a#\n" + + "\x0faction_telegram\x18k \x01(\v2\x11.v1.Hook.TelegramH\x00R\x0eactionTelegram\x12=\n" + + "\x10action_sync_lock\x18m \x01(\v2\x11.v1.Hook.SyncLockH\x00R\x0eactionSyncLock\x1a#\n" + "\aCommand\x12\x18\n" + "\acommand\x18\x01 \x01(\tR\acommand\x1a\xa1\x01\n" + "\aWebhook\x12\x1f\n" + @@ -2533,7 +2609,10 @@ const file_v1_config_proto_rawDesc = "" + "\bTelegram\x12\x1b\n" + "\tbot_token\x18\x01 \x01(\tR\bbotToken\x12\x17\n" + "\achat_id\x18\x02 \x01(\tR\x06chatId\x12\x1a\n" + - "\btemplate\x18\x03 \x01(\tR\btemplate\"\xf5\x03\n" + + "\btemplate\x18\x03 \x01(\tR\btemplate\x1aS\n" + + "\bSyncLock\x12,\n" + + "\x12target_instance_id\x18\x01 \x01(\tR\x10targetInstanceId\x12\x19\n" + + "\block_key\x18\x02 \x01(\tR\alockKey\"\xa7\x04\n" + "\tCondition\x12\x15\n" + "\x11CONDITION_UNKNOWN\x10\x00\x12\x17\n" + "\x13CONDITION_ANY_ERROR\x10\x01\x12\x1c\n" + @@ -2551,7 +2630,9 @@ const file_v1_config_proto_rawDesc = "" + "\x17CONDITION_CHECK_SUCCESS\x10\xca\x01\x12\x1b\n" + "\x16CONDITION_FORGET_START\x10\xac\x02\x12\x1b\n" + "\x16CONDITION_FORGET_ERROR\x10\xad\x02\x12\x1d\n" + - "\x18CONDITION_FORGET_SUCCESS\x10\xae\x02\"\xa9\x01\n" + + "\x18CONDITION_FORGET_SUCCESS\x10\xae\x02\x12\x18\n" + + "\x13CONDITION_ANY_START\x10\x90\x03\x12\x16\n" + + "\x11CONDITION_ANY_END\x10\x91\x03\"\xa9\x01\n" + "\aOnError\x12\x13\n" + "\x0fON_ERROR_IGNORE\x10\x00\x12\x13\n" + "\x0fON_ERROR_CANCEL\x10\x01\x12\x12\n" + @@ -2582,7 +2663,7 @@ func file_v1_config_proto_rawDescGZIP() []byte { } var file_v1_config_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 24) +var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_v1_config_proto_goTypes = []any{ (Multihost_Permission_Type)(0), // 0: v1.Multihost.Permission.Type (CommandPrefix_IONiceLevel)(0), // 1: v1.CommandPrefix.IONiceLevel @@ -2615,14 +2696,15 @@ var file_v1_config_proto_goTypes = []any{ (*Hook_Shoutrrr)(nil), // 28: v1.Hook.Shoutrrr (*Hook_Healthchecks)(nil), // 29: v1.Hook.Healthchecks (*Hook_Telegram)(nil), // 30: v1.Hook.Telegram - (*PrivateKey)(nil), // 31: v1.PrivateKey + (*Hook_SyncLock)(nil), // 31: v1.Hook.SyncLock + (*PrivateKey)(nil), // 32: v1.PrivateKey } var file_v1_config_proto_depIdxs = []int32{ 9, // 0: v1.Config.repos:type_name -> v1.Repo 10, // 1: v1.Config.plans:type_name -> v1.Plan 17, // 2: v1.Config.auth:type_name -> v1.Auth 8, // 3: v1.Config.multihost:type_name -> v1.Multihost - 31, // 4: v1.Multihost.identity:type_name -> v1.PrivateKey + 32, // 4: v1.Multihost.identity:type_name -> v1.PrivateKey 19, // 5: v1.Multihost.known_hosts:type_name -> v1.Multihost.Peer 19, // 6: v1.Multihost.authorized_clients:type_name -> v1.Multihost.Peer 20, // 7: v1.Multihost.pairing_tokens:type_name -> v1.Multihost.PairingToken @@ -2649,16 +2731,17 @@ var file_v1_config_proto_depIdxs = []int32{ 28, // 28: v1.Hook.action_shoutrrr:type_name -> v1.Hook.Shoutrrr 29, // 29: v1.Hook.action_healthchecks:type_name -> v1.Hook.Healthchecks 30, // 30: v1.Hook.action_telegram:type_name -> v1.Hook.Telegram - 18, // 31: v1.Auth.users:type_name -> v1.User - 21, // 32: v1.Multihost.Peer.permissions:type_name -> v1.Multihost.Permission - 21, // 33: v1.Multihost.PairingToken.permissions:type_name -> v1.Multihost.Permission - 0, // 34: v1.Multihost.Permission.type:type_name -> v1.Multihost.Permission.Type - 6, // 35: v1.Hook.Webhook.method:type_name -> v1.Hook.Webhook.Method - 36, // [36:36] is the sub-list for method output_type - 36, // [36:36] is the sub-list for method input_type - 36, // [36:36] is the sub-list for extension type_name - 36, // [36:36] is the sub-list for extension extendee - 0, // [0:36] is the sub-list for field type_name + 31, // 31: v1.Hook.action_sync_lock:type_name -> v1.Hook.SyncLock + 18, // 32: v1.Auth.users:type_name -> v1.User + 21, // 33: v1.Multihost.Peer.permissions:type_name -> v1.Multihost.Permission + 21, // 34: v1.Multihost.PairingToken.permissions:type_name -> v1.Multihost.Permission + 0, // 35: v1.Multihost.Permission.type:type_name -> v1.Multihost.Permission.Type + 6, // 36: v1.Hook.Webhook.method:type_name -> v1.Hook.Webhook.Method + 37, // [37:37] is the sub-list for method output_type + 37, // [37:37] is the sub-list for method input_type + 37, // [37:37] is the sub-list for extension type_name + 37, // [37:37] is the sub-list for extension extendee + 0, // [0:37] is the sub-list for field type_name } func init() { file_v1_config_proto_init() } @@ -2691,6 +2774,7 @@ func file_v1_config_proto_init() { (*Hook_ActionShoutrrr)(nil), (*Hook_ActionHealthchecks)(nil), (*Hook_ActionTelegram)(nil), + (*Hook_ActionSyncLock)(nil), } file_v1_config_proto_msgTypes[11].OneofWrappers = []any{ (*User_PasswordBcrypt)(nil), @@ -2701,7 +2785,7 @@ func file_v1_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_config_proto_rawDesc), len(file_v1_config_proto_rawDesc)), NumEnums: 7, - NumMessages: 24, + NumMessages: 25, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/go/v1sync/syncservice.pb.go b/gen/go/v1sync/syncservice.pb.go index 51c23f79..9f4e5eb3 100644 --- a/gen/go/v1sync/syncservice.pb.go +++ b/gen/go/v1sync/syncservice.pb.go @@ -917,6 +917,10 @@ type SyncStreamItem struct { // *SyncStreamItem_ReceiveResources // *SyncStreamItem_RequestLog // *SyncStreamItem_ReceiveLogData + // *SyncStreamItem_AcquireLock + // *SyncStreamItem_AcquireLockResponse + // *SyncStreamItem_ReleaseLock + // *SyncStreamItem_RefreshLock // *SyncStreamItem_Throttle Action isSyncStreamItem_Action `protobuf_oneof:"action"` unknownFields protoimpl.UnknownFields @@ -1068,6 +1072,42 @@ func (x *SyncStreamItem) GetReceiveLogData() *SyncStreamItem_SyncActionReceiveLo return nil } +func (x *SyncStreamItem) GetAcquireLock() *SyncStreamItem_SyncActionAcquireLock { + if x != nil { + if x, ok := x.Action.(*SyncStreamItem_AcquireLock); ok { + return x.AcquireLock + } + } + return nil +} + +func (x *SyncStreamItem) GetAcquireLockResponse() *SyncStreamItem_SyncActionAcquireLockResponse { + if x != nil { + if x, ok := x.Action.(*SyncStreamItem_AcquireLockResponse); ok { + return x.AcquireLockResponse + } + } + return nil +} + +func (x *SyncStreamItem) GetReleaseLock() *SyncStreamItem_SyncActionReleaseLock { + if x != nil { + if x, ok := x.Action.(*SyncStreamItem_ReleaseLock); ok { + return x.ReleaseLock + } + } + return nil +} + +func (x *SyncStreamItem) GetRefreshLock() *SyncStreamItem_SyncActionRefreshLock { + if x != nil { + if x, ok := x.Action.(*SyncStreamItem_RefreshLock); ok { + return x.RefreshLock + } + } + return nil +} + func (x *SyncStreamItem) GetThrottle() *SyncStreamItem_SyncActionThrottle { if x != nil { if x, ok := x.Action.(*SyncStreamItem_Throttle); ok { @@ -1129,6 +1169,22 @@ type SyncStreamItem_ReceiveLogData struct { ReceiveLogData *SyncStreamItem_SyncActionReceiveLogData `protobuf:"bytes,31,opt,name=receive_log_data,json=receiveLogData,proto3,oneof"` } +type SyncStreamItem_AcquireLock struct { + AcquireLock *SyncStreamItem_SyncActionAcquireLock `protobuf:"bytes,40,opt,name=acquire_lock,json=acquireLock,proto3,oneof"` +} + +type SyncStreamItem_AcquireLockResponse struct { + AcquireLockResponse *SyncStreamItem_SyncActionAcquireLockResponse `protobuf:"bytes,41,opt,name=acquire_lock_response,json=acquireLockResponse,proto3,oneof"` +} + +type SyncStreamItem_ReleaseLock struct { + ReleaseLock *SyncStreamItem_SyncActionReleaseLock `protobuf:"bytes,42,opt,name=release_lock,json=releaseLock,proto3,oneof"` +} + +type SyncStreamItem_RefreshLock struct { + RefreshLock *SyncStreamItem_SyncActionRefreshLock `protobuf:"bytes,43,opt,name=refresh_lock,json=refreshLock,proto3,oneof"` +} + type SyncStreamItem_Throttle struct { Throttle *SyncStreamItem_SyncActionThrottle `protobuf:"bytes,1000,opt,name=throttle,proto3,oneof"` } @@ -1157,6 +1213,14 @@ func (*SyncStreamItem_RequestLog) isSyncStreamItem_Action() {} func (*SyncStreamItem_ReceiveLogData) isSyncStreamItem_Action() {} +func (*SyncStreamItem_AcquireLock) isSyncStreamItem_Action() {} + +func (*SyncStreamItem_AcquireLockResponse) isSyncStreamItem_Action() {} + +func (*SyncStreamItem_ReleaseLock) isSyncStreamItem_Action() {} + +func (*SyncStreamItem_RefreshLock) isSyncStreamItem_Action() {} + func (*SyncStreamItem_Throttle) isSyncStreamItem_Action() {} type SyncStreamItem_SyncActionHandshake struct { @@ -1861,6 +1925,215 @@ func (x *SyncStreamItem_SyncEstablishSharedSecret) GetEcdsaPub() string { return "" } +// Lock protocol messages for coordinating repo access between peers. +type SyncStreamItem_SyncActionAcquireLock struct { + state protoimpl.MessageState `protogen:"open.v1"` + LockKey string `protobuf:"bytes,1,opt,name=lock_key,json=lockKey,proto3" json:"lock_key,omitempty"` // the lock key, typically a repo ID. + HolderId string `protobuf:"bytes,2,opt,name=holder_id,json=holderId,proto3" json:"holder_id,omitempty"` // the instance ID of the lock requester. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncStreamItem_SyncActionAcquireLock) Reset() { + *x = SyncStreamItem_SyncActionAcquireLock{} + mi := &file_v1sync_syncservice_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionAcquireLock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionAcquireLock) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionAcquireLock) ProtoReflect() protoreflect.Message { + mi := &file_v1sync_syncservice_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionAcquireLock.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionAcquireLock) Descriptor() ([]byte, []int) { + return file_v1sync_syncservice_proto_rawDescGZIP(), []int{13, 14} +} + +func (x *SyncStreamItem_SyncActionAcquireLock) GetLockKey() string { + if x != nil { + return x.LockKey + } + return "" +} + +func (x *SyncStreamItem_SyncActionAcquireLock) GetHolderId() string { + if x != nil { + return x.HolderId + } + return "" +} + +type SyncStreamItem_SyncActionAcquireLockResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Acquired bool `protobuf:"varint,1,opt,name=acquired,proto3" json:"acquired,omitempty"` // whether the lock was acquired. + LockKey string `protobuf:"bytes,2,opt,name=lock_key,json=lockKey,proto3" json:"lock_key,omitempty"` // the lock key that was requested. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncStreamItem_SyncActionAcquireLockResponse) Reset() { + *x = SyncStreamItem_SyncActionAcquireLockResponse{} + mi := &file_v1sync_syncservice_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionAcquireLockResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionAcquireLockResponse) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionAcquireLockResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1sync_syncservice_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionAcquireLockResponse.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionAcquireLockResponse) Descriptor() ([]byte, []int) { + return file_v1sync_syncservice_proto_rawDescGZIP(), []int{13, 15} +} + +func (x *SyncStreamItem_SyncActionAcquireLockResponse) GetAcquired() bool { + if x != nil { + return x.Acquired + } + return false +} + +func (x *SyncStreamItem_SyncActionAcquireLockResponse) GetLockKey() string { + if x != nil { + return x.LockKey + } + return "" +} + +type SyncStreamItem_SyncActionReleaseLock struct { + state protoimpl.MessageState `protogen:"open.v1"` + LockKey string `protobuf:"bytes,1,opt,name=lock_key,json=lockKey,proto3" json:"lock_key,omitempty"` // the lock key to release. + HolderId string `protobuf:"bytes,2,opt,name=holder_id,json=holderId,proto3" json:"holder_id,omitempty"` // the instance ID of the lock holder. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncStreamItem_SyncActionReleaseLock) Reset() { + *x = SyncStreamItem_SyncActionReleaseLock{} + mi := &file_v1sync_syncservice_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionReleaseLock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionReleaseLock) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionReleaseLock) ProtoReflect() protoreflect.Message { + mi := &file_v1sync_syncservice_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionReleaseLock.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionReleaseLock) Descriptor() ([]byte, []int) { + return file_v1sync_syncservice_proto_rawDescGZIP(), []int{13, 16} +} + +func (x *SyncStreamItem_SyncActionReleaseLock) GetLockKey() string { + if x != nil { + return x.LockKey + } + return "" +} + +func (x *SyncStreamItem_SyncActionReleaseLock) GetHolderId() string { + if x != nil { + return x.HolderId + } + return "" +} + +type SyncStreamItem_SyncActionRefreshLock struct { + state protoimpl.MessageState `protogen:"open.v1"` + LockKey string `protobuf:"bytes,1,opt,name=lock_key,json=lockKey,proto3" json:"lock_key,omitempty"` // the lock key to refresh. + HolderId string `protobuf:"bytes,2,opt,name=holder_id,json=holderId,proto3" json:"holder_id,omitempty"` // the instance ID of the lock holder. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncStreamItem_SyncActionRefreshLock) Reset() { + *x = SyncStreamItem_SyncActionRefreshLock{} + mi := &file_v1sync_syncservice_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStreamItem_SyncActionRefreshLock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStreamItem_SyncActionRefreshLock) ProtoMessage() {} + +func (x *SyncStreamItem_SyncActionRefreshLock) ProtoReflect() protoreflect.Message { + mi := &file_v1sync_syncservice_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStreamItem_SyncActionRefreshLock.ProtoReflect.Descriptor instead. +func (*SyncStreamItem_SyncActionRefreshLock) Descriptor() ([]byte, []int) { + return file_v1sync_syncservice_proto_rawDescGZIP(), []int{13, 17} +} + +func (x *SyncStreamItem_SyncActionRefreshLock) GetLockKey() string { + if x != nil { + return x.LockKey + } + return "" +} + +func (x *SyncStreamItem_SyncActionRefreshLock) GetHolderId() string { + if x != nil { + return x.HolderId + } + return "" +} + var File_v1sync_syncservice_proto protoreflect.FileDescriptor const file_v1sync_syncservice_proto_rawDesc = "" + @@ -1922,7 +2195,7 @@ const file_v1sync_syncservice_proto_rawDesc = "" + "\n" + "public_key\x18\x01 \x01(\v2\r.v1.PublicKeyR\tpublicKey\x122\n" + "\vinstance_id\x18\x02 \x01(\v2\x11.v1.SignedMessageR\n" + - "instanceId\"\xff\x13\n" + + "instanceId\"\xaf\x19\n" + "\x0eSyncStreamItem\x12:\n" + "\x0esigned_message\x18\x01 \x01(\v2\x11.v1.SignedMessageH\x00R\rsignedMessage\x12J\n" + "\thandshake\x18\x03 \x01(\v2*.v1sync.SyncStreamItem.SyncActionHandshakeH\x00R\thandshake\x12J\n" + @@ -1937,7 +2210,11 @@ const file_v1sync_syncservice_proto_rawDesc = "" + "\x11receive_resources\x18\x1a \x01(\v21.v1sync.SyncStreamItem.SyncActionReceiveResourcesH\x00R\x10receiveResources\x12N\n" + "\vrequest_log\x18\x1e \x01(\v2+.v1sync.SyncStreamItem.SyncActionRequestLogH\x00R\n" + "requestLog\x12[\n" + - "\x10receive_log_data\x18\x1f \x01(\v2/.v1sync.SyncStreamItem.SyncActionReceiveLogDataH\x00R\x0ereceiveLogData\x12H\n" + + "\x10receive_log_data\x18\x1f \x01(\v2/.v1sync.SyncStreamItem.SyncActionReceiveLogDataH\x00R\x0ereceiveLogData\x12Q\n" + + "\facquire_lock\x18( \x01(\v2,.v1sync.SyncStreamItem.SyncActionAcquireLockH\x00R\vacquireLock\x12j\n" + + "\x15acquire_lock_response\x18) \x01(\v24.v1sync.SyncStreamItem.SyncActionAcquireLockResponseH\x00R\x13acquireLockResponse\x12Q\n" + + "\frelease_lock\x18* \x01(\v2,.v1sync.SyncStreamItem.SyncActionReleaseLockH\x00R\vreleaseLock\x12Q\n" + + "\frefresh_lock\x18+ \x01(\v2,.v1sync.SyncStreamItem.SyncActionRefreshLockH\x00R\vrefreshLock\x12H\n" + "\bthrottle\x18\xe8\a \x01(\v2).v1sync.SyncStreamItem.SyncActionThrottleH\x00R\bthrottle\x1a\xc9\x01\n" + "\x13SyncActionHandshake\x12)\n" + "\x10protocol_version\x18\x01 \x01(\x03R\x0fprotocolVersion\x12,\n" + @@ -1979,7 +2256,19 @@ const file_v1sync_syncservice_proto_rawDesc = "" + "\x12SyncActionThrottle\x12\x19\n" + "\bdelay_ms\x18\x01 \x01(\x03R\adelayMs\x1a8\n" + "\x19SyncEstablishSharedSecret\x12\x1b\n" + - "\tecdsa_pub\x18\x02 \x01(\tR\becdsaPub\"\xb4\x01\n" + + "\tecdsa_pub\x18\x02 \x01(\tR\becdsaPub\x1aO\n" + + "\x15SyncActionAcquireLock\x12\x19\n" + + "\block_key\x18\x01 \x01(\tR\alockKey\x12\x1b\n" + + "\tholder_id\x18\x02 \x01(\tR\bholderId\x1aV\n" + + "\x1dSyncActionAcquireLockResponse\x12\x1a\n" + + "\bacquired\x18\x01 \x01(\bR\bacquired\x12\x19\n" + + "\block_key\x18\x02 \x01(\tR\alockKey\x1aO\n" + + "\x15SyncActionReleaseLock\x12\x19\n" + + "\block_key\x18\x01 \x01(\tR\alockKey\x12\x1b\n" + + "\tholder_id\x18\x02 \x01(\tR\bholderId\x1aO\n" + + "\x15SyncActionRefreshLock\x12\x19\n" + + "\block_key\x18\x01 \x01(\tR\alockKey\x12\x1b\n" + + "\tholder_id\x18\x02 \x01(\tR\bholderId\"\xb4\x01\n" + "\x13RepoConnectionState\x12\x1c\n" + "\x18CONNECTION_STATE_UNKNOWN\x10\x00\x12\x1c\n" + "\x18CONNECTION_STATE_PENDING\x10\x01\x12\x1e\n" + @@ -2016,7 +2305,7 @@ func file_v1sync_syncservice_proto_rawDescGZIP() []byte { } var file_v1sync_syncservice_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_v1sync_syncservice_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_v1sync_syncservice_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_v1sync_syncservice_proto_goTypes = []any{ (ConnectionState)(0), // 0: v1sync.ConnectionState (SyncStreamItem_RepoConnectionState)(0), // 1: v1sync.SyncStreamItem.RepoConnectionState @@ -2048,29 +2337,33 @@ var file_v1sync_syncservice_proto_goTypes = []any{ (*SyncStreamItem_SyncActionReceiveLogData)(nil), // 27: v1sync.SyncStreamItem.SyncActionReceiveLogData (*SyncStreamItem_SyncActionThrottle)(nil), // 28: v1sync.SyncStreamItem.SyncActionThrottle (*SyncStreamItem_SyncEstablishSharedSecret)(nil), // 29: v1sync.SyncStreamItem.SyncEstablishSharedSecret - (*v1.SignedMessage)(nil), // 30: v1.SignedMessage - (*v1.Plan)(nil), // 31: v1.Plan - (*v1.Repo)(nil), // 32: v1.Repo - (*v1.PublicKey)(nil), // 33: v1.PublicKey - (*v1.OperationEvent)(nil), // 34: v1.OperationEvent + (*SyncStreamItem_SyncActionAcquireLock)(nil), // 30: v1sync.SyncStreamItem.SyncActionAcquireLock + (*SyncStreamItem_SyncActionAcquireLockResponse)(nil), // 31: v1sync.SyncStreamItem.SyncActionAcquireLockResponse + (*SyncStreamItem_SyncActionReleaseLock)(nil), // 32: v1sync.SyncStreamItem.SyncActionReleaseLock + (*SyncStreamItem_SyncActionRefreshLock)(nil), // 33: v1sync.SyncStreamItem.SyncActionRefreshLock + (*v1.SignedMessage)(nil), // 34: v1.SignedMessage + (*v1.Plan)(nil), // 35: v1.Plan + (*v1.Repo)(nil), // 36: v1.Repo + (*v1.PublicKey)(nil), // 37: v1.PublicKey + (*v1.OperationEvent)(nil), // 38: v1.OperationEvent } var file_v1sync_syncservice_proto_depIdxs = []int32{ 0, // 0: v1sync.PeerState.state:type_name -> v1sync.ConnectionState 9, // 1: v1sync.PeerState.known_plans:type_name -> v1sync.PlanMetadata 8, // 2: v1sync.PeerState.known_repos:type_name -> v1sync.RepoMetadata 13, // 3: v1sync.PeerState.remote_config:type_name -> v1sync.RemoteConfig - 30, // 4: v1sync.AuthenticateRequest.instance_id:type_name -> v1.SignedMessage + 34, // 4: v1sync.AuthenticateRequest.instance_id:type_name -> v1.SignedMessage 9, // 5: v1sync.SetAvailableResourcesRequest.repos:type_name -> v1sync.PlanMetadata 8, // 6: v1sync.SetAvailableResourcesRequest.plans:type_name -> v1sync.RepoMetadata - 31, // 7: v1sync.SetConfigRequest.plans:type_name -> v1.Plan - 32, // 8: v1sync.SetConfigRequest.repos:type_name -> v1.Repo - 32, // 9: v1sync.SetRemoteClientConfigRequest.repos:type_name -> v1.Repo - 31, // 10: v1sync.SetRemoteClientConfigRequest.plans:type_name -> v1.Plan - 32, // 11: v1sync.RemoteConfig.repos:type_name -> v1.Repo - 31, // 12: v1sync.RemoteConfig.plans:type_name -> v1.Plan - 33, // 13: v1sync.AuthorizationToken.public_key:type_name -> v1.PublicKey - 30, // 14: v1sync.AuthorizationToken.instance_id:type_name -> v1.SignedMessage - 30, // 15: v1sync.SyncStreamItem.signed_message:type_name -> v1.SignedMessage + 35, // 7: v1sync.SetConfigRequest.plans:type_name -> v1.Plan + 36, // 8: v1sync.SetConfigRequest.repos:type_name -> v1.Repo + 36, // 9: v1sync.SetRemoteClientConfigRequest.repos:type_name -> v1.Repo + 35, // 10: v1sync.SetRemoteClientConfigRequest.plans:type_name -> v1.Plan + 36, // 11: v1sync.RemoteConfig.repos:type_name -> v1.Repo + 35, // 12: v1sync.RemoteConfig.plans:type_name -> v1.Plan + 37, // 13: v1sync.AuthorizationToken.public_key:type_name -> v1.PublicKey + 34, // 14: v1sync.AuthorizationToken.instance_id:type_name -> v1.SignedMessage + 34, // 15: v1sync.SyncStreamItem.signed_message:type_name -> v1.SignedMessage 16, // 16: v1sync.SyncStreamItem.handshake:type_name -> v1sync.SyncStreamItem.SyncActionHandshake 17, // 17: v1sync.SyncStreamItem.heartbeat:type_name -> v1sync.SyncStreamItem.SyncActionHeartbeat 23, // 18: v1sync.SyncStreamItem.operation_manifest:type_name -> v1sync.SyncStreamItem.SyncActionOperationManifest @@ -2082,26 +2375,30 @@ var file_v1sync_syncservice_proto_depIdxs = []int32{ 21, // 24: v1sync.SyncStreamItem.receive_resources:type_name -> v1sync.SyncStreamItem.SyncActionReceiveResources 26, // 25: v1sync.SyncStreamItem.request_log:type_name -> v1sync.SyncStreamItem.SyncActionRequestLog 27, // 26: v1sync.SyncStreamItem.receive_log_data:type_name -> v1sync.SyncStreamItem.SyncActionReceiveLogData - 28, // 27: v1sync.SyncStreamItem.throttle:type_name -> v1sync.SyncStreamItem.SyncActionThrottle - 33, // 28: v1sync.SyncStreamItem.SyncActionHandshake.public_key:type_name -> v1.PublicKey - 30, // 29: v1sync.SyncStreamItem.SyncActionHandshake.instance_id:type_name -> v1.SignedMessage - 13, // 30: v1sync.SyncStreamItem.SyncActionReceiveConfig.config:type_name -> v1sync.RemoteConfig - 32, // 31: v1sync.SyncStreamItem.SyncActionSetConfig.repos:type_name -> v1.Repo - 31, // 32: v1sync.SyncStreamItem.SyncActionSetConfig.plans:type_name -> v1.Plan - 8, // 33: v1sync.SyncStreamItem.SyncActionReceiveResources.repos:type_name -> v1sync.RepoMetadata - 9, // 34: v1sync.SyncStreamItem.SyncActionReceiveResources.plans:type_name -> v1sync.PlanMetadata - 34, // 35: v1sync.SyncStreamItem.SyncActionReceiveOperations.event:type_name -> v1.OperationEvent - 15, // 36: v1sync.BackrestSyncService.Sync:input_type -> v1sync.SyncStreamItem - 2, // 37: v1sync.BackrestSyncStateService.GetPeerSyncStatesStream:input_type -> v1sync.SyncStateStreamRequest - 11, // 38: v1sync.BackrestSyncStateService.SetRemoteClientConfig:input_type -> v1sync.SetRemoteClientConfigRequest - 15, // 39: v1sync.BackrestSyncService.Sync:output_type -> v1sync.SyncStreamItem - 3, // 40: v1sync.BackrestSyncStateService.GetPeerSyncStatesStream:output_type -> v1sync.PeerState - 12, // 41: v1sync.BackrestSyncStateService.SetRemoteClientConfig:output_type -> v1sync.SetRemoteClientConfigResponse - 39, // [39:42] is the sub-list for method output_type - 36, // [36:39] is the sub-list for method input_type - 36, // [36:36] is the sub-list for extension type_name - 36, // [36:36] is the sub-list for extension extendee - 0, // [0:36] is the sub-list for field type_name + 30, // 27: v1sync.SyncStreamItem.acquire_lock:type_name -> v1sync.SyncStreamItem.SyncActionAcquireLock + 31, // 28: v1sync.SyncStreamItem.acquire_lock_response:type_name -> v1sync.SyncStreamItem.SyncActionAcquireLockResponse + 32, // 29: v1sync.SyncStreamItem.release_lock:type_name -> v1sync.SyncStreamItem.SyncActionReleaseLock + 33, // 30: v1sync.SyncStreamItem.refresh_lock:type_name -> v1sync.SyncStreamItem.SyncActionRefreshLock + 28, // 31: v1sync.SyncStreamItem.throttle:type_name -> v1sync.SyncStreamItem.SyncActionThrottle + 37, // 32: v1sync.SyncStreamItem.SyncActionHandshake.public_key:type_name -> v1.PublicKey + 34, // 33: v1sync.SyncStreamItem.SyncActionHandshake.instance_id:type_name -> v1.SignedMessage + 13, // 34: v1sync.SyncStreamItem.SyncActionReceiveConfig.config:type_name -> v1sync.RemoteConfig + 36, // 35: v1sync.SyncStreamItem.SyncActionSetConfig.repos:type_name -> v1.Repo + 35, // 36: v1sync.SyncStreamItem.SyncActionSetConfig.plans:type_name -> v1.Plan + 8, // 37: v1sync.SyncStreamItem.SyncActionReceiveResources.repos:type_name -> v1sync.RepoMetadata + 9, // 38: v1sync.SyncStreamItem.SyncActionReceiveResources.plans:type_name -> v1sync.PlanMetadata + 38, // 39: v1sync.SyncStreamItem.SyncActionReceiveOperations.event:type_name -> v1.OperationEvent + 15, // 40: v1sync.BackrestSyncService.Sync:input_type -> v1sync.SyncStreamItem + 2, // 41: v1sync.BackrestSyncStateService.GetPeerSyncStatesStream:input_type -> v1sync.SyncStateStreamRequest + 11, // 42: v1sync.BackrestSyncStateService.SetRemoteClientConfig:input_type -> v1sync.SetRemoteClientConfigRequest + 15, // 43: v1sync.BackrestSyncService.Sync:output_type -> v1sync.SyncStreamItem + 3, // 44: v1sync.BackrestSyncStateService.GetPeerSyncStatesStream:output_type -> v1sync.PeerState + 12, // 45: v1sync.BackrestSyncStateService.SetRemoteClientConfig:output_type -> v1sync.SetRemoteClientConfigResponse + 43, // [43:46] is the sub-list for method output_type + 40, // [40:43] is the sub-list for method input_type + 40, // [40:40] is the sub-list for extension type_name + 40, // [40:40] is the sub-list for extension extendee + 0, // [0:40] is the sub-list for field type_name } func init() { file_v1sync_syncservice_proto_init() } @@ -2122,6 +2419,10 @@ func file_v1sync_syncservice_proto_init() { (*SyncStreamItem_ReceiveResources)(nil), (*SyncStreamItem_RequestLog)(nil), (*SyncStreamItem_ReceiveLogData)(nil), + (*SyncStreamItem_AcquireLock)(nil), + (*SyncStreamItem_AcquireLockResponse)(nil), + (*SyncStreamItem_ReleaseLock)(nil), + (*SyncStreamItem_RefreshLock)(nil), (*SyncStreamItem_Throttle)(nil), } type x struct{} @@ -2130,7 +2431,7 @@ func file_v1sync_syncservice_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1sync_syncservice_proto_rawDesc), len(file_v1sync_syncservice_proto_rawDesc)), NumEnums: 2, - NumMessages: 28, + NumMessages: 32, NumExtensions: 0, NumServices: 2, }, diff --git a/internal/api/syncapi/authmiddleware.go b/internal/api/syncapi/authmiddleware.go index 2070297d..ce24f4c1 100644 --- a/internal/api/syncapi/authmiddleware.go +++ b/internal/api/syncapi/authmiddleware.go @@ -52,7 +52,7 @@ func newAuthHandler(config *config.ConfigManager, next http.Handler) http.Handle } rw.Header().Set(authTokenHeader, authHeaderValue) - peer, err := decodeAndVerifyAuthHeader(r, config.Instance, config.GetMultihost().GetAuthorizedClients()) + peer, err := decodeAndVerifyAuthHeader(r.Header, config.GetMultihost().GetAuthorizedClients()) if err != nil { http.Error(rw, fmt.Sprintf("unauthorized: %v", err), http.StatusUnauthorized) return @@ -116,7 +116,8 @@ func (c *authHeaderClient) Do(req *http.Request) (*http.Response, error) { if resp.StatusCode != http.StatusOK { return resp, fmt.Errorf("HTTP request failed with status %d: %s", resp.StatusCode, resp.Status) } - peer, err := decodeAndVerifyAuthHeader(req, cfg.Instance, cfg.GetMultihost().GetAuthorizedClients()) + // Verify the server's response header against our known hosts. + peer, err := decodeAndVerifyAuthHeader(resp.Header, cfg.GetMultihost().GetKnownHosts()) if err != nil { return resp, fmt.Errorf("verify auth header: %w", err) } @@ -138,8 +139,8 @@ func newHTTPClientWithConfig(cfg *config.ConfigManager, delegate connect.HTTPCli }, nil } -func decodeAndVerifyAuthHeader(r *http.Request, localInstanceID string, peers []*v1.Multihost_Peer) (*v1.Multihost_Peer, error) { - authHeader := r.Header.Get(authTokenHeader) +func decodeAndVerifyAuthHeader(header http.Header, peers []*v1.Multihost_Peer) (*v1.Multihost_Peer, error) { + authHeader := header.Get(authTokenHeader) if len(authHeader) == 0 { return nil, errors.New("missing authorization header") } @@ -195,7 +196,7 @@ func createSignedMessage(payload []byte, identity *cryptoutil.PrivateKey) (*v1.S timestampMillis := time.Now().UnixMilli() payloadWithTimestamp := make([]byte, 0, len(payload)+8) - binary.BigEndian.AppendUint64(payloadWithTimestamp, uint64(timestampMillis)) + payloadWithTimestamp = binary.BigEndian.AppendUint64(payloadWithTimestamp, uint64(timestampMillis)) payloadWithTimestamp = append(payloadWithTimestamp, payload...) signature, err := identity.Sign(payloadWithTimestamp) @@ -230,7 +231,7 @@ func verifySignedMessage(msg *v1.SignedMessage, publicKey *cryptoutil.PublicKey) } payloadWithTimestamp := make([]byte, 0, len(msg.GetPayload())+8) - binary.BigEndian.AppendUint64(payloadWithTimestamp, uint64(msg.GetTimestampMillis())) + payloadWithTimestamp = binary.BigEndian.AppendUint64(payloadWithTimestamp, uint64(msg.GetTimestampMillis())) payloadWithTimestamp = append(payloadWithTimestamp, msg.GetPayload()...) if err := publicKey.Verify(payloadWithTimestamp, msg.GetSignature()); err != nil { diff --git a/internal/api/syncapi/cmdstreamutil.go b/internal/api/syncapi/cmdstreamutil.go index c4f52340..3db793cb 100644 --- a/internal/api/syncapi/cmdstreamutil.go +++ b/internal/api/syncapi/cmdstreamutil.go @@ -27,7 +27,7 @@ type bidiSyncCommandStream struct { func newBidiSyncCommandStream() *bidiSyncCommandStream { return &bidiSyncCommandStream{ - sendChan: make(chan *v1sync.SyncStreamItem, 64), // Buffered channel to allow sending items without blocking + sendChan: make(chan *v1sync.SyncStreamItem, 256), // Buffered channel to allow sending items without blocking recvChan: make(chan *v1sync.SyncStreamItem, 1), terminateWithErrChan: make(chan error, 1), } @@ -74,15 +74,19 @@ func (s *bidiSyncCommandStream) ConnectStream(ctx context.Context, stream syncCo ctx, cancel := context.WithCancel(ctx) defer cancel() go func() { - for ctx.Err() == nil { - if val, err := stream.Receive(); err != nil { + defer close(s.recvChan) + for { + val, err := stream.Receive() + if err != nil { s.SendErrorAndTerminate(NewSyncErrorDisconnected(fmt.Errorf("receiving item: %w", err))) - break - } else { - s.recvChan <- val + return + } + select { + case s.recvChan <- val: + case <-ctx.Done(): + return } } - close(s.recvChan) }() for { diff --git a/internal/api/syncapi/lockmanager.go b/internal/api/syncapi/lockmanager.go new file mode 100644 index 00000000..a12d92bd --- /dev/null +++ b/internal/api/syncapi/lockmanager.go @@ -0,0 +1,69 @@ +package syncapi + +import ( + "sync" + "time" +) + +const lockExpiry = 30 * time.Second + +type lockEntry struct { + holderID string + expiresAt time.Time +} + +// LockManager provides an in-memory best-effort lock store for coordinating +// repo access between sync peers. Locks expire after 30 seconds if not refreshed. +type LockManager struct { + mu sync.Mutex + locks map[string]*lockEntry +} + +func NewLockManager() *LockManager { + return &LockManager{ + locks: make(map[string]*lockEntry), + } +} + +// Acquire attempts to acquire a lock for the given key. Returns true if the +// lock was acquired (key was free, expired, or already held by the same holder). +func (lm *LockManager) Acquire(key, holderID string) bool { + lm.mu.Lock() + defer lm.mu.Unlock() + + entry, exists := lm.locks[key] + if !exists || time.Now().After(entry.expiresAt) || entry.holderID == holderID { + lm.locks[key] = &lockEntry{ + holderID: holderID, + expiresAt: time.Now().Add(lockExpiry), + } + return true + } + return false +} + +// Release releases a lock if the holder matches. +func (lm *LockManager) Release(key, holderID string) bool { + lm.mu.Lock() + defer lm.mu.Unlock() + + entry, exists := lm.locks[key] + if exists && entry.holderID == holderID { + delete(lm.locks, key) + return true + } + return false +} + +// Refresh extends the expiry of a lock if the holder matches. +func (lm *LockManager) Refresh(key, holderID string) bool { + lm.mu.Lock() + defer lm.mu.Unlock() + + entry, exists := lm.locks[key] + if exists && entry.holderID == holderID { + entry.expiresAt = time.Now().Add(lockExpiry) + return true + } + return false +} diff --git a/internal/api/syncapi/syncclient.go b/internal/api/syncapi/syncclient.go index d026b9b2..b383f897 100644 --- a/internal/api/syncapi/syncclient.go +++ b/internal/api/syncapi/syncclient.go @@ -35,6 +35,11 @@ type SyncClient struct { l *zap.Logger reconnectAttempts int + + // Lock protocol support + streamMu sync.Mutex + activeStream *bidiSyncCommandStream // set while connected, nil otherwise + lockResponseCh chan *v1sync.SyncStreamItem_SyncActionAcquireLockResponse } func newInsecureClient() *http.Client { @@ -73,6 +78,7 @@ func NewSyncClient( client: client, oplog: oplog, l: zap.L().Named(fmt.Sprintf("syncclient for %q", peer.GetInstanceId())), + lockResponseCh: make(chan *v1sync.SyncStreamItem_SyncActionAcquireLockResponse, 1), } c.mgr.peerStateManager.SetPeerState(peer.Keyid, newPeerState(peer.InstanceId, peer.Keyid)) return c, nil @@ -92,10 +98,15 @@ func (c *SyncClient) RunSync(ctx context.Context) { c.syncConfigSnapshot, c.oplog, c.peer, + c, ) cmdStream := newBidiSyncCommandStream() + c.streamMu.Lock() + c.activeStream = cmdStream + c.streamMu.Unlock() + c.l.Sugar().Infof("connecting to peer %q (%s) at %s", c.peer.InstanceId, c.peer.Keyid, c.peer.GetInstanceUrl()) var wg sync.WaitGroup @@ -115,33 +126,45 @@ func (c *SyncClient) RunSync(ctx context.Context) { cmdStream.SendErrorAndTerminate(err) }() - if err := cmdStream.ConnectStream(ctx, c.client.Sync(ctx)); err != nil { - c.l.Sugar().Infof("lost stream connection to peer %q (%s): %v", c.peer.InstanceId, c.peer.Keyid, err) + connectErr := cmdStream.ConnectStream(ctx, c.client.Sync(ctx)) + c.streamMu.Lock() + c.activeStream = nil + c.streamMu.Unlock() + if connectErr != nil { + c.l.Sugar().Infof("lost stream connection to peer %q (%s): %v", c.peer.InstanceId, c.peer.Keyid, connectErr) var syncErr *SyncError state := c.mgr.peerStateManager.GetPeerState(c.peer.Keyid).Clone() if state == nil { state = newPeerState(c.peer.InstanceId, c.peer.Keyid) } state.LastHeartbeat = time.Now() - if errors.As(err, &syncErr) { + if errors.As(connectErr, &syncErr) { state.ConnectionState = syncErr.State state.ConnectionStateMessage = syncErr.Message.Error() } else { state.ConnectionState = v1sync.ConnectionState_CONNECTION_STATE_ERROR_INTERNAL - state.ConnectionStateMessage = err.Error() + state.ConnectionStateMessage = connectErr.Error() } c.mgr.peerStateManager.SetPeerState(c.peer.Keyid, state) - } else { - c.reconnectAttempts = 0 } wg.Wait() + // Reset reconnect backoff if the session lasted long enough to be considered a real success, + // rather than a handshake that failed immediately. Using reconnectDelay as the threshold means + // any session that ran at least one full retry window counts as stable. + if time.Since(lastConnect) >= c.reconnectDelay { + c.reconnectAttempts = 0 + } + delay := c.reconnectDelay - time.Since(lastConnect) if c.reconnectAttempts > 0 { backoff := time.Duration(1< clockJumpGrace { + zap.S().Warnf("syncmanager detected clock jump of %v, forcing reconnection", delta) + config, err := m.configMgr.Get() + if err != nil { + zap.S().Errorf("syncmanager failed to get config after clock jump: %v", err) + continue + } + startSync(config) + } } } } @@ -201,13 +280,18 @@ func (m *SyncManager) runSyncWithPeerInternal(ctx context.Context, config *v1.Co return fmt.Errorf("creating sync client: %w", err) } m.mu.Lock() - m.syncClients[knownHostPeer.InstanceId] = newClient + m.syncClients[knownHostPeer.Keyid] = newClient m.mu.Unlock() go func() { newClient.RunSync(ctx) m.mu.Lock() - delete(m.syncClients, knownHostPeer.InstanceId) + // Only remove the entry if it still points at us. On reconfiguration the new + // client may have already inserted itself under the same key; deleting blindly + // would wipe the replacement. + if m.syncClients[knownHostPeer.Keyid] == newClient { + delete(m.syncClients, knownHostPeer.Keyid) + } m.mu.Unlock() }() diff --git a/internal/api/syncapi/syncserver.go b/internal/api/syncapi/syncserver.go index cb036824..2c44e109 100644 --- a/internal/api/syncapi/syncserver.go +++ b/internal/api/syncapi/syncserver.go @@ -29,7 +29,10 @@ type BackrestSyncHandler struct { var _ v1syncconnect.BackrestSyncServiceHandler = &BackrestSyncHandler{} func NewBackrestSyncHandler(mgr *SyncManager) *BackrestSyncHandler { - mapper, _ := newRemoteOpIDMapper(mgr.oplog, 4096) // error can be ignored, it just checks for valid size + mapper, err := newRemoteOpIDMapper(mgr.oplog, 4096) + if err != nil { + panic(fmt.Errorf("syncapi: constructing remote op ID mapper: %w", err)) + } return &BackrestSyncHandler{ mgr: mgr, mapper: mapper, @@ -230,6 +233,32 @@ func (h *syncSessionHandlerServer) OnConnectionDisconnected() { } } +func (h *syncSessionHandlerServer) HandleAcquireLock(ctx context.Context, stream *bidiSyncCommandStream, item *v1sync.SyncStreamItem_SyncActionAcquireLock) error { + acquired := h.mgr.lockManager.Acquire(item.GetLockKey(), item.GetHolderId()) + h.l.Debug("lock acquire request", zap.String("key", item.GetLockKey()), zap.String("holder", item.GetHolderId()), zap.Bool("acquired", acquired)) + stream.Send(&v1sync.SyncStreamItem{ + Action: &v1sync.SyncStreamItem_AcquireLockResponse{ + AcquireLockResponse: &v1sync.SyncStreamItem_SyncActionAcquireLockResponse{ + Acquired: acquired, + LockKey: item.GetLockKey(), + }, + }, + }) + return nil +} + +func (h *syncSessionHandlerServer) HandleReleaseLock(ctx context.Context, stream *bidiSyncCommandStream, item *v1sync.SyncStreamItem_SyncActionReleaseLock) error { + released := h.mgr.lockManager.Release(item.GetLockKey(), item.GetHolderId()) + h.l.Debug("lock release request", zap.String("key", item.GetLockKey()), zap.String("holder", item.GetHolderId()), zap.Bool("released", released)) + return nil +} + +func (h *syncSessionHandlerServer) HandleRefreshLock(ctx context.Context, stream *bidiSyncCommandStream, item *v1sync.SyncStreamItem_SyncActionRefreshLock) error { + refreshed := h.mgr.lockManager.Refresh(item.GetLockKey(), item.GetHolderId()) + h.l.Debug("lock refresh request", zap.String("key", item.GetLockKey()), zap.String("holder", item.GetHolderId()), zap.Bool("refreshed", refreshed)) + return nil +} + func (h *syncSessionHandlerServer) HandleHeartbeat(ctx context.Context, stream *bidiSyncCommandStream, item *v1sync.SyncStreamItem_SyncActionHeartbeat) error { peerState := h.mgr.peerStateManager.GetPeerState(h.peer.Keyid).Clone() if peerState == nil { @@ -385,6 +414,30 @@ func (h *syncSessionHandlerServer) sendSharedReposToClient(stream *bidiSyncComma if repo.GetShared() { repoCopy := proto.Clone(repo).(*v1.Repo) repoCopy.OriginInstanceId = config.Instance + // Inject lock hooks so the client acquires a lock on this server + // before running operations on the shared repo. + repoCopy.Hooks = append(repoCopy.Hooks, + &v1.Hook{ + Conditions: []v1.Hook_Condition{v1.Hook_CONDITION_ANY_START}, + OnError: v1.Hook_ON_ERROR_IGNORE, + Action: &v1.Hook_ActionSyncLock{ + ActionSyncLock: &v1.Hook_SyncLock{ + TargetInstanceId: config.Instance, + LockKey: repo.GetId(), + }, + }, + }, + &v1.Hook{ + Conditions: []v1.Hook_Condition{v1.Hook_CONDITION_ANY_END}, + OnError: v1.Hook_ON_ERROR_IGNORE, + Action: &v1.Hook_ActionSyncLock{ + ActionSyncLock: &v1.Hook_SyncLock{ + TargetInstanceId: config.Instance, + LockKey: repo.GetId(), + }, + }, + }, + ) sharedRepos = append(sharedRepos, repoCopy) } } diff --git a/internal/hook/types/synclock.go b/internal/hook/types/synclock.go new file mode 100644 index 00000000..4b704c20 --- /dev/null +++ b/internal/hook/types/synclock.go @@ -0,0 +1,160 @@ +package types + +import ( + "context" + "fmt" + "reflect" + "sync" + "time" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "go.uber.org/zap" +) + +// SyncLockClient is an interface for acquiring and releasing locks on remote peers. +// This is implemented by syncapi.SyncClient. +type SyncLockClient interface { + AcquireLock(ctx context.Context, lockKey string) (bool, error) + ReleaseLock(lockKey string) + RefreshLock(lockKey string) +} + +// SyncLockClientProvider provides SyncLockClients by target instance ID. +type SyncLockClientProvider interface { + GetSyncLockClient(instanceID string) SyncLockClient +} + +var ( + syncLockProviderMu sync.Mutex + syncLockProvider SyncLockClientProvider +) + +// SetSyncLockClientProvider registers the provider used by the synclock hook handler. +func SetSyncLockClientProvider(provider SyncLockClientProvider) { + syncLockProviderMu.Lock() + defer syncLockProviderMu.Unlock() + syncLockProvider = provider +} + +func getSyncLockClientProvider() SyncLockClientProvider { + syncLockProviderMu.Lock() + defer syncLockProviderMu.Unlock() + return syncLockProvider +} + +const ( + lockRefreshInterval = 10 * time.Second + lockMaxRetryDelay = 60 * time.Second + lockInitialRetryDelay = 1 * time.Second + lockMaxRetries = 7 // 1s, 2s, 4s, 8s, 16s, 32s, 60s +) + +type syncLockHandler struct{} + +func (syncLockHandler) Name() string { + return "synclock" +} + +func (syncLockHandler) ActionType() reflect.Type { + return reflect.TypeOf(&v1.Hook_ActionSyncLock{}) +} + +func (h syncLockHandler) Execute(ctx context.Context, hook *v1.Hook, vars interface{}, runner tasks.TaskRunner, event v1.Hook_Condition) error { + lockConfig := hook.GetActionSyncLock() + if lockConfig == nil { + return fmt.Errorf("synclock hook missing action config") + } + + provider := getSyncLockClientProvider() + if provider == nil { + zap.L().Warn("synclock: no provider registered, skipping lock operation") + return nil + } + + client := provider.GetSyncLockClient(lockConfig.GetTargetInstanceId()) + if client == nil { + zap.L().Warn("synclock: no client for target instance, skipping lock operation", + zap.String("targetInstance", lockConfig.GetTargetInstanceId())) + return nil + } + + switch event { + case v1.Hook_CONDITION_ANY_START: + return h.acquireLock(ctx, client, lockConfig) + case v1.Hook_CONDITION_ANY_END: + h.releaseLock(client, lockConfig) + return nil + default: + return nil + } +} + +func (h syncLockHandler) acquireLock(ctx context.Context, client SyncLockClient, config *v1.Hook_SyncLock) error { + lockKey := config.GetLockKey() + delay := lockInitialRetryDelay + + for attempt := 0; attempt <= lockMaxRetries; attempt++ { + acquired, err := client.AcquireLock(ctx, lockKey) + if err != nil { + zap.L().Warn("synclock: error acquiring lock, proceeding without lock (best-effort)", + zap.String("lockKey", lockKey), zap.Error(err)) + return nil // best-effort: proceed without lock + } + if acquired { + zap.L().Info("synclock: acquired lock", zap.String("lockKey", lockKey)) + // Start refresh goroutine + go h.refreshLoop(ctx, client, lockKey) + return nil + } + + if attempt == lockMaxRetries { + break + } + + zap.L().Info("synclock: lock not acquired, retrying", + zap.String("lockKey", lockKey), + zap.Int("attempt", attempt+1), + zap.Duration("delay", delay)) + + select { + case <-time.After(delay): + case <-ctx.Done(): + zap.L().Warn("synclock: context cancelled while waiting for lock, proceeding without lock", + zap.String("lockKey", lockKey)) + return nil + } + + delay *= 2 + if delay > lockMaxRetryDelay { + delay = lockMaxRetryDelay + } + } + + zap.L().Warn("synclock: could not acquire lock after retries, proceeding without lock (best-effort)", + zap.String("lockKey", lockKey)) + return nil // best-effort: default open +} + +func (h syncLockHandler) releaseLock(client SyncLockClient, config *v1.Hook_SyncLock) { + client.ReleaseLock(config.GetLockKey()) + zap.L().Info("synclock: released lock", zap.String("lockKey", config.GetLockKey())) +} + +func (h syncLockHandler) refreshLoop(ctx context.Context, client SyncLockClient, lockKey string) { + ticker := time.NewTicker(lockRefreshInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + client.RefreshLock(lockKey) + case <-ctx.Done(): + return + } + } +} + +func init() { + DefaultRegistry().RegisterHandler(&syncLockHandler{}) +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 811dabd4..c1764909 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -281,12 +281,6 @@ func (o *Orchestrator) ScheduleDefaultTasks(config *v1.Config) error { return fmt.Errorf("repo %q not found for plan %q", plan.Repo, plan.Id) } - // Skip plans targeting repos managed by a remote instance; the remote - // instance's orchestrator owns scheduling for those. - if repo.GetOriginInstanceId() != "" { - continue - } - t := tasks.NewScheduledBackupTask(repo, plan) if err := o.ScheduleTask(t, tasks.TaskPriorityDefault); err != nil { return fmt.Errorf("schedule backup task for plan %q: %w", plan.Id, err) @@ -658,8 +652,23 @@ func (o *Orchestrator) cleanupTaskContext(ctx context.Context, op *v1.Operation, func (o *Orchestrator) executeTask(ctx context.Context, st tasks.ScheduledTask) error { start := time.Now() runner := newTaskRunnerImpl(o, st.Task, st.Op) + + // Execute ANY_START hooks (best-effort, errors are logged but don't block) + if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{v1.Hook_CONDITION_ANY_START}, tasks.HookVars{}); err != nil { + runner.Logger(ctx).Warn("ANY_START hook error (best-effort, continuing)", zap.Error(err)) + } + err := st.Task.Run(ctx, st, runner) + // Execute ANY_END hooks (best-effort) + endVars := tasks.HookVars{} + if err != nil { + endVars.Error = err.Error() + } + if endErr := runner.ExecuteHooks(ctx, []v1.Hook_Condition{v1.Hook_CONDITION_ANY_END}, endVars); endErr != nil { + runner.Logger(ctx).Warn("ANY_END hook error (best-effort, continuing)", zap.Error(endErr)) + } + // Record metrics based on task result if err != nil { runner.Logger(ctx).Error("task failed", zap.Error(err), zap.Duration("duration", time.Since(start))) diff --git a/internal/orchestrator/tasks/hookvars.go b/internal/orchestrator/tasks/hookvars.go index 781809cc..e55ec05f 100644 --- a/internal/orchestrator/tasks/hookvars.go +++ b/internal/orchestrator/tasks/hookvars.go @@ -61,6 +61,10 @@ func (v HookVars) EventName(cond v1.Hook_Condition) string { return "forget error" case v1.Hook_CONDITION_FORGET_SUCCESS: return "forget success" + case v1.Hook_CONDITION_ANY_START: + return "any start" + case v1.Hook_CONDITION_ANY_END: + return "any end" default: return "unknown" } diff --git a/internal/protoutil/conditions.go b/internal/protoutil/conditions.go index 8c6aa6e3..171aa9b1 100644 --- a/internal/protoutil/conditions.go +++ b/internal/protoutil/conditions.go @@ -9,6 +9,7 @@ var startConditionsMap = map[v1.Hook_Condition]bool{ v1.Hook_CONDITION_PRUNE_START: true, v1.Hook_CONDITION_SNAPSHOT_START: true, v1.Hook_CONDITION_FORGET_START: true, + v1.Hook_CONDITION_ANY_START: true, } var errorConditionsMap = map[v1.Hook_Condition]bool{ diff --git a/proto/v1/config.proto b/proto/v1/config.proto index b68ad937..69c391fc 100644 --- a/proto/v1/config.proto +++ b/proto/v1/config.proto @@ -202,6 +202,10 @@ message Hook { CONDITION_FORGET_START = 300; // forget started. CONDITION_FORGET_ERROR = 301; // forget failed. CONDITION_FORGET_SUCCESS = 302; // forget succeeded. + + // any operation conditions + CONDITION_ANY_START = 400; // before any operation starts. + CONDITION_ANY_END = 401; // after any operation ends (success or fail). } enum OnError { @@ -225,6 +229,7 @@ message Hook { Shoutrrr action_shoutrrr = 105 [json_name="actionShoutrrr"]; Healthchecks action_healthchecks = 106 [json_name="actionHealthchecks"]; Telegram action_telegram = 107 [json_name="actionTelegram"]; + SyncLock action_sync_lock = 109 [json_name="actionSyncLock"]; } message Command { @@ -275,6 +280,11 @@ message Hook { string chat_id = 2 [json_name="chatId"]; string template = 3 [json_name="template"]; // template for the message text. } + + message SyncLock { + string target_instance_id = 1 [json_name="targetInstanceId"]; // the instance ID of the peer to acquire the lock on. + string lock_key = 2 [json_name="lockKey"]; // the lock key, typically the repo ID. + } } message Auth { diff --git a/proto/v1sync/syncservice.proto b/proto/v1sync/syncservice.proto index 00c73437..6886c801 100644 --- a/proto/v1sync/syncservice.proto +++ b/proto/v1sync/syncservice.proto @@ -134,6 +134,11 @@ message SyncStreamItem { SyncActionRequestLog request_log = 30; SyncActionReceiveLogData receive_log_data = 31; + SyncActionAcquireLock acquire_lock = 40; + SyncActionAcquireLockResponse acquire_lock_response = 41; + SyncActionReleaseLock release_lock = 42; + SyncActionRefreshLock refresh_lock = 43; + SyncActionThrottle throttle = 1000; } @@ -217,4 +222,25 @@ message SyncStreamItem { // See https://pkg.go.dev/crypto/ecdh#PrivateKey.ECDH . string ecdsa_pub = 2 [json_name="ecdsaPub"]; // base64 encoded public key } + + // Lock protocol messages for coordinating repo access between peers. + message SyncActionAcquireLock { + string lock_key = 1; // the lock key, typically a repo ID. + string holder_id = 2; // the instance ID of the lock requester. + } + + message SyncActionAcquireLockResponse { + bool acquired = 1; // whether the lock was acquired. + string lock_key = 2; // the lock key that was requested. + } + + message SyncActionReleaseLock { + string lock_key = 1; // the lock key to release. + string holder_id = 2; // the instance ID of the lock holder. + } + + message SyncActionRefreshLock { + string lock_key = 1; // the lock key to refresh. + string holder_id = 2; // the instance ID of the lock holder. + } } diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index 0fc68f0f..bc9177bb 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -13,7 +13,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file v1/config.proto. */ export const file_v1_config: GenFile = /*@__PURE__*/ - fileDesc("Cg92MS9jb25maWcucHJvdG8SAnYxIqwBCgZDb25maWcSDQoFbW9kbm8YASABKAUSDwoHdmVyc2lvbhgGIAEoBRIQCghpbnN0YW5jZRgCIAEoCRIXCgVyZXBvcxgDIAMoCzIILnYxLlJlcG8SFwoFcGxhbnMYBCADKAsyCC52MS5QbGFuEhYKBGF1dGgYBSABKAsyCC52MS5BdXRoEiYKCW11bHRpaG9zdBgHIAEoCzINLnYxLk11bHRpaG9zdFIEc3luYyL6BQoJTXVsdGlob3N0EiAKCGlkZW50aXR5GAEgASgLMg4udjEuUHJpdmF0ZUtleRInCgtrbm93bl9ob3N0cxgCIAMoCzISLnYxLk11bHRpaG9zdC5QZWVyEi4KEmF1dGhvcml6ZWRfY2xpZW50cxgDIAMoCzISLnYxLk11bHRpaG9zdC5QZWVyEjIKDnBhaXJpbmdfdG9rZW5zGAQgAygLMhoudjEuTXVsdGlob3N0LlBhaXJpbmdUb2tlbhqcAQoEUGVlchITCgtpbnN0YW5jZV9pZBgBIAEoCRIUCgVrZXlpZBgCIAEoCVIFa2V5SWQSLQoLcGVybWlzc2lvbnMYBSADKAsyGC52MS5NdWx0aWhvc3QuUGVybWlzc2lvbhIUCgxpbnN0YW5jZV91cmwYBCABKAkSHgoWaW5pdGlhbF9wYWlyaW5nX3NlY3JldBgGIAEoCUoECAMQBBquAQoMUGFpcmluZ1Rva2VuEg4KBnNlY3JldBgBIAEoCRINCgVsYWJlbBgCIAEoCRIXCg9jcmVhdGVkX2F0X3VuaXgYAyABKAMSFwoPZXhwaXJlc19hdF91bml4GAQgASgDEhAKCG1heF91c2VzGAUgASgFEgwKBHVzZXMYBiABKAUSLQoLcGVybWlzc2lvbnMYByADKAsyGC52MS5NdWx0aWhvc3QuUGVybWlzc2lvbhrtAQoKUGVybWlzc2lvbhIrCgR0eXBlGAEgASgOMh0udjEuTXVsdGlob3N0LlBlcm1pc3Npb24uVHlwZRIOCgZzY29wZXMYAiADKAkioQEKBFR5cGUSFgoSUEVSTUlTU0lPTl9VTktOT1dOEAASHgoaUEVSTUlTU0lPTl9SRUFEX09QRVJBVElPTlMQARIaChZQRVJNSVNTSU9OX1JFQURfQ09ORklHEAISIAocUEVSTUlTU0lPTl9SRUFEX1dSSVRFX0NPTkZJRxADEiMKH1BFUk1JU1NJT05fUkVDRUlWRV9TSEFSRURfUkVQT1MQBCLHAgoEUmVwbxIKCgJpZBgBIAEoCRILCgN1cmkYAiABKAkSDAoEZ3VpZBgLIAEoCRIQCghwYXNzd29yZBgDIAEoCRILCgNlbnYYBCADKAkSDQoFZmxhZ3MYBSADKAkSJQoMcHJ1bmVfcG9saWN5GAYgASgLMg8udjEuUHJ1bmVQb2xpY3kSJQoMY2hlY2tfcG9saWN5GAkgASgLMg8udjEuQ2hlY2tQb2xpY3kSFwoFaG9va3MYByADKAsyCC52MS5Ib29rEhMKC2F1dG9fdW5sb2NrGAggASgIEhcKD2F1dG9faW5pdGlhbGl6ZRgMIAEoCBIpCg5jb21tYW5kX3ByZWZpeBgKIAEoCzIRLnYxLkNvbW1hbmRQcmVmaXgSDgoGc2hhcmVkGA0gASgIEhoKEm9yaWdpbl9pbnN0YW5jZV9pZBgOIAEoCSKGAgoEUGxhbhIKCgJpZBgBIAEoCRIMCgRyZXBvGAIgASgJEg0KBXBhdGhzGAQgAygJEhAKCGV4Y2x1ZGVzGAUgAygJEhEKCWlleGNsdWRlcxgJIAMoCRIeCghzY2hlZHVsZRgMIAEoCzIMLnYxLlNjaGVkdWxlEiYKCXJldGVudGlvbhgHIAEoCzITLnYxLlJldGVudGlvblBvbGljeRIXCgVob29rcxgIIAMoCzIILnYxLkhvb2sSIgoMYmFja3VwX2ZsYWdzGAogAygJUgxiYWNrdXBfZmxhZ3MSGQoRc2tpcF9pZl91bmNoYW5nZWQYDSABKAhKBAgDEARKBAgGEAdKBAgLEAwiigIKDUNvbW1hbmRQcmVmaXgSLgoHaW9fbmljZRgBIAEoDjIdLnYxLkNvbW1hbmRQcmVmaXguSU9OaWNlTGV2ZWwSMAoIY3B1X25pY2UYAiABKA4yHi52MS5Db21tYW5kUHJlZml4LkNQVU5pY2VMZXZlbCJbCgtJT05pY2VMZXZlbBIOCgpJT19ERUZBVUxUEAASFgoSSU9fQkVTVF9FRkZPUlRfTE9XEAESFwoTSU9fQkVTVF9FRkZPUlRfSElHSBACEgsKB0lPX0lETEUQAyI6CgxDUFVOaWNlTGV2ZWwSDwoLQ1BVX0RFRkFVTFQQABIMCghDUFVfSElHSBABEgsKB0NQVV9MT1cQAiKXAgoPUmV0ZW50aW9uUG9saWN5EhwKEnBvbGljeV9rZWVwX2xhc3RfbhgKIAEoBUgAEkYKFHBvbGljeV90aW1lX2J1Y2tldGVkGAsgASgLMiYudjEuUmV0ZW50aW9uUG9saWN5LlRpbWVCdWNrZXRlZENvdW50c0gAEhkKD3BvbGljeV9rZWVwX2FsbBgMIAEoCEgAGnkKElRpbWVCdWNrZXRlZENvdW50cxIOCgZob3VybHkYASABKAUSDQoFZGFpbHkYAiABKAUSDgoGd2Vla2x5GAMgASgFEg8KB21vbnRobHkYBCABKAUSDgoGeWVhcmx5GAUgASgFEhMKC2tlZXBfbGFzdF9uGAYgASgFQggKBnBvbGljeSJjCgtQcnVuZVBvbGljeRIeCghzY2hlZHVsZRgCIAEoCzIMLnYxLlNjaGVkdWxlEhgKEG1heF91bnVzZWRfYnl0ZXMYAyABKAMSGgoSbWF4X3VudXNlZF9wZXJjZW50GAQgASgBInMKC0NoZWNrUG9saWN5Eh4KCHNjaGVkdWxlGAEgASgLMgwudjEuU2NoZWR1bGUSGAoOc3RydWN0dXJlX29ubHkYZCABKAhIABIiChhyZWFkX2RhdGFfc3Vic2V0X3BlcmNlbnQYZSABKAFIAEIGCgRtb2RlIusBCghTY2hlZHVsZRISCghkaXNhYmxlZBgBIAEoCEgAEg4KBGNyb24YAiABKAlIABIaChBtYXhGcmVxdWVuY3lEYXlzGAMgASgFSAASGwoRbWF4RnJlcXVlbmN5SG91cnMYBCABKAVIABIhCgVjbG9jaxgFIAEoDjISLnYxLlNjaGVkdWxlLkNsb2NrIlMKBUNsb2NrEhEKDUNMT0NLX0RFRkFVTFQQABIPCgtDTE9DS19MT0NBTBABEg0KCUNMT0NLX1VUQxACEhcKE0NMT0NLX0xBU1RfUlVOX1RJTUUQA0IKCghzY2hlZHVsZSKADQoESG9vaxImCgpjb25kaXRpb25zGAEgAygOMhIudjEuSG9vay5Db25kaXRpb24SIgoIb25fZXJyb3IYAiABKA4yEC52MS5Ib29rLk9uRXJyb3ISKgoOYWN0aW9uX2NvbW1hbmQYZCABKAsyEC52MS5Ib29rLkNvbW1hbmRIABIqCg5hY3Rpb25fd2ViaG9vaxhlIAEoCzIQLnYxLkhvb2suV2ViaG9va0gAEioKDmFjdGlvbl9kaXNjb3JkGGYgASgLMhAudjEuSG9vay5EaXNjb3JkSAASKAoNYWN0aW9uX2dvdGlmeRhnIAEoCzIPLnYxLkhvb2suR290aWZ5SAASJgoMYWN0aW9uX3NsYWNrGGggASgLMg4udjEuSG9vay5TbGFja0gAEiwKD2FjdGlvbl9zaG91dHJychhpIAEoCzIRLnYxLkhvb2suU2hvdXRycnJIABI0ChNhY3Rpb25faGVhbHRoY2hlY2tzGGogASgLMhUudjEuSG9vay5IZWFsdGhjaGVja3NIABIsCg9hY3Rpb25fdGVsZWdyYW0YayABKAsyES52MS5Ib29rLlRlbGVncmFtSAAaGgoHQ29tbWFuZBIPCgdjb21tYW5kGAEgASgJGoMBCgdXZWJob29rEhMKC3dlYmhvb2tfdXJsGAEgASgJEicKBm1ldGhvZBgCIAEoDjIXLnYxLkhvb2suV2ViaG9vay5NZXRob2QSEAoIdGVtcGxhdGUYZCABKAkiKAoGTWV0aG9kEgsKB1VOS05PV04QABIHCgNHRVQQARIICgRQT1NUEAIaMAoHRGlzY29yZBITCgt3ZWJob29rX3VybBgBIAEoCRIQCgh0ZW1wbGF0ZRgCIAEoCRplCgZHb3RpZnkSEAoIYmFzZV91cmwYASABKAkSDQoFdG9rZW4YAyABKAkSEAoIdGVtcGxhdGUYZCABKAkSFgoOdGl0bGVfdGVtcGxhdGUYZSABKAkSEAoIcHJpb3JpdHkYZiABKAUaLgoFU2xhY2sSEwoLd2ViaG9va191cmwYASABKAkSEAoIdGVtcGxhdGUYAiABKAkaMgoIU2hvdXRycnISFAoMc2hvdXRycnJfdXJsGAEgASgJEhAKCHRlbXBsYXRlGAIgASgJGjUKDEhlYWx0aGNoZWNrcxITCgt3ZWJob29rX3VybBgBIAEoCRIQCgh0ZW1wbGF0ZRgCIAEoCRpACghUZWxlZ3JhbRIRCglib3RfdG9rZW4YASABKAkSDwoHY2hhdF9pZBgCIAEoCRIQCgh0ZW1wbGF0ZRgDIAEoCSL1AwoJQ29uZGl0aW9uEhUKEUNPTkRJVElPTl9VTktOT1dOEAASFwoTQ09ORElUSU9OX0FOWV9FUlJPUhABEhwKGENPTkRJVElPTl9TTkFQU0hPVF9TVEFSVBACEhoKFkNPTkRJVElPTl9TTkFQU0hPVF9FTkQQAxIcChhDT05ESVRJT05fU05BUFNIT1RfRVJST1IQBBIeChpDT05ESVRJT05fU05BUFNIT1RfV0FSTklORxAFEh4KGkNPTkRJVElPTl9TTkFQU0hPVF9TVUNDRVNTEAYSHgoaQ09ORElUSU9OX1NOQVBTSE9UX1NLSVBQRUQQBxIZChVDT05ESVRJT05fUFJVTkVfU1RBUlQQZBIZChVDT05ESVRJT05fUFJVTkVfRVJST1IQZRIbChdDT05ESVRJT05fUFJVTkVfU1VDQ0VTUxBmEhoKFUNPTkRJVElPTl9DSEVDS19TVEFSVBDIARIaChVDT05ESVRJT05fQ0hFQ0tfRVJST1IQyQESHAoXQ09ORElUSU9OX0NIRUNLX1NVQ0NFU1MQygESGwoWQ09ORElUSU9OX0ZPUkdFVF9TVEFSVBCsAhIbChZDT05ESVRJT05fRk9SR0VUX0VSUk9SEK0CEh0KGENPTkRJVElPTl9GT1JHRVRfU1VDQ0VTUxCuAiKpAQoHT25FcnJvchITCg9PTl9FUlJPUl9JR05PUkUQABITCg9PTl9FUlJPUl9DQU5DRUwQARISCg5PTl9FUlJPUl9GQVRBTBACEhoKFk9OX0VSUk9SX1JFVFJZXzFNSU5VVEUQZBIcChhPTl9FUlJPUl9SRVRSWV8xME1JTlVURVMQZRImCiJPTl9FUlJPUl9SRVRSWV9FWFBPTkVOVElBTF9CQUNLT0ZGEGdCCAoGYWN0aW9uIjEKBEF1dGgSEAoIZGlzYWJsZWQYASABKAgSFwoFdXNlcnMYAiADKAsyCC52MS5Vc2VyIjsKBFVzZXISDAoEbmFtZRgBIAEoCRIZCg9wYXNzd29yZF9iY3J5cHQYAiABKAlIAEIKCghwYXNzd29yZEIsWipnaXRodWIuY29tL2dhcmV0aGdlb3JnZS9iYWNrcmVzdC9nZW4vZ28vdjFiBnByb3RvMw", [file_google_protobuf_empty, file_v1_crypto]); + fileDesc("Cg92MS9jb25maWcucHJvdG8SAnYxIqwBCgZDb25maWcSDQoFbW9kbm8YASABKAUSDwoHdmVyc2lvbhgGIAEoBRIQCghpbnN0YW5jZRgCIAEoCRIXCgVyZXBvcxgDIAMoCzIILnYxLlJlcG8SFwoFcGxhbnMYBCADKAsyCC52MS5QbGFuEhYKBGF1dGgYBSABKAsyCC52MS5BdXRoEiYKCW11bHRpaG9zdBgHIAEoCzINLnYxLk11bHRpaG9zdFIEc3luYyL6BQoJTXVsdGlob3N0EiAKCGlkZW50aXR5GAEgASgLMg4udjEuUHJpdmF0ZUtleRInCgtrbm93bl9ob3N0cxgCIAMoCzISLnYxLk11bHRpaG9zdC5QZWVyEi4KEmF1dGhvcml6ZWRfY2xpZW50cxgDIAMoCzISLnYxLk11bHRpaG9zdC5QZWVyEjIKDnBhaXJpbmdfdG9rZW5zGAQgAygLMhoudjEuTXVsdGlob3N0LlBhaXJpbmdUb2tlbhqcAQoEUGVlchITCgtpbnN0YW5jZV9pZBgBIAEoCRIUCgVrZXlpZBgCIAEoCVIFa2V5SWQSLQoLcGVybWlzc2lvbnMYBSADKAsyGC52MS5NdWx0aWhvc3QuUGVybWlzc2lvbhIUCgxpbnN0YW5jZV91cmwYBCABKAkSHgoWaW5pdGlhbF9wYWlyaW5nX3NlY3JldBgGIAEoCUoECAMQBBquAQoMUGFpcmluZ1Rva2VuEg4KBnNlY3JldBgBIAEoCRINCgVsYWJlbBgCIAEoCRIXCg9jcmVhdGVkX2F0X3VuaXgYAyABKAMSFwoPZXhwaXJlc19hdF91bml4GAQgASgDEhAKCG1heF91c2VzGAUgASgFEgwKBHVzZXMYBiABKAUSLQoLcGVybWlzc2lvbnMYByADKAsyGC52MS5NdWx0aWhvc3QuUGVybWlzc2lvbhrtAQoKUGVybWlzc2lvbhIrCgR0eXBlGAEgASgOMh0udjEuTXVsdGlob3N0LlBlcm1pc3Npb24uVHlwZRIOCgZzY29wZXMYAiADKAkioQEKBFR5cGUSFgoSUEVSTUlTU0lPTl9VTktOT1dOEAASHgoaUEVSTUlTU0lPTl9SRUFEX09QRVJBVElPTlMQARIaChZQRVJNSVNTSU9OX1JFQURfQ09ORklHEAISIAocUEVSTUlTU0lPTl9SRUFEX1dSSVRFX0NPTkZJRxADEiMKH1BFUk1JU1NJT05fUkVDRUlWRV9TSEFSRURfUkVQT1MQBCLHAgoEUmVwbxIKCgJpZBgBIAEoCRILCgN1cmkYAiABKAkSDAoEZ3VpZBgLIAEoCRIQCghwYXNzd29yZBgDIAEoCRILCgNlbnYYBCADKAkSDQoFZmxhZ3MYBSADKAkSJQoMcHJ1bmVfcG9saWN5GAYgASgLMg8udjEuUHJ1bmVQb2xpY3kSJQoMY2hlY2tfcG9saWN5GAkgASgLMg8udjEuQ2hlY2tQb2xpY3kSFwoFaG9va3MYByADKAsyCC52MS5Ib29rEhMKC2F1dG9fdW5sb2NrGAggASgIEhcKD2F1dG9faW5pdGlhbGl6ZRgMIAEoCBIpCg5jb21tYW5kX3ByZWZpeBgKIAEoCzIRLnYxLkNvbW1hbmRQcmVmaXgSDgoGc2hhcmVkGA0gASgIEhoKEm9yaWdpbl9pbnN0YW5jZV9pZBgOIAEoCSKGAgoEUGxhbhIKCgJpZBgBIAEoCRIMCgRyZXBvGAIgASgJEg0KBXBhdGhzGAQgAygJEhAKCGV4Y2x1ZGVzGAUgAygJEhEKCWlleGNsdWRlcxgJIAMoCRIeCghzY2hlZHVsZRgMIAEoCzIMLnYxLlNjaGVkdWxlEiYKCXJldGVudGlvbhgHIAEoCzITLnYxLlJldGVudGlvblBvbGljeRIXCgVob29rcxgIIAMoCzIILnYxLkhvb2sSIgoMYmFja3VwX2ZsYWdzGAogAygJUgxiYWNrdXBfZmxhZ3MSGQoRc2tpcF9pZl91bmNoYW5nZWQYDSABKAhKBAgDEARKBAgGEAdKBAgLEAwiigIKDUNvbW1hbmRQcmVmaXgSLgoHaW9fbmljZRgBIAEoDjIdLnYxLkNvbW1hbmRQcmVmaXguSU9OaWNlTGV2ZWwSMAoIY3B1X25pY2UYAiABKA4yHi52MS5Db21tYW5kUHJlZml4LkNQVU5pY2VMZXZlbCJbCgtJT05pY2VMZXZlbBIOCgpJT19ERUZBVUxUEAASFgoSSU9fQkVTVF9FRkZPUlRfTE9XEAESFwoTSU9fQkVTVF9FRkZPUlRfSElHSBACEgsKB0lPX0lETEUQAyI6CgxDUFVOaWNlTGV2ZWwSDwoLQ1BVX0RFRkFVTFQQABIMCghDUFVfSElHSBABEgsKB0NQVV9MT1cQAiKXAgoPUmV0ZW50aW9uUG9saWN5EhwKEnBvbGljeV9rZWVwX2xhc3RfbhgKIAEoBUgAEkYKFHBvbGljeV90aW1lX2J1Y2tldGVkGAsgASgLMiYudjEuUmV0ZW50aW9uUG9saWN5LlRpbWVCdWNrZXRlZENvdW50c0gAEhkKD3BvbGljeV9rZWVwX2FsbBgMIAEoCEgAGnkKElRpbWVCdWNrZXRlZENvdW50cxIOCgZob3VybHkYASABKAUSDQoFZGFpbHkYAiABKAUSDgoGd2Vla2x5GAMgASgFEg8KB21vbnRobHkYBCABKAUSDgoGeWVhcmx5GAUgASgFEhMKC2tlZXBfbGFzdF9uGAYgASgFQggKBnBvbGljeSJjCgtQcnVuZVBvbGljeRIeCghzY2hlZHVsZRgCIAEoCzIMLnYxLlNjaGVkdWxlEhgKEG1heF91bnVzZWRfYnl0ZXMYAyABKAMSGgoSbWF4X3VudXNlZF9wZXJjZW50GAQgASgBInMKC0NoZWNrUG9saWN5Eh4KCHNjaGVkdWxlGAEgASgLMgwudjEuU2NoZWR1bGUSGAoOc3RydWN0dXJlX29ubHkYZCABKAhIABIiChhyZWFkX2RhdGFfc3Vic2V0X3BlcmNlbnQYZSABKAFIAEIGCgRtb2RlIusBCghTY2hlZHVsZRISCghkaXNhYmxlZBgBIAEoCEgAEg4KBGNyb24YAiABKAlIABIaChBtYXhGcmVxdWVuY3lEYXlzGAMgASgFSAASGwoRbWF4RnJlcXVlbmN5SG91cnMYBCABKAVIABIhCgVjbG9jaxgFIAEoDjISLnYxLlNjaGVkdWxlLkNsb2NrIlMKBUNsb2NrEhEKDUNMT0NLX0RFRkFVTFQQABIPCgtDTE9DS19MT0NBTBABEg0KCUNMT0NLX1VUQxACEhcKE0NMT0NLX0xBU1RfUlVOX1RJTUUQA0IKCghzY2hlZHVsZSKbDgoESG9vaxImCgpjb25kaXRpb25zGAEgAygOMhIudjEuSG9vay5Db25kaXRpb24SIgoIb25fZXJyb3IYAiABKA4yEC52MS5Ib29rLk9uRXJyb3ISKgoOYWN0aW9uX2NvbW1hbmQYZCABKAsyEC52MS5Ib29rLkNvbW1hbmRIABIqCg5hY3Rpb25fd2ViaG9vaxhlIAEoCzIQLnYxLkhvb2suV2ViaG9va0gAEioKDmFjdGlvbl9kaXNjb3JkGGYgASgLMhAudjEuSG9vay5EaXNjb3JkSAASKAoNYWN0aW9uX2dvdGlmeRhnIAEoCzIPLnYxLkhvb2suR290aWZ5SAASJgoMYWN0aW9uX3NsYWNrGGggASgLMg4udjEuSG9vay5TbGFja0gAEiwKD2FjdGlvbl9zaG91dHJychhpIAEoCzIRLnYxLkhvb2suU2hvdXRycnJIABI0ChNhY3Rpb25faGVhbHRoY2hlY2tzGGogASgLMhUudjEuSG9vay5IZWFsdGhjaGVja3NIABIsCg9hY3Rpb25fdGVsZWdyYW0YayABKAsyES52MS5Ib29rLlRlbGVncmFtSAASLQoQYWN0aW9uX3N5bmNfbG9jaxhtIAEoCzIRLnYxLkhvb2suU3luY0xvY2tIABoaCgdDb21tYW5kEg8KB2NvbW1hbmQYASABKAkagwEKB1dlYmhvb2sSEwoLd2ViaG9va191cmwYASABKAkSJwoGbWV0aG9kGAIgASgOMhcudjEuSG9vay5XZWJob29rLk1ldGhvZBIQCgh0ZW1wbGF0ZRhkIAEoCSIoCgZNZXRob2QSCwoHVU5LTk9XThAAEgcKA0dFVBABEggKBFBPU1QQAhowCgdEaXNjb3JkEhMKC3dlYmhvb2tfdXJsGAEgASgJEhAKCHRlbXBsYXRlGAIgASgJGmUKBkdvdGlmeRIQCghiYXNlX3VybBgBIAEoCRINCgV0b2tlbhgDIAEoCRIQCgh0ZW1wbGF0ZRhkIAEoCRIWCg50aXRsZV90ZW1wbGF0ZRhlIAEoCRIQCghwcmlvcml0eRhmIAEoBRouCgVTbGFjaxITCgt3ZWJob29rX3VybBgBIAEoCRIQCgh0ZW1wbGF0ZRgCIAEoCRoyCghTaG91dHJychIUCgxzaG91dHJycl91cmwYASABKAkSEAoIdGVtcGxhdGUYAiABKAkaNQoMSGVhbHRoY2hlY2tzEhMKC3dlYmhvb2tfdXJsGAEgASgJEhAKCHRlbXBsYXRlGAIgASgJGkAKCFRlbGVncmFtEhEKCWJvdF90b2tlbhgBIAEoCRIPCgdjaGF0X2lkGAIgASgJEhAKCHRlbXBsYXRlGAMgASgJGjgKCFN5bmNMb2NrEhoKEnRhcmdldF9pbnN0YW5jZV9pZBgBIAEoCRIQCghsb2NrX2tleRgCIAEoCSKnBAoJQ29uZGl0aW9uEhUKEUNPTkRJVElPTl9VTktOT1dOEAASFwoTQ09ORElUSU9OX0FOWV9FUlJPUhABEhwKGENPTkRJVElPTl9TTkFQU0hPVF9TVEFSVBACEhoKFkNPTkRJVElPTl9TTkFQU0hPVF9FTkQQAxIcChhDT05ESVRJT05fU05BUFNIT1RfRVJST1IQBBIeChpDT05ESVRJT05fU05BUFNIT1RfV0FSTklORxAFEh4KGkNPTkRJVElPTl9TTkFQU0hPVF9TVUNDRVNTEAYSHgoaQ09ORElUSU9OX1NOQVBTSE9UX1NLSVBQRUQQBxIZChVDT05ESVRJT05fUFJVTkVfU1RBUlQQZBIZChVDT05ESVRJT05fUFJVTkVfRVJST1IQZRIbChdDT05ESVRJT05fUFJVTkVfU1VDQ0VTUxBmEhoKFUNPTkRJVElPTl9DSEVDS19TVEFSVBDIARIaChVDT05ESVRJT05fQ0hFQ0tfRVJST1IQyQESHAoXQ09ORElUSU9OX0NIRUNLX1NVQ0NFU1MQygESGwoWQ09ORElUSU9OX0ZPUkdFVF9TVEFSVBCsAhIbChZDT05ESVRJT05fRk9SR0VUX0VSUk9SEK0CEh0KGENPTkRJVElPTl9GT1JHRVRfU1VDQ0VTUxCuAhIYChNDT05ESVRJT05fQU5ZX1NUQVJUEJADEhYKEUNPTkRJVElPTl9BTllfRU5EEJEDIqkBCgdPbkVycm9yEhMKD09OX0VSUk9SX0lHTk9SRRAAEhMKD09OX0VSUk9SX0NBTkNFTBABEhIKDk9OX0VSUk9SX0ZBVEFMEAISGgoWT05fRVJST1JfUkVUUllfMU1JTlVURRBkEhwKGE9OX0VSUk9SX1JFVFJZXzEwTUlOVVRFUxBlEiYKIk9OX0VSUk9SX1JFVFJZX0VYUE9ORU5USUFMX0JBQ0tPRkYQZ0IICgZhY3Rpb24iMQoEQXV0aBIQCghkaXNhYmxlZBgBIAEoCBIXCgV1c2VycxgCIAMoCzIILnYxLlVzZXIiOwoEVXNlchIMCgRuYW1lGAEgASgJEhkKD3Bhc3N3b3JkX2JjcnlwdBgCIAEoCUgAQgoKCHBhc3N3b3JkQixaKmdpdGh1Yi5jb20vZ2FyZXRoZ2VvcmdlL2JhY2tyZXN0L2dlbi9nby92MWIGcHJvdG8z", [file_google_protobuf_empty, file_v1_crypto]); /** * Config is the top level config object for restic UI. @@ -879,6 +879,12 @@ export type Hook = Message<"v1.Hook"> & { */ value: Hook_Telegram; case: "actionTelegram"; + } | { + /** + * @generated from field: v1.Hook.SyncLock action_sync_lock = 109; + */ + value: Hook_SyncLock; + case: "actionSyncLock"; } | { case: undefined; value?: undefined }; }; @@ -1123,6 +1129,32 @@ export type Hook_Telegram = Message<"v1.Hook.Telegram"> & { export const Hook_TelegramSchema: GenMessage = /*@__PURE__*/ messageDesc(file_v1_config, 9, 7); +/** + * @generated from message v1.Hook.SyncLock + */ +export type Hook_SyncLock = Message<"v1.Hook.SyncLock"> & { + /** + * the instance ID of the peer to acquire the lock on. + * + * @generated from field: string target_instance_id = 1; + */ + targetInstanceId: string; + + /** + * the lock key, typically the repo ID. + * + * @generated from field: string lock_key = 2; + */ + lockKey: string; +}; + +/** + * Describes the message v1.Hook.SyncLock. + * Use `create(Hook_SyncLockSchema)` to create a new message. + */ +export const Hook_SyncLockSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1_config, 9, 8); + /** * @generated from enum v1.Hook.Condition */ @@ -1249,6 +1281,22 @@ export enum Hook_Condition { * @generated from enum value: CONDITION_FORGET_SUCCESS = 302; */ FORGET_SUCCESS = 302, + + /** + * any operation conditions + * + * before any operation starts. + * + * @generated from enum value: CONDITION_ANY_START = 400; + */ + ANY_START = 400, + + /** + * after any operation ends (success or fail). + * + * @generated from enum value: CONDITION_ANY_END = 401; + */ + ANY_END = 401, } /** diff --git a/webui/gen/ts/v1sync/syncservice_pb.ts b/webui/gen/ts/v1sync/syncservice_pb.ts index 96c37b6f..2d59309e 100644 --- a/webui/gen/ts/v1sync/syncservice_pb.ts +++ b/webui/gen/ts/v1sync/syncservice_pb.ts @@ -21,7 +21,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file v1sync/syncservice.proto. */ export const file_v1sync_syncservice: GenFile = /*@__PURE__*/ - fileDesc("Chh2MXN5bmMvc3luY3NlcnZpY2UucHJvdG8SBnYxc3luYyIrChZTeW5jU3RhdGVTdHJlYW1SZXF1ZXN0EhEKCXN1YnNjcmliZRgBIAEoCCKbAgoJUGVlclN0YXRlEhgKEHBlZXJfaW5zdGFuY2VfaWQYASABKAkSEgoKcGVlcl9rZXlpZBgCIAEoCRImCgVzdGF0ZRgDIAEoDjIXLnYxc3luYy5Db25uZWN0aW9uU3RhdGUSFgoOc3RhdHVzX21lc3NhZ2UYBCABKAkSKQoLa25vd25fcGxhbnMYBSADKAsyFC52MXN5bmMuUGxhbk1ldGFkYXRhEikKC2tub3duX3JlcG9zGAYgAygLMhQudjFzeW5jLlJlcG9NZXRhZGF0YRIrCg1yZW1vdGVfY29uZmlnGAcgASgLMhQudjFzeW5jLlJlbW90ZUNvbmZpZxIdChVsYXN0X2hlYXJ0YmVhdF9taWxsaXMYCCABKAMiPQoTQXV0aGVudGljYXRlUmVxdWVzdBImCgtpbnN0YW5jZV9pZBgBIAEoCzIRLnYxLlNpZ25lZE1lc3NhZ2UiPgocR2V0T3BlcmF0aW9uTWV0YWRhdGFSZXNwb25zZRIOCgZvcF9pZHMYASADKAMSDgoGbW9kbm9zGAIgAygDIl0KDExvZ0RhdGFFbnRyeRIOCgZsb2dfaWQYASABKAkSEgoKb3duZXJfb3BpZBgCIAEoAxIaChJleHBpcmF0aW9uX3RzX3VuaXgYAyABKAMSDQoFY2h1bmsYBCABKAwiaAocU2V0QXZhaWxhYmxlUmVzb3VyY2VzUmVxdWVzdBIjCgVyZXBvcxgBIAMoCzIULnYxc3luYy5QbGFuTWV0YWRhdGESIwoFcGxhbnMYAiADKAsyFC52MXN5bmMuUmVwb01ldGFkYXRhIigKDFJlcG9NZXRhZGF0YRIKCgJpZBgBIAEoCRIMCgRndWlkGAIgASgJIhoKDFBsYW5NZXRhZGF0YRIKCgJpZBgBIAEoCSJ2ChBTZXRDb25maWdSZXF1ZXN0EhcKBXBsYW5zGAEgAygLMggudjEuUGxhbhIXCgVyZXBvcxgCIAMoCzIILnYxLlJlcG8SFwoPcmVwb3NfdG9fZGVsZXRlGAMgAygJEhcKD3BsYW5zX3RvX2RlbGV0ZRgEIAMoCSKWAQocU2V0UmVtb3RlQ2xpZW50Q29uZmlnUmVxdWVzdBISCgpwZWVyX2tleWlkGAEgASgJEhcKBXJlcG9zGAIgAygLMggudjEuUmVwbxIXCgVwbGFucxgDIAMoCzIILnYxLlBsYW4SFwoPcmVwb3NfdG9fZGVsZXRlGAQgAygJEhcKD3BsYW5zX3RvX2RlbGV0ZRgFIAMoCSIfCh1TZXRSZW1vdGVDbGllbnRDb25maWdSZXNwb25zZSJgCgxSZW1vdGVDb25maWcSDQoFbW9kbm8YASABKAUSDwoHdmVyc2lvbhgCIAEoBRIXCgVyZXBvcxgDIAMoCzIILnYxLlJlcG8SFwoFcGxhbnMYBCADKAsyCC52MS5QbGFuIl8KEkF1dGhvcml6YXRpb25Ub2tlbhIhCgpwdWJsaWNfa2V5GAEgASgLMg0udjEuUHVibGljS2V5EiYKC2luc3RhbmNlX2lkGAIgASgLMhEudjEuU2lnbmVkTWVzc2FnZSLIEAoOU3luY1N0cmVhbUl0ZW0SKwoOc2lnbmVkX21lc3NhZ2UYASABKAsyES52MS5TaWduZWRNZXNzYWdlSAASPwoJaGFuZHNoYWtlGAMgASgLMioudjFzeW5jLlN5bmNTdHJlYW1JdGVtLlN5bmNBY3Rpb25IYW5kc2hha2VIABI/CgloZWFydGJlYXQYBCABKAsyKi52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvbkhlYXJ0YmVhdEgAElAKEm9wZXJhdGlvbl9tYW5pZmVzdBgUIAEoCzIyLnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uT3BlcmF0aW9uTWFuaWZlc3RIABJQChJyZWNlaXZlX29wZXJhdGlvbnMYFSABKAsyMi52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblJlY2VpdmVPcGVyYXRpb25zSAASVwoWcmVxdWVzdF9vcGVyYXRpb25fZGF0YRgWIAEoCzI1LnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uUmVxdWVzdE9wZXJhdGlvbkRhdGFIABJICg5yZWNlaXZlX2NvbmZpZxgXIAEoCzIuLnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uUmVjZWl2ZUNvbmZpZ0gAEkAKCnNldF9jb25maWcYGCABKAsyKi52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblNldENvbmZpZ0gAEk4KEXJlcXVlc3RfcmVzb3VyY2VzGBkgASgLMjEudjFzeW5jLlN5bmNTdHJlYW1JdGVtLlN5bmNBY3Rpb25SZXF1ZXN0UmVzb3VyY2VzSAASTgoRcmVjZWl2ZV9yZXNvdXJjZXMYGiABKAsyMS52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblJlY2VpdmVSZXNvdXJjZXNIABJCCgtyZXF1ZXN0X2xvZxgeIAEoCzIrLnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uUmVxdWVzdExvZ0gAEksKEHJlY2VpdmVfbG9nX2RhdGEYHyABKAsyLy52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblJlY2VpdmVMb2dEYXRhSAASPgoIdGhyb3R0bGUY6AcgASgLMikudjFzeW5jLlN5bmNTdHJlYW1JdGVtLlN5bmNBY3Rpb25UaHJvdHRsZUgAGpIBChNTeW5jQWN0aW9uSGFuZHNoYWtlEhgKEHByb3RvY29sX3ZlcnNpb24YASABKAMSIQoKcHVibGljX2tleRgCIAEoCzINLnYxLlB1YmxpY0tleRImCgtpbnN0YW5jZV9pZBgDIAEoCzIRLnYxLlNpZ25lZE1lc3NhZ2USFgoOcGFpcmluZ19zZWNyZXQYBCABKAkaFQoTU3luY0FjdGlvbkhlYXJ0YmVhdBo/ChdTeW5jQWN0aW9uUmVjZWl2ZUNvbmZpZxIkCgZjb25maWcYASABKAsyFC52MXN5bmMuUmVtb3RlQ29uZmlnGnkKE1N5bmNBY3Rpb25TZXRDb25maWcSFwoFcmVwb3MYASADKAsyCC52MS5SZXBvEhcKBXBsYW5zGAIgAygLMggudjEuUGxhbhIXCg9yZXBvc190b19kZWxldGUYAyADKAkSFwoPcGxhbnNfdG9fZGVsZXRlGAQgAygJGhwKGlN5bmNBY3Rpb25SZXF1ZXN0UmVzb3VyY2VzGmYKGlN5bmNBY3Rpb25SZWNlaXZlUmVzb3VyY2VzEiMKBXJlcG9zGAEgAygLMhQudjFzeW5jLlJlcG9NZXRhZGF0YRIjCgVwbGFucxgCIAMoCzIULnYxc3luYy5QbGFuTWV0YWRhdGEaKAoVU3luY0FjdGlvbkNvbm5lY3RSZXBvEg8KB3JlcG9faWQYASABKAkaPQobU3luY0FjdGlvbk9wZXJhdGlvbk1hbmlmZXN0Eg4KBm9wX2lkcxgBIAMoAxIOCgZtb2Rub3MYAiADKAMaMAoeU3luY0FjdGlvblJlcXVlc3RPcGVyYXRpb25EYXRhEg4KBm9wX2lkcxgBIAMoAxpAChtTeW5jQWN0aW9uUmVjZWl2ZU9wZXJhdGlvbnMSIQoFZXZlbnQYASABKAsyEi52MS5PcGVyYXRpb25FdmVudBomChRTeW5jQWN0aW9uUmVxdWVzdExvZxIOCgZsb2dfaWQYASABKAkagAEKGFN5bmNBY3Rpb25SZWNlaXZlTG9nRGF0YRIOCgZsb2dfaWQYASABKAkSEgoKb3duZXJfb3BpZBgCIAEoAxIaChJleHBpcmF0aW9uX3RzX3VuaXgYAyABKAMSDQoFY2h1bmsYBCABKAwSFQoNZXJyb3JfbWVzc2FnZRgFIAEoCRomChJTeW5jQWN0aW9uVGhyb3R0bGUSEAoIZGVsYXlfbXMYASABKAMaLgoZU3luY0VzdGFibGlzaFNoYXJlZFNlY3JldBIRCgllY2RzYV9wdWIYAiABKAkitAEKE1JlcG9Db25uZWN0aW9uU3RhdGUSHAoYQ09OTkVDVElPTl9TVEFURV9VTktOT1dOEAASHAoYQ09OTkVDVElPTl9TVEFURV9QRU5ESU5HEAESHgoaQ09OTkVDVElPTl9TVEFURV9DT05ORUNURUQQAhIhCh1DT05ORUNUSU9OX1NUQVRFX1VOQVVUSE9SSVpFRBADEh4KGkNPTk5FQ1RJT05fU1RBVEVfTk9UX0ZPVU5EEARCCAoGYWN0aW9uKpwCCg9Db25uZWN0aW9uU3RhdGUSHAoYQ09OTkVDVElPTl9TVEFURV9VTktOT1dOEAASHAoYQ09OTkVDVElPTl9TVEFURV9QRU5ESU5HEAESHgoaQ09OTkVDVElPTl9TVEFURV9DT05ORUNURUQQAhIhCh1DT05ORUNUSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBADEh8KG0NPTk5FQ1RJT05fU1RBVEVfUkVUUllfV0FJVBAEEh8KG0NPTk5FQ1RJT05fU1RBVEVfRVJST1JfQVVUSBAKEiMKH0NPTk5FQ1RJT05fU1RBVEVfRVJST1JfUFJPVE9DT0wQCxIjCh9DT05ORUNUSU9OX1NUQVRFX0VSUk9SX0lOVEVSTkFMEAwyUwoTQmFja3Jlc3RTeW5jU2VydmljZRI8CgRTeW5jEhYudjFzeW5jLlN5bmNTdHJlYW1JdGVtGhYudjFzeW5jLlN5bmNTdHJlYW1JdGVtIgAoATABMtQBChhCYWNrcmVzdFN5bmNTdGF0ZVNlcnZpY2USUAoXR2V0UGVlclN5bmNTdGF0ZXNTdHJlYW0SHi52MXN5bmMuU3luY1N0YXRlU3RyZWFtUmVxdWVzdBoRLnYxc3luYy5QZWVyU3RhdGUiADABEmYKFVNldFJlbW90ZUNsaWVudENvbmZpZxIkLnYxc3luYy5TZXRSZW1vdGVDbGllbnRDb25maWdSZXF1ZXN0GiUudjFzeW5jLlNldFJlbW90ZUNsaWVudENvbmZpZ1Jlc3BvbnNlIgBCMFouZ2l0aHViLmNvbS9nYXJldGhnZW9yZ2UvYmFja3Jlc3QvZ2VuL2dvL3Yxc3luY2IGcHJvdG8z", [file_v1_config, file_v1_crypto, file_v1_restic, file_v1_service, file_v1_operations, file_types_value, file_google_protobuf_empty, file_google_api_annotations, file_google_protobuf_any]); + fileDesc("Chh2MXN5bmMvc3luY3NlcnZpY2UucHJvdG8SBnYxc3luYyIrChZTeW5jU3RhdGVTdHJlYW1SZXF1ZXN0EhEKCXN1YnNjcmliZRgBIAEoCCKbAgoJUGVlclN0YXRlEhgKEHBlZXJfaW5zdGFuY2VfaWQYASABKAkSEgoKcGVlcl9rZXlpZBgCIAEoCRImCgVzdGF0ZRgDIAEoDjIXLnYxc3luYy5Db25uZWN0aW9uU3RhdGUSFgoOc3RhdHVzX21lc3NhZ2UYBCABKAkSKQoLa25vd25fcGxhbnMYBSADKAsyFC52MXN5bmMuUGxhbk1ldGFkYXRhEikKC2tub3duX3JlcG9zGAYgAygLMhQudjFzeW5jLlJlcG9NZXRhZGF0YRIrCg1yZW1vdGVfY29uZmlnGAcgASgLMhQudjFzeW5jLlJlbW90ZUNvbmZpZxIdChVsYXN0X2hlYXJ0YmVhdF9taWxsaXMYCCABKAMiPQoTQXV0aGVudGljYXRlUmVxdWVzdBImCgtpbnN0YW5jZV9pZBgBIAEoCzIRLnYxLlNpZ25lZE1lc3NhZ2UiPgocR2V0T3BlcmF0aW9uTWV0YWRhdGFSZXNwb25zZRIOCgZvcF9pZHMYASADKAMSDgoGbW9kbm9zGAIgAygDIl0KDExvZ0RhdGFFbnRyeRIOCgZsb2dfaWQYASABKAkSEgoKb3duZXJfb3BpZBgCIAEoAxIaChJleHBpcmF0aW9uX3RzX3VuaXgYAyABKAMSDQoFY2h1bmsYBCABKAwiaAocU2V0QXZhaWxhYmxlUmVzb3VyY2VzUmVxdWVzdBIjCgVyZXBvcxgBIAMoCzIULnYxc3luYy5QbGFuTWV0YWRhdGESIwoFcGxhbnMYAiADKAsyFC52MXN5bmMuUmVwb01ldGFkYXRhIigKDFJlcG9NZXRhZGF0YRIKCgJpZBgBIAEoCRIMCgRndWlkGAIgASgJIhoKDFBsYW5NZXRhZGF0YRIKCgJpZBgBIAEoCSJ2ChBTZXRDb25maWdSZXF1ZXN0EhcKBXBsYW5zGAEgAygLMggudjEuUGxhbhIXCgVyZXBvcxgCIAMoCzIILnYxLlJlcG8SFwoPcmVwb3NfdG9fZGVsZXRlGAMgAygJEhcKD3BsYW5zX3RvX2RlbGV0ZRgEIAMoCSKWAQocU2V0UmVtb3RlQ2xpZW50Q29uZmlnUmVxdWVzdBISCgpwZWVyX2tleWlkGAEgASgJEhcKBXJlcG9zGAIgAygLMggudjEuUmVwbxIXCgVwbGFucxgDIAMoCzIILnYxLlBsYW4SFwoPcmVwb3NfdG9fZGVsZXRlGAQgAygJEhcKD3BsYW5zX3RvX2RlbGV0ZRgFIAMoCSIfCh1TZXRSZW1vdGVDbGllbnRDb25maWdSZXNwb25zZSJgCgxSZW1vdGVDb25maWcSDQoFbW9kbm8YASABKAUSDwoHdmVyc2lvbhgCIAEoBRIXCgVyZXBvcxgDIAMoCzIILnYxLlJlcG8SFwoFcGxhbnMYBCADKAsyCC52MS5QbGFuIl8KEkF1dGhvcml6YXRpb25Ub2tlbhIhCgpwdWJsaWNfa2V5GAEgASgLMg0udjEuUHVibGljS2V5EiYKC2luc3RhbmNlX2lkGAIgASgLMhEudjEuU2lnbmVkTWVzc2FnZSLwFAoOU3luY1N0cmVhbUl0ZW0SKwoOc2lnbmVkX21lc3NhZ2UYASABKAsyES52MS5TaWduZWRNZXNzYWdlSAASPwoJaGFuZHNoYWtlGAMgASgLMioudjFzeW5jLlN5bmNTdHJlYW1JdGVtLlN5bmNBY3Rpb25IYW5kc2hha2VIABI/CgloZWFydGJlYXQYBCABKAsyKi52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvbkhlYXJ0YmVhdEgAElAKEm9wZXJhdGlvbl9tYW5pZmVzdBgUIAEoCzIyLnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uT3BlcmF0aW9uTWFuaWZlc3RIABJQChJyZWNlaXZlX29wZXJhdGlvbnMYFSABKAsyMi52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblJlY2VpdmVPcGVyYXRpb25zSAASVwoWcmVxdWVzdF9vcGVyYXRpb25fZGF0YRgWIAEoCzI1LnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uUmVxdWVzdE9wZXJhdGlvbkRhdGFIABJICg5yZWNlaXZlX2NvbmZpZxgXIAEoCzIuLnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uUmVjZWl2ZUNvbmZpZ0gAEkAKCnNldF9jb25maWcYGCABKAsyKi52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblNldENvbmZpZ0gAEk4KEXJlcXVlc3RfcmVzb3VyY2VzGBkgASgLMjEudjFzeW5jLlN5bmNTdHJlYW1JdGVtLlN5bmNBY3Rpb25SZXF1ZXN0UmVzb3VyY2VzSAASTgoRcmVjZWl2ZV9yZXNvdXJjZXMYGiABKAsyMS52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblJlY2VpdmVSZXNvdXJjZXNIABJCCgtyZXF1ZXN0X2xvZxgeIAEoCzIrLnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uUmVxdWVzdExvZ0gAEksKEHJlY2VpdmVfbG9nX2RhdGEYHyABKAsyLy52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblJlY2VpdmVMb2dEYXRhSAASRAoMYWNxdWlyZV9sb2NrGCggASgLMiwudjFzeW5jLlN5bmNTdHJlYW1JdGVtLlN5bmNBY3Rpb25BY3F1aXJlTG9ja0gAElUKFWFjcXVpcmVfbG9ja19yZXNwb25zZRgpIAEoCzI0LnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uQWNxdWlyZUxvY2tSZXNwb25zZUgAEkQKDHJlbGVhc2VfbG9jaxgqIAEoCzIsLnYxc3luYy5TeW5jU3RyZWFtSXRlbS5TeW5jQWN0aW9uUmVsZWFzZUxvY2tIABJECgxyZWZyZXNoX2xvY2sYKyABKAsyLC52MXN5bmMuU3luY1N0cmVhbUl0ZW0uU3luY0FjdGlvblJlZnJlc2hMb2NrSAASPgoIdGhyb3R0bGUY6AcgASgLMikudjFzeW5jLlN5bmNTdHJlYW1JdGVtLlN5bmNBY3Rpb25UaHJvdHRsZUgAGpIBChNTeW5jQWN0aW9uSGFuZHNoYWtlEhgKEHByb3RvY29sX3ZlcnNpb24YASABKAMSIQoKcHVibGljX2tleRgCIAEoCzINLnYxLlB1YmxpY0tleRImCgtpbnN0YW5jZV9pZBgDIAEoCzIRLnYxLlNpZ25lZE1lc3NhZ2USFgoOcGFpcmluZ19zZWNyZXQYBCABKAkaFQoTU3luY0FjdGlvbkhlYXJ0YmVhdBo/ChdTeW5jQWN0aW9uUmVjZWl2ZUNvbmZpZxIkCgZjb25maWcYASABKAsyFC52MXN5bmMuUmVtb3RlQ29uZmlnGnkKE1N5bmNBY3Rpb25TZXRDb25maWcSFwoFcmVwb3MYASADKAsyCC52MS5SZXBvEhcKBXBsYW5zGAIgAygLMggudjEuUGxhbhIXCg9yZXBvc190b19kZWxldGUYAyADKAkSFwoPcGxhbnNfdG9fZGVsZXRlGAQgAygJGhwKGlN5bmNBY3Rpb25SZXF1ZXN0UmVzb3VyY2VzGmYKGlN5bmNBY3Rpb25SZWNlaXZlUmVzb3VyY2VzEiMKBXJlcG9zGAEgAygLMhQudjFzeW5jLlJlcG9NZXRhZGF0YRIjCgVwbGFucxgCIAMoCzIULnYxc3luYy5QbGFuTWV0YWRhdGEaKAoVU3luY0FjdGlvbkNvbm5lY3RSZXBvEg8KB3JlcG9faWQYASABKAkaPQobU3luY0FjdGlvbk9wZXJhdGlvbk1hbmlmZXN0Eg4KBm9wX2lkcxgBIAMoAxIOCgZtb2Rub3MYAiADKAMaMAoeU3luY0FjdGlvblJlcXVlc3RPcGVyYXRpb25EYXRhEg4KBm9wX2lkcxgBIAMoAxpAChtTeW5jQWN0aW9uUmVjZWl2ZU9wZXJhdGlvbnMSIQoFZXZlbnQYASABKAsyEi52MS5PcGVyYXRpb25FdmVudBomChRTeW5jQWN0aW9uUmVxdWVzdExvZxIOCgZsb2dfaWQYASABKAkagAEKGFN5bmNBY3Rpb25SZWNlaXZlTG9nRGF0YRIOCgZsb2dfaWQYASABKAkSEgoKb3duZXJfb3BpZBgCIAEoAxIaChJleHBpcmF0aW9uX3RzX3VuaXgYAyABKAMSDQoFY2h1bmsYBCABKAwSFQoNZXJyb3JfbWVzc2FnZRgFIAEoCRomChJTeW5jQWN0aW9uVGhyb3R0bGUSEAoIZGVsYXlfbXMYASABKAMaLgoZU3luY0VzdGFibGlzaFNoYXJlZFNlY3JldBIRCgllY2RzYV9wdWIYAiABKAkaPAoVU3luY0FjdGlvbkFjcXVpcmVMb2NrEhAKCGxvY2tfa2V5GAEgASgJEhEKCWhvbGRlcl9pZBgCIAEoCRpDCh1TeW5jQWN0aW9uQWNxdWlyZUxvY2tSZXNwb25zZRIQCghhY3F1aXJlZBgBIAEoCBIQCghsb2NrX2tleRgCIAEoCRo8ChVTeW5jQWN0aW9uUmVsZWFzZUxvY2sSEAoIbG9ja19rZXkYASABKAkSEQoJaG9sZGVyX2lkGAIgASgJGjwKFVN5bmNBY3Rpb25SZWZyZXNoTG9jaxIQCghsb2NrX2tleRgBIAEoCRIRCglob2xkZXJfaWQYAiABKAkitAEKE1JlcG9Db25uZWN0aW9uU3RhdGUSHAoYQ09OTkVDVElPTl9TVEFURV9VTktOT1dOEAASHAoYQ09OTkVDVElPTl9TVEFURV9QRU5ESU5HEAESHgoaQ09OTkVDVElPTl9TVEFURV9DT05ORUNURUQQAhIhCh1DT05ORUNUSU9OX1NUQVRFX1VOQVVUSE9SSVpFRBADEh4KGkNPTk5FQ1RJT05fU1RBVEVfTk9UX0ZPVU5EEARCCAoGYWN0aW9uKpwCCg9Db25uZWN0aW9uU3RhdGUSHAoYQ09OTkVDVElPTl9TVEFURV9VTktOT1dOEAASHAoYQ09OTkVDVElPTl9TVEFURV9QRU5ESU5HEAESHgoaQ09OTkVDVElPTl9TVEFURV9DT05ORUNURUQQAhIhCh1DT05ORUNUSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBADEh8KG0NPTk5FQ1RJT05fU1RBVEVfUkVUUllfV0FJVBAEEh8KG0NPTk5FQ1RJT05fU1RBVEVfRVJST1JfQVVUSBAKEiMKH0NPTk5FQ1RJT05fU1RBVEVfRVJST1JfUFJPVE9DT0wQCxIjCh9DT05ORUNUSU9OX1NUQVRFX0VSUk9SX0lOVEVSTkFMEAwyUwoTQmFja3Jlc3RTeW5jU2VydmljZRI8CgRTeW5jEhYudjFzeW5jLlN5bmNTdHJlYW1JdGVtGhYudjFzeW5jLlN5bmNTdHJlYW1JdGVtIgAoATABMtQBChhCYWNrcmVzdFN5bmNTdGF0ZVNlcnZpY2USUAoXR2V0UGVlclN5bmNTdGF0ZXNTdHJlYW0SHi52MXN5bmMuU3luY1N0YXRlU3RyZWFtUmVxdWVzdBoRLnYxc3luYy5QZWVyU3RhdGUiADABEmYKFVNldFJlbW90ZUNsaWVudENvbmZpZxIkLnYxc3luYy5TZXRSZW1vdGVDbGllbnRDb25maWdSZXF1ZXN0GiUudjFzeW5jLlNldFJlbW90ZUNsaWVudENvbmZpZ1Jlc3BvbnNlIgBCMFouZ2l0aHViLmNvbS9nYXJldGhnZW9yZ2UvYmFja3Jlc3QvZ2VuL2dvL3Yxc3luY2IGcHJvdG8z", [file_v1_config, file_v1_crypto, file_v1_restic, file_v1_service, file_v1_operations, file_types_value, file_google_protobuf_empty, file_google_api_annotations, file_google_protobuf_any]); /** * @generated from message v1sync.SyncStateStreamRequest @@ -497,6 +497,30 @@ export type SyncStreamItem = Message<"v1sync.SyncStreamItem"> & { */ value: SyncStreamItem_SyncActionReceiveLogData; case: "receiveLogData"; + } | { + /** + * @generated from field: v1sync.SyncStreamItem.SyncActionAcquireLock acquire_lock = 40; + */ + value: SyncStreamItem_SyncActionAcquireLock; + case: "acquireLock"; + } | { + /** + * @generated from field: v1sync.SyncStreamItem.SyncActionAcquireLockResponse acquire_lock_response = 41; + */ + value: SyncStreamItem_SyncActionAcquireLockResponse; + case: "acquireLockResponse"; + } | { + /** + * @generated from field: v1sync.SyncStreamItem.SyncActionReleaseLock release_lock = 42; + */ + value: SyncStreamItem_SyncActionReleaseLock; + case: "releaseLock"; + } | { + /** + * @generated from field: v1sync.SyncStreamItem.SyncActionRefreshLock refresh_lock = 43; + */ + value: SyncStreamItem_SyncActionRefreshLock; + case: "refreshLock"; } | { /** * @generated from field: v1sync.SyncStreamItem.SyncActionThrottle throttle = 1000; @@ -822,6 +846,112 @@ export type SyncStreamItem_SyncEstablishSharedSecret = Message<"v1sync.SyncStrea export const SyncStreamItem_SyncEstablishSharedSecretSchema: GenMessage = /*@__PURE__*/ messageDesc(file_v1sync_syncservice, 13, 13); +/** + * Lock protocol messages for coordinating repo access between peers. + * + * @generated from message v1sync.SyncStreamItem.SyncActionAcquireLock + */ +export type SyncStreamItem_SyncActionAcquireLock = Message<"v1sync.SyncStreamItem.SyncActionAcquireLock"> & { + /** + * the lock key, typically a repo ID. + * + * @generated from field: string lock_key = 1; + */ + lockKey: string; + + /** + * the instance ID of the lock requester. + * + * @generated from field: string holder_id = 2; + */ + holderId: string; +}; + +/** + * Describes the message v1sync.SyncStreamItem.SyncActionAcquireLock. + * Use `create(SyncStreamItem_SyncActionAcquireLockSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionAcquireLockSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1sync_syncservice, 13, 14); + +/** + * @generated from message v1sync.SyncStreamItem.SyncActionAcquireLockResponse + */ +export type SyncStreamItem_SyncActionAcquireLockResponse = Message<"v1sync.SyncStreamItem.SyncActionAcquireLockResponse"> & { + /** + * whether the lock was acquired. + * + * @generated from field: bool acquired = 1; + */ + acquired: boolean; + + /** + * the lock key that was requested. + * + * @generated from field: string lock_key = 2; + */ + lockKey: string; +}; + +/** + * Describes the message v1sync.SyncStreamItem.SyncActionAcquireLockResponse. + * Use `create(SyncStreamItem_SyncActionAcquireLockResponseSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionAcquireLockResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1sync_syncservice, 13, 15); + +/** + * @generated from message v1sync.SyncStreamItem.SyncActionReleaseLock + */ +export type SyncStreamItem_SyncActionReleaseLock = Message<"v1sync.SyncStreamItem.SyncActionReleaseLock"> & { + /** + * the lock key to release. + * + * @generated from field: string lock_key = 1; + */ + lockKey: string; + + /** + * the instance ID of the lock holder. + * + * @generated from field: string holder_id = 2; + */ + holderId: string; +}; + +/** + * Describes the message v1sync.SyncStreamItem.SyncActionReleaseLock. + * Use `create(SyncStreamItem_SyncActionReleaseLockSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionReleaseLockSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1sync_syncservice, 13, 16); + +/** + * @generated from message v1sync.SyncStreamItem.SyncActionRefreshLock + */ +export type SyncStreamItem_SyncActionRefreshLock = Message<"v1sync.SyncStreamItem.SyncActionRefreshLock"> & { + /** + * the lock key to refresh. + * + * @generated from field: string lock_key = 1; + */ + lockKey: string; + + /** + * the instance ID of the lock holder. + * + * @generated from field: string holder_id = 2; + */ + holderId: string; +}; + +/** + * Describes the message v1sync.SyncStreamItem.SyncActionRefreshLock. + * Use `create(SyncStreamItem_SyncActionRefreshLockSchema)` to create a new message. + */ +export const SyncStreamItem_SyncActionRefreshLockSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_v1sync_syncservice, 13, 17); + /** * @generated from enum v1sync.SyncStreamItem.RepoConnectionState */ diff --git a/webui/src/app/App.tsx b/webui/src/app/App.tsx index e9982fb9..28e94593 100644 --- a/webui/src/app/App.tsx +++ b/webui/src/app/App.tsx @@ -552,6 +552,17 @@ const SidebarRepoItem = React.memo( > {repo.id} + {repo.originInstanceId && ( + + {repo.originInstanceId} + + )} { + const [config] = useConfig(); const allPeerStates = useSyncStates(); const peerStates = useMemo(() => { const map = new Map(); @@ -371,6 +372,19 @@ const MultihostSummary = ({ return map; }, [allPeerStates]); + // Build a map of host instance ID -> repo IDs shared by that host (repos in local config with originInstanceId set) + const sharedReposByHost = useMemo(() => { + const map = new Map(); + for (const repo of config?.repos || []) { + if (repo.originInstanceId) { + const repos = map.get(repo.originInstanceId) || []; + repos.push(repo.id); + map.set(repo.originInstanceId, repos); + } + } + return map; + }, [config?.repos]); + const knownHostTiles: JSX.Element[] = []; for (const cfgPeer of multihostConfig?.knownHosts || []) { const peerState = peerStates.get(cfgPeer.keyid); @@ -378,7 +392,11 @@ const MultihostSummary = ({ continue; } knownHostTiles.push( - , + , ); } @@ -411,7 +429,13 @@ const MultihostSummary = ({ ); }; -const PeerStateTile = ({ peerState }: { peerState: PeerState }) => { +const PeerStateTile = ({ + peerState, + sharedRepoIds, +}: { + peerState: PeerState; + sharedRepoIds?: string[]; +}) => { const state = useState(1); useEffect(() => { // Force rerender every second to update the last heartbeat time @@ -470,6 +494,27 @@ const PeerStateTile = ({ peerState }: { peerState: PeerState }) => { } /> )} + {sharedRepoIds && sharedRepoIds.length > 0 && ( + + {sharedRepoIds.map((repoId) => ( + + {repoId} + + ))} + + } + /> + )}