From f1b67b9909890bf8a887267a81300aacf98093cc Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 25 Mar 2026 15:06:20 -0700 Subject: [PATCH] Add GetNotebook RPC for pulling complete notebook data New RPC returns notebook metadata, all pages, and all strokes for a given server-side notebook ID. Enables desktop and other clients to download notebooks from the server (pull sync). Co-Authored-By: Claude Opus 4.6 (1M context) --- gen/engpad/v1/sync.pb.go | 301 +++++++++++++++++++++++++--------- gen/engpad/v1/sync_grpc.pb.go | 38 +++++ internal/grpcserver/sync.go | 65 ++++++++ proto/engpad/v1/sync.proto | 14 ++ 4 files changed, 340 insertions(+), 78 deletions(-) diff --git a/gen/engpad/v1/sync.pb.go b/gen/engpad/v1/sync.pb.go index 1a58f3f..1a25c57 100644 --- a/gen/engpad/v1/sync.pb.go +++ b/gen/engpad/v1/sync.pb.go @@ -278,6 +278,134 @@ func (x *SyncNotebookResponse) GetSyncedAt() *timestamppb.Timestamp { return nil } +type GetNotebookRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NotebookId int64 `protobuf:"varint,1,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` // Server-side notebook ID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetNotebookRequest) Reset() { + *x = GetNotebookRequest{} + mi := &file_proto_engpad_v1_sync_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetNotebookRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNotebookRequest) ProtoMessage() {} + +func (x *GetNotebookRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_engpad_v1_sync_proto_msgTypes[4] + 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 GetNotebookRequest.ProtoReflect.Descriptor instead. +func (*GetNotebookRequest) Descriptor() ([]byte, []int) { + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{4} +} + +func (x *GetNotebookRequest) GetNotebookId() int64 { + if x != nil { + return x.NotebookId + } + return 0 +} + +type GetNotebookResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServerNotebookId int64 `protobuf:"varint,1,opt,name=server_notebook_id,json=serverNotebookId,proto3" json:"server_notebook_id,omitempty"` + RemoteId int64 `protobuf:"varint,2,opt,name=remote_id,json=remoteId,proto3" json:"remote_id,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + PageSize string `protobuf:"bytes,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + Pages []*PageData `protobuf:"bytes,5,rep,name=pages,proto3" json:"pages,omitempty"` + SyncedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=synced_at,json=syncedAt,proto3" json:"synced_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetNotebookResponse) Reset() { + *x = GetNotebookResponse{} + mi := &file_proto_engpad_v1_sync_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetNotebookResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNotebookResponse) ProtoMessage() {} + +func (x *GetNotebookResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_engpad_v1_sync_proto_msgTypes[5] + 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 GetNotebookResponse.ProtoReflect.Descriptor instead. +func (*GetNotebookResponse) Descriptor() ([]byte, []int) { + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{5} +} + +func (x *GetNotebookResponse) GetServerNotebookId() int64 { + if x != nil { + return x.ServerNotebookId + } + return 0 +} + +func (x *GetNotebookResponse) GetRemoteId() int64 { + if x != nil { + return x.RemoteId + } + return 0 +} + +func (x *GetNotebookResponse) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *GetNotebookResponse) GetPageSize() string { + if x != nil { + return x.PageSize + } + return "" +} + +func (x *GetNotebookResponse) GetPages() []*PageData { + if x != nil { + return x.Pages + } + return nil +} + +func (x *GetNotebookResponse) GetSyncedAt() *timestamppb.Timestamp { + if x != nil { + return x.SyncedAt + } + return nil +} + type DeleteNotebookRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NotebookId int64 `protobuf:"varint,1,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` @@ -287,7 +415,7 @@ type DeleteNotebookRequest struct { func (x *DeleteNotebookRequest) Reset() { *x = DeleteNotebookRequest{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[4] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -299,7 +427,7 @@ func (x *DeleteNotebookRequest) String() string { func (*DeleteNotebookRequest) ProtoMessage() {} func (x *DeleteNotebookRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[4] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -312,7 +440,7 @@ func (x *DeleteNotebookRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteNotebookRequest.ProtoReflect.Descriptor instead. func (*DeleteNotebookRequest) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{4} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{6} } func (x *DeleteNotebookRequest) GetNotebookId() int64 { @@ -330,7 +458,7 @@ type DeleteNotebookResponse struct { func (x *DeleteNotebookResponse) Reset() { *x = DeleteNotebookResponse{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[5] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -342,7 +470,7 @@ func (x *DeleteNotebookResponse) String() string { func (*DeleteNotebookResponse) ProtoMessage() {} func (x *DeleteNotebookResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[5] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -355,7 +483,7 @@ func (x *DeleteNotebookResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteNotebookResponse.ProtoReflect.Descriptor instead. func (*DeleteNotebookResponse) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{5} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{7} } type ListNotebooksRequest struct { @@ -366,7 +494,7 @@ type ListNotebooksRequest struct { func (x *ListNotebooksRequest) Reset() { *x = ListNotebooksRequest{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[6] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -378,7 +506,7 @@ func (x *ListNotebooksRequest) String() string { func (*ListNotebooksRequest) ProtoMessage() {} func (x *ListNotebooksRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[6] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -391,7 +519,7 @@ func (x *ListNotebooksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNotebooksRequest.ProtoReflect.Descriptor instead. func (*ListNotebooksRequest) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{6} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{8} } type ListNotebooksResponse struct { @@ -403,7 +531,7 @@ type ListNotebooksResponse struct { func (x *ListNotebooksResponse) Reset() { *x = ListNotebooksResponse{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[7] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -415,7 +543,7 @@ func (x *ListNotebooksResponse) String() string { func (*ListNotebooksResponse) ProtoMessage() {} func (x *ListNotebooksResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[7] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -428,7 +556,7 @@ func (x *ListNotebooksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNotebooksResponse.ProtoReflect.Descriptor instead. func (*ListNotebooksResponse) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{7} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{9} } func (x *ListNotebooksResponse) GetNotebooks() []*NotebookSummary { @@ -452,7 +580,7 @@ type NotebookSummary struct { func (x *NotebookSummary) Reset() { *x = NotebookSummary{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[8] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -464,7 +592,7 @@ func (x *NotebookSummary) String() string { func (*NotebookSummary) ProtoMessage() {} func (x *NotebookSummary) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[8] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -477,7 +605,7 @@ func (x *NotebookSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NotebookSummary.ProtoReflect.Descriptor instead. func (*NotebookSummary) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{8} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{10} } func (x *NotebookSummary) GetServerId() int64 { @@ -532,7 +660,7 @@ type CreateShareLinkRequest struct { func (x *CreateShareLinkRequest) Reset() { *x = CreateShareLinkRequest{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[9] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -544,7 +672,7 @@ func (x *CreateShareLinkRequest) String() string { func (*CreateShareLinkRequest) ProtoMessage() {} func (x *CreateShareLinkRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[9] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -557,7 +685,7 @@ func (x *CreateShareLinkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateShareLinkRequest.ProtoReflect.Descriptor instead. func (*CreateShareLinkRequest) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{9} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{11} } func (x *CreateShareLinkRequest) GetNotebookId() int64 { @@ -585,7 +713,7 @@ type CreateShareLinkResponse struct { func (x *CreateShareLinkResponse) Reset() { *x = CreateShareLinkResponse{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[10] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -597,7 +725,7 @@ func (x *CreateShareLinkResponse) String() string { func (*CreateShareLinkResponse) ProtoMessage() {} func (x *CreateShareLinkResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[10] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -610,7 +738,7 @@ func (x *CreateShareLinkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateShareLinkResponse.ProtoReflect.Descriptor instead. func (*CreateShareLinkResponse) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{10} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{12} } func (x *CreateShareLinkResponse) GetToken() string { @@ -643,7 +771,7 @@ type RevokeShareLinkRequest struct { func (x *RevokeShareLinkRequest) Reset() { *x = RevokeShareLinkRequest{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[11] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -655,7 +783,7 @@ func (x *RevokeShareLinkRequest) String() string { func (*RevokeShareLinkRequest) ProtoMessage() {} func (x *RevokeShareLinkRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[11] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -668,7 +796,7 @@ func (x *RevokeShareLinkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeShareLinkRequest.ProtoReflect.Descriptor instead. func (*RevokeShareLinkRequest) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{11} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{13} } func (x *RevokeShareLinkRequest) GetToken() string { @@ -686,7 +814,7 @@ type RevokeShareLinkResponse struct { func (x *RevokeShareLinkResponse) Reset() { *x = RevokeShareLinkResponse{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[12] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -698,7 +826,7 @@ func (x *RevokeShareLinkResponse) String() string { func (*RevokeShareLinkResponse) ProtoMessage() {} func (x *RevokeShareLinkResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[12] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -711,7 +839,7 @@ func (x *RevokeShareLinkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeShareLinkResponse.ProtoReflect.Descriptor instead. func (*RevokeShareLinkResponse) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{12} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{14} } type ListShareLinksRequest struct { @@ -723,7 +851,7 @@ type ListShareLinksRequest struct { func (x *ListShareLinksRequest) Reset() { *x = ListShareLinksRequest{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[13] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -735,7 +863,7 @@ func (x *ListShareLinksRequest) String() string { func (*ListShareLinksRequest) ProtoMessage() {} func (x *ListShareLinksRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[13] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -748,7 +876,7 @@ func (x *ListShareLinksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListShareLinksRequest.ProtoReflect.Descriptor instead. func (*ListShareLinksRequest) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{13} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{15} } func (x *ListShareLinksRequest) GetNotebookId() int64 { @@ -767,7 +895,7 @@ type ListShareLinksResponse struct { func (x *ListShareLinksResponse) Reset() { *x = ListShareLinksResponse{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[14] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -779,7 +907,7 @@ func (x *ListShareLinksResponse) String() string { func (*ListShareLinksResponse) ProtoMessage() {} func (x *ListShareLinksResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[14] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -792,7 +920,7 @@ func (x *ListShareLinksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListShareLinksResponse.ProtoReflect.Descriptor instead. func (*ListShareLinksResponse) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{14} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{16} } func (x *ListShareLinksResponse) GetLinks() []*ShareLinkInfo { @@ -814,7 +942,7 @@ type ShareLinkInfo struct { func (x *ShareLinkInfo) Reset() { *x = ShareLinkInfo{} - mi := &file_proto_engpad_v1_sync_proto_msgTypes[15] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -826,7 +954,7 @@ func (x *ShareLinkInfo) String() string { func (*ShareLinkInfo) ProtoMessage() {} func (x *ShareLinkInfo) ProtoReflect() protoreflect.Message { - mi := &file_proto_engpad_v1_sync_proto_msgTypes[15] + mi := &file_proto_engpad_v1_sync_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -839,7 +967,7 @@ func (x *ShareLinkInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ShareLinkInfo.ProtoReflect.Descriptor instead. func (*ShareLinkInfo) Descriptor() ([]byte, []int) { - return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{15} + return file_proto_engpad_v1_sync_proto_rawDescGZIP(), []int{17} } func (x *ShareLinkInfo) GetToken() string { @@ -896,7 +1024,17 @@ const file_proto_engpad_v1_sync_proto_rawDesc = "" + "\fstroke_order\x18\x05 \x01(\x05R\vstrokeOrder\"}\n" + "\x14SyncNotebookResponse\x12,\n" + "\x12server_notebook_id\x18\x01 \x01(\x03R\x10serverNotebookId\x127\n" + - "\tsynced_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bsyncedAt\"8\n" + + "\tsynced_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bsyncedAt\"5\n" + + "\x12GetNotebookRequest\x12\x1f\n" + + "\vnotebook_id\x18\x01 \x01(\x03R\n" + + "notebookId\"\xf7\x01\n" + + "\x13GetNotebookResponse\x12,\n" + + "\x12server_notebook_id\x18\x01 \x01(\x03R\x10serverNotebookId\x12\x1b\n" + + "\tremote_id\x18\x02 \x01(\x03R\bremoteId\x12\x14\n" + + "\x05title\x18\x03 \x01(\tR\x05title\x12\x1b\n" + + "\tpage_size\x18\x04 \x01(\tR\bpageSize\x12)\n" + + "\x05pages\x18\x05 \x03(\v2\x13.engpad.v1.PageDataR\x05pages\x127\n" + + "\tsynced_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\bsyncedAt\"8\n" + "\x15DeleteNotebookRequest\x12\x1f\n" + "\vnotebook_id\x18\x01 \x01(\x03R\n" + "notebookId\"\x18\n" + @@ -935,9 +1073,10 @@ const file_proto_engpad_v1_sync_proto_rawDesc = "" + "\n" + "created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + "\n" + - "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt2\x9a\x04\n" + + "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt2\xe8\x04\n" + "\x11EngPadSyncService\x12O\n" + - "\fSyncNotebook\x12\x1e.engpad.v1.SyncNotebookRequest\x1a\x1f.engpad.v1.SyncNotebookResponse\x12U\n" + + "\fSyncNotebook\x12\x1e.engpad.v1.SyncNotebookRequest\x1a\x1f.engpad.v1.SyncNotebookResponse\x12L\n" + + "\vGetNotebook\x12\x1d.engpad.v1.GetNotebookRequest\x1a\x1e.engpad.v1.GetNotebookResponse\x12U\n" + "\x0eDeleteNotebook\x12 .engpad.v1.DeleteNotebookRequest\x1a!.engpad.v1.DeleteNotebookResponse\x12R\n" + "\rListNotebooks\x12\x1f.engpad.v1.ListNotebooksRequest\x1a .engpad.v1.ListNotebooksResponse\x12X\n" + "\x0fCreateShareLink\x12!.engpad.v1.CreateShareLinkRequest\x1a\".engpad.v1.CreateShareLinkResponse\x12X\n" + @@ -957,53 +1096,59 @@ func file_proto_engpad_v1_sync_proto_rawDescGZIP() []byte { return file_proto_engpad_v1_sync_proto_rawDescData } -var file_proto_engpad_v1_sync_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_proto_engpad_v1_sync_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_proto_engpad_v1_sync_proto_goTypes = []any{ (*SyncNotebookRequest)(nil), // 0: engpad.v1.SyncNotebookRequest (*PageData)(nil), // 1: engpad.v1.PageData (*StrokeData)(nil), // 2: engpad.v1.StrokeData (*SyncNotebookResponse)(nil), // 3: engpad.v1.SyncNotebookResponse - (*DeleteNotebookRequest)(nil), // 4: engpad.v1.DeleteNotebookRequest - (*DeleteNotebookResponse)(nil), // 5: engpad.v1.DeleteNotebookResponse - (*ListNotebooksRequest)(nil), // 6: engpad.v1.ListNotebooksRequest - (*ListNotebooksResponse)(nil), // 7: engpad.v1.ListNotebooksResponse - (*NotebookSummary)(nil), // 8: engpad.v1.NotebookSummary - (*CreateShareLinkRequest)(nil), // 9: engpad.v1.CreateShareLinkRequest - (*CreateShareLinkResponse)(nil), // 10: engpad.v1.CreateShareLinkResponse - (*RevokeShareLinkRequest)(nil), // 11: engpad.v1.RevokeShareLinkRequest - (*RevokeShareLinkResponse)(nil), // 12: engpad.v1.RevokeShareLinkResponse - (*ListShareLinksRequest)(nil), // 13: engpad.v1.ListShareLinksRequest - (*ListShareLinksResponse)(nil), // 14: engpad.v1.ListShareLinksResponse - (*ShareLinkInfo)(nil), // 15: engpad.v1.ShareLinkInfo - (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp + (*GetNotebookRequest)(nil), // 4: engpad.v1.GetNotebookRequest + (*GetNotebookResponse)(nil), // 5: engpad.v1.GetNotebookResponse + (*DeleteNotebookRequest)(nil), // 6: engpad.v1.DeleteNotebookRequest + (*DeleteNotebookResponse)(nil), // 7: engpad.v1.DeleteNotebookResponse + (*ListNotebooksRequest)(nil), // 8: engpad.v1.ListNotebooksRequest + (*ListNotebooksResponse)(nil), // 9: engpad.v1.ListNotebooksResponse + (*NotebookSummary)(nil), // 10: engpad.v1.NotebookSummary + (*CreateShareLinkRequest)(nil), // 11: engpad.v1.CreateShareLinkRequest + (*CreateShareLinkResponse)(nil), // 12: engpad.v1.CreateShareLinkResponse + (*RevokeShareLinkRequest)(nil), // 13: engpad.v1.RevokeShareLinkRequest + (*RevokeShareLinkResponse)(nil), // 14: engpad.v1.RevokeShareLinkResponse + (*ListShareLinksRequest)(nil), // 15: engpad.v1.ListShareLinksRequest + (*ListShareLinksResponse)(nil), // 16: engpad.v1.ListShareLinksResponse + (*ShareLinkInfo)(nil), // 17: engpad.v1.ShareLinkInfo + (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp } var file_proto_engpad_v1_sync_proto_depIdxs = []int32{ 1, // 0: engpad.v1.SyncNotebookRequest.pages:type_name -> engpad.v1.PageData 2, // 1: engpad.v1.PageData.strokes:type_name -> engpad.v1.StrokeData - 16, // 2: engpad.v1.SyncNotebookResponse.synced_at:type_name -> google.protobuf.Timestamp - 8, // 3: engpad.v1.ListNotebooksResponse.notebooks:type_name -> engpad.v1.NotebookSummary - 16, // 4: engpad.v1.NotebookSummary.synced_at:type_name -> google.protobuf.Timestamp - 16, // 5: engpad.v1.CreateShareLinkResponse.expires_at:type_name -> google.protobuf.Timestamp - 15, // 6: engpad.v1.ListShareLinksResponse.links:type_name -> engpad.v1.ShareLinkInfo - 16, // 7: engpad.v1.ShareLinkInfo.created_at:type_name -> google.protobuf.Timestamp - 16, // 8: engpad.v1.ShareLinkInfo.expires_at:type_name -> google.protobuf.Timestamp - 0, // 9: engpad.v1.EngPadSyncService.SyncNotebook:input_type -> engpad.v1.SyncNotebookRequest - 4, // 10: engpad.v1.EngPadSyncService.DeleteNotebook:input_type -> engpad.v1.DeleteNotebookRequest - 6, // 11: engpad.v1.EngPadSyncService.ListNotebooks:input_type -> engpad.v1.ListNotebooksRequest - 9, // 12: engpad.v1.EngPadSyncService.CreateShareLink:input_type -> engpad.v1.CreateShareLinkRequest - 11, // 13: engpad.v1.EngPadSyncService.RevokeShareLink:input_type -> engpad.v1.RevokeShareLinkRequest - 13, // 14: engpad.v1.EngPadSyncService.ListShareLinks:input_type -> engpad.v1.ListShareLinksRequest - 3, // 15: engpad.v1.EngPadSyncService.SyncNotebook:output_type -> engpad.v1.SyncNotebookResponse - 5, // 16: engpad.v1.EngPadSyncService.DeleteNotebook:output_type -> engpad.v1.DeleteNotebookResponse - 7, // 17: engpad.v1.EngPadSyncService.ListNotebooks:output_type -> engpad.v1.ListNotebooksResponse - 10, // 18: engpad.v1.EngPadSyncService.CreateShareLink:output_type -> engpad.v1.CreateShareLinkResponse - 12, // 19: engpad.v1.EngPadSyncService.RevokeShareLink:output_type -> engpad.v1.RevokeShareLinkResponse - 14, // 20: engpad.v1.EngPadSyncService.ListShareLinks:output_type -> engpad.v1.ListShareLinksResponse - 15, // [15:21] is the sub-list for method output_type - 9, // [9:15] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 18, // 2: engpad.v1.SyncNotebookResponse.synced_at:type_name -> google.protobuf.Timestamp + 1, // 3: engpad.v1.GetNotebookResponse.pages:type_name -> engpad.v1.PageData + 18, // 4: engpad.v1.GetNotebookResponse.synced_at:type_name -> google.protobuf.Timestamp + 10, // 5: engpad.v1.ListNotebooksResponse.notebooks:type_name -> engpad.v1.NotebookSummary + 18, // 6: engpad.v1.NotebookSummary.synced_at:type_name -> google.protobuf.Timestamp + 18, // 7: engpad.v1.CreateShareLinkResponse.expires_at:type_name -> google.protobuf.Timestamp + 17, // 8: engpad.v1.ListShareLinksResponse.links:type_name -> engpad.v1.ShareLinkInfo + 18, // 9: engpad.v1.ShareLinkInfo.created_at:type_name -> google.protobuf.Timestamp + 18, // 10: engpad.v1.ShareLinkInfo.expires_at:type_name -> google.protobuf.Timestamp + 0, // 11: engpad.v1.EngPadSyncService.SyncNotebook:input_type -> engpad.v1.SyncNotebookRequest + 4, // 12: engpad.v1.EngPadSyncService.GetNotebook:input_type -> engpad.v1.GetNotebookRequest + 6, // 13: engpad.v1.EngPadSyncService.DeleteNotebook:input_type -> engpad.v1.DeleteNotebookRequest + 8, // 14: engpad.v1.EngPadSyncService.ListNotebooks:input_type -> engpad.v1.ListNotebooksRequest + 11, // 15: engpad.v1.EngPadSyncService.CreateShareLink:input_type -> engpad.v1.CreateShareLinkRequest + 13, // 16: engpad.v1.EngPadSyncService.RevokeShareLink:input_type -> engpad.v1.RevokeShareLinkRequest + 15, // 17: engpad.v1.EngPadSyncService.ListShareLinks:input_type -> engpad.v1.ListShareLinksRequest + 3, // 18: engpad.v1.EngPadSyncService.SyncNotebook:output_type -> engpad.v1.SyncNotebookResponse + 5, // 19: engpad.v1.EngPadSyncService.GetNotebook:output_type -> engpad.v1.GetNotebookResponse + 7, // 20: engpad.v1.EngPadSyncService.DeleteNotebook:output_type -> engpad.v1.DeleteNotebookResponse + 9, // 21: engpad.v1.EngPadSyncService.ListNotebooks:output_type -> engpad.v1.ListNotebooksResponse + 12, // 22: engpad.v1.EngPadSyncService.CreateShareLink:output_type -> engpad.v1.CreateShareLinkResponse + 14, // 23: engpad.v1.EngPadSyncService.RevokeShareLink:output_type -> engpad.v1.RevokeShareLinkResponse + 16, // 24: engpad.v1.EngPadSyncService.ListShareLinks:output_type -> engpad.v1.ListShareLinksResponse + 18, // [18:25] is the sub-list for method output_type + 11, // [11:18] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_proto_engpad_v1_sync_proto_init() } @@ -1017,7 +1162,7 @@ func file_proto_engpad_v1_sync_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_engpad_v1_sync_proto_rawDesc), len(file_proto_engpad_v1_sync_proto_rawDesc)), NumEnums: 0, - NumMessages: 16, + NumMessages: 18, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/engpad/v1/sync_grpc.pb.go b/gen/engpad/v1/sync_grpc.pb.go index ec904ee..169d02a 100644 --- a/gen/engpad/v1/sync_grpc.pb.go +++ b/gen/engpad/v1/sync_grpc.pb.go @@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9 const ( EngPadSyncService_SyncNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/SyncNotebook" + EngPadSyncService_GetNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/GetNotebook" EngPadSyncService_DeleteNotebook_FullMethodName = "/engpad.v1.EngPadSyncService/DeleteNotebook" EngPadSyncService_ListNotebooks_FullMethodName = "/engpad.v1.EngPadSyncService/ListNotebooks" EngPadSyncService_CreateShareLink_FullMethodName = "/engpad.v1.EngPadSyncService/CreateShareLink" @@ -32,6 +33,7 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type EngPadSyncServiceClient interface { SyncNotebook(ctx context.Context, in *SyncNotebookRequest, opts ...grpc.CallOption) (*SyncNotebookResponse, error) + GetNotebook(ctx context.Context, in *GetNotebookRequest, opts ...grpc.CallOption) (*GetNotebookResponse, error) DeleteNotebook(ctx context.Context, in *DeleteNotebookRequest, opts ...grpc.CallOption) (*DeleteNotebookResponse, error) ListNotebooks(ctx context.Context, in *ListNotebooksRequest, opts ...grpc.CallOption) (*ListNotebooksResponse, error) CreateShareLink(ctx context.Context, in *CreateShareLinkRequest, opts ...grpc.CallOption) (*CreateShareLinkResponse, error) @@ -57,6 +59,16 @@ func (c *engPadSyncServiceClient) SyncNotebook(ctx context.Context, in *SyncNote return out, nil } +func (c *engPadSyncServiceClient) GetNotebook(ctx context.Context, in *GetNotebookRequest, opts ...grpc.CallOption) (*GetNotebookResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetNotebookResponse) + err := c.cc.Invoke(ctx, EngPadSyncService_GetNotebook_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *engPadSyncServiceClient) DeleteNotebook(ctx context.Context, in *DeleteNotebookRequest, opts ...grpc.CallOption) (*DeleteNotebookResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteNotebookResponse) @@ -112,6 +124,7 @@ func (c *engPadSyncServiceClient) ListShareLinks(ctx context.Context, in *ListSh // for forward compatibility. type EngPadSyncServiceServer interface { SyncNotebook(context.Context, *SyncNotebookRequest) (*SyncNotebookResponse, error) + GetNotebook(context.Context, *GetNotebookRequest) (*GetNotebookResponse, error) DeleteNotebook(context.Context, *DeleteNotebookRequest) (*DeleteNotebookResponse, error) ListNotebooks(context.Context, *ListNotebooksRequest) (*ListNotebooksResponse, error) CreateShareLink(context.Context, *CreateShareLinkRequest) (*CreateShareLinkResponse, error) @@ -130,6 +143,9 @@ type UnimplementedEngPadSyncServiceServer struct{} func (UnimplementedEngPadSyncServiceServer) SyncNotebook(context.Context, *SyncNotebookRequest) (*SyncNotebookResponse, error) { return nil, status.Error(codes.Unimplemented, "method SyncNotebook not implemented") } +func (UnimplementedEngPadSyncServiceServer) GetNotebook(context.Context, *GetNotebookRequest) (*GetNotebookResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetNotebook not implemented") +} func (UnimplementedEngPadSyncServiceServer) DeleteNotebook(context.Context, *DeleteNotebookRequest) (*DeleteNotebookResponse, error) { return nil, status.Error(codes.Unimplemented, "method DeleteNotebook not implemented") } @@ -184,6 +200,24 @@ func _EngPadSyncService_SyncNotebook_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _EngPadSyncService_GetNotebook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetNotebookRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EngPadSyncServiceServer).GetNotebook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: EngPadSyncService_GetNotebook_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EngPadSyncServiceServer).GetNotebook(ctx, req.(*GetNotebookRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _EngPadSyncService_DeleteNotebook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteNotebookRequest) if err := dec(in); err != nil { @@ -285,6 +319,10 @@ var EngPadSyncService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SyncNotebook", Handler: _EngPadSyncService_SyncNotebook_Handler, }, + { + MethodName: "GetNotebook", + Handler: _EngPadSyncService_GetNotebook_Handler, + }, { MethodName: "DeleteNotebook", Handler: _EngPadSyncService_DeleteNotebook_Handler, diff --git a/internal/grpcserver/sync.go b/internal/grpcserver/sync.go index 19600aa..8dd9066 100644 --- a/internal/grpcserver/sync.go +++ b/internal/grpcserver/sync.go @@ -114,6 +114,71 @@ func (s *SyncService) SyncNotebook(ctx context.Context, req *pb.SyncNotebookRequ }, nil } +func (s *SyncService) GetNotebook(ctx context.Context, req *pb.GetNotebookRequest) (*pb.GetNotebookResponse, error) { + userID, ok := UserIDFromContext(ctx) + if !ok { + return nil, status.Error(codes.Internal, "missing user context") + } + + var resp pb.GetNotebookResponse + var syncedAt int64 + err := s.DB.QueryRowContext(ctx, + "SELECT id, remote_id, title, page_size, synced_at FROM notebooks WHERE id = ? AND user_id = ?", + req.NotebookId, userID, + ).Scan(&resp.ServerNotebookId, &resp.RemoteId, &resp.Title, &resp.PageSize, &syncedAt) + if err == sql.ErrNoRows { + return nil, status.Error(codes.NotFound, "notebook not found") + } + if err != nil { + return nil, status.Errorf(codes.Internal, "query notebook: %v", err) + } + resp.SyncedAt = timestamppb.New(time.UnixMilli(syncedAt)) + + pageRows, err := s.DB.QueryContext(ctx, + "SELECT id, remote_id, page_number FROM pages WHERE notebook_id = ? ORDER BY page_number", + resp.ServerNotebookId, + ) + if err != nil { + return nil, status.Errorf(codes.Internal, "query pages: %v", err) + } + defer func() { _ = pageRows.Close() }() + + for pageRows.Next() { + var pageID, remoteID int64 + var pageNum int32 + if err := pageRows.Scan(&pageID, &remoteID, &pageNum); err != nil { + return nil, status.Errorf(codes.Internal, "scan page: %v", err) + } + + pd := &pb.PageData{ + PageId: remoteID, + PageNumber: pageNum, + } + + strokeRows, err := s.DB.QueryContext(ctx, + "SELECT pen_size, color, style, point_data, stroke_order FROM strokes WHERE page_id = ? ORDER BY stroke_order", + pageID, + ) + if err != nil { + return nil, status.Errorf(codes.Internal, "query strokes: %v", err) + } + + for strokeRows.Next() { + var sd pb.StrokeData + if err := strokeRows.Scan(&sd.PenSize, &sd.Color, &sd.Style, &sd.PointData, &sd.StrokeOrder); err != nil { + _ = strokeRows.Close() + return nil, status.Errorf(codes.Internal, "scan stroke: %v", err) + } + pd.Strokes = append(pd.Strokes, &sd) + } + _ = strokeRows.Close() + + resp.Pages = append(resp.Pages, pd) + } + + return &resp, nil +} + func (s *SyncService) DeleteNotebook(ctx context.Context, req *pb.DeleteNotebookRequest) (*pb.DeleteNotebookResponse, error) { userID, ok := UserIDFromContext(ctx) if !ok { diff --git a/proto/engpad/v1/sync.proto b/proto/engpad/v1/sync.proto index 9a36203..a9cc6bc 100644 --- a/proto/engpad/v1/sync.proto +++ b/proto/engpad/v1/sync.proto @@ -9,6 +9,7 @@ import "google/protobuf/timestamp.proto"; service EngPadSyncService { rpc SyncNotebook(SyncNotebookRequest) returns (SyncNotebookResponse); + rpc GetNotebook(GetNotebookRequest) returns (GetNotebookResponse); rpc DeleteNotebook(DeleteNotebookRequest) returns (DeleteNotebookResponse); rpc ListNotebooks(ListNotebooksRequest) returns (ListNotebooksResponse); rpc CreateShareLink(CreateShareLinkRequest) returns (CreateShareLinkResponse); @@ -42,6 +43,19 @@ message SyncNotebookResponse { google.protobuf.Timestamp synced_at = 2; } +message GetNotebookRequest { + int64 notebook_id = 1; // Server-side notebook ID +} + +message GetNotebookResponse { + int64 server_notebook_id = 1; + int64 remote_id = 2; + string title = 3; + string page_size = 4; + repeated PageData pages = 5; + google.protobuf.Timestamp synced_at = 6; +} + message DeleteNotebookRequest { int64 notebook_id = 1; }