Merge: protobuf + sync client skeleton
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package net.metacircular.engpad.data.sync
|
||||
|
||||
import io.grpc.CallOptions
|
||||
import io.grpc.Channel
|
||||
import io.grpc.ClientCall
|
||||
import io.grpc.ClientInterceptor
|
||||
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall
|
||||
import io.grpc.ManagedChannel
|
||||
import io.grpc.Metadata
|
||||
import io.grpc.MethodDescriptor
|
||||
import io.grpc.okhttp.OkHttpChannelBuilder
|
||||
import net.metacircular.engpad.proto.v1.EngPadSyncGrpcKt
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import java.security.KeyStore
|
||||
|
||||
/**
|
||||
* gRPC client for the eng-pad sync service.
|
||||
*
|
||||
* Uses OkHttp transport (suitable for Android) with TLS.
|
||||
* Authentication credentials are sent as gRPC metadata on every call.
|
||||
*/
|
||||
class SyncClient(
|
||||
host: String,
|
||||
port: Int,
|
||||
private val username: String,
|
||||
private val password: String,
|
||||
) {
|
||||
private val channel: ManagedChannel = OkHttpChannelBuilder
|
||||
.forAddress(host, port)
|
||||
.useTransportSecurity()
|
||||
.intercept(AuthInterceptor(username, password))
|
||||
.build()
|
||||
|
||||
val stub: EngPadSyncGrpcKt.EngPadSyncCoroutineStub =
|
||||
EngPadSyncGrpcKt.EngPadSyncCoroutineStub(channel)
|
||||
|
||||
fun shutdown() {
|
||||
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS)
|
||||
}
|
||||
}
|
||||
|
||||
private val USERNAME_KEY: Metadata.Key<String> =
|
||||
Metadata.Key.of("x-engpad-username", Metadata.ASCII_STRING_MARSHALLER)
|
||||
|
||||
private val PASSWORD_KEY: Metadata.Key<String> =
|
||||
Metadata.Key.of("x-engpad-password", Metadata.ASCII_STRING_MARSHALLER)
|
||||
|
||||
/**
|
||||
* Interceptor that attaches username and password metadata to every outgoing call.
|
||||
*/
|
||||
private class AuthInterceptor(
|
||||
private val username: String,
|
||||
private val password: String,
|
||||
) : ClientInterceptor {
|
||||
override fun <ReqT, RespT> interceptCall(
|
||||
method: MethodDescriptor<ReqT, RespT>,
|
||||
callOptions: CallOptions,
|
||||
next: Channel,
|
||||
): ClientCall<ReqT, RespT> {
|
||||
return object : SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
|
||||
override fun start(responseListener: Listener<RespT>, headers: Metadata) {
|
||||
headers.put(USERNAME_KEY, username)
|
||||
headers.put(PASSWORD_KEY, password)
|
||||
super.start(responseListener, headers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package net.metacircular.engpad.data.sync
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.timestamp
|
||||
import net.metacircular.engpad.data.db.NotebookDao
|
||||
import net.metacircular.engpad.data.db.PageDao
|
||||
import net.metacircular.engpad.data.db.StrokeDao
|
||||
import net.metacircular.engpad.data.db.toFloatArray
|
||||
import net.metacircular.engpad.proto.v1.DeleteNotebookRequestKt
|
||||
import net.metacircular.engpad.proto.v1.ListNotebooksRequestKt
|
||||
import net.metacircular.engpad.proto.v1.NotebookSummary
|
||||
import net.metacircular.engpad.proto.v1.deleteNotebookRequest
|
||||
import net.metacircular.engpad.proto.v1.listNotebooksRequest
|
||||
import net.metacircular.engpad.proto.v1.notebook
|
||||
import net.metacircular.engpad.proto.v1.page
|
||||
import net.metacircular.engpad.proto.v1.stroke
|
||||
import net.metacircular.engpad.proto.v1.syncNotebookRequest
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* Coordinates syncing local notebook data to the remote eng-pad sync service.
|
||||
*/
|
||||
class SyncManager(
|
||||
private val client: SyncClient,
|
||||
private val notebookDao: NotebookDao,
|
||||
private val pageDao: PageDao,
|
||||
private val strokeDao: StrokeDao,
|
||||
) {
|
||||
/**
|
||||
* Upload a notebook with all its pages and strokes to the sync server.
|
||||
*/
|
||||
suspend fun syncNotebook(notebookId: Long) {
|
||||
val localNotebook = notebookDao.getById(notebookId)
|
||||
?: throw IllegalArgumentException("Notebook $notebookId not found")
|
||||
val localPages = pageDao.getByNotebookIdList(notebookId)
|
||||
|
||||
val protoNotebook = notebook {
|
||||
id = localNotebook.id
|
||||
title = localNotebook.title
|
||||
pageSize = localNotebook.pageSize
|
||||
createdAt = millisToTimestamp(localNotebook.createdAt)
|
||||
updatedAt = millisToTimestamp(localNotebook.updatedAt)
|
||||
lastPageId = localNotebook.lastPageId
|
||||
for (localPage in localPages) {
|
||||
val localStrokes = strokeDao.getByPageId(localPage.id)
|
||||
pages += page {
|
||||
id = localPage.id
|
||||
notebookId = localPage.notebookId
|
||||
pageNumber = localPage.pageNumber
|
||||
createdAt = millisToTimestamp(localPage.createdAt)
|
||||
for (localStroke in localStrokes) {
|
||||
strokes += stroke {
|
||||
id = localStroke.id
|
||||
pageId = localStroke.pageId
|
||||
penSize = localStroke.penSize
|
||||
color = localStroke.color
|
||||
pointData = ByteString.copyFrom(localStroke.pointData)
|
||||
strokeOrder = localStroke.strokeOrder
|
||||
createdAt = millisToTimestamp(localStroke.createdAt)
|
||||
style = localStroke.style
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val request = syncNotebookRequest {
|
||||
notebook = protoNotebook
|
||||
}
|
||||
client.stub.syncNotebook(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a notebook on the sync server.
|
||||
*/
|
||||
suspend fun deleteNotebook(notebookId: Long) {
|
||||
val request = deleteNotebookRequest {
|
||||
this.notebookId = notebookId
|
||||
}
|
||||
client.stub.deleteNotebook(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* List all notebooks available on the sync server.
|
||||
*/
|
||||
suspend fun listRemoteNotebooks(): List<NotebookSummary> {
|
||||
val request = listNotebooksRequest {}
|
||||
val response = client.stub.listNotebooks(request)
|
||||
return response.notebooksList
|
||||
}
|
||||
}
|
||||
|
||||
private fun millisToTimestamp(millis: Long) = timestamp {
|
||||
seconds = millis / 1000
|
||||
nanos = ((millis % 1000) * 1_000_000).toInt()
|
||||
}
|
||||
@@ -1,47 +1,27 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package engpad.v1;
|
||||
|
||||
option go_package = "git.wntrmute.dev/kyle/eng-pad-server/gen/engpad/v1;engpadv1";
|
||||
option go_package = "git.wntrmute.dev/kyle/engpad/gen/engpad/v1;engpadv1";
|
||||
option java_package = "net.metacircular.engpad.proto.v1";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
service EngPadSync {
|
||||
rpc SyncNotebook(SyncNotebookRequest) returns (SyncNotebookResponse);
|
||||
rpc DeleteNotebook(DeleteNotebookRequest) returns (DeleteNotebookResponse);
|
||||
rpc ListNotebooks(ListNotebooksRequest) returns (ListNotebooksResponse);
|
||||
rpc CreateShareLink(CreateShareLinkRequest) returns (CreateShareLinkResponse);
|
||||
rpc RevokeShareLink(RevokeShareLinkRequest) returns (RevokeShareLinkResponse);
|
||||
rpc ListShareLinks(ListShareLinksRequest) returns (ListShareLinksResponse);
|
||||
rpc SyncNotebook(SyncNotebookRequest) returns (SyncNotebookResponse);
|
||||
rpc DeleteNotebook(DeleteNotebookRequest) returns (DeleteNotebookResponse);
|
||||
rpc ListNotebooks(ListNotebooksRequest) returns (ListNotebooksResponse);
|
||||
}
|
||||
|
||||
message SyncNotebookRequest {
|
||||
int64 notebook_id = 1;
|
||||
string title = 2;
|
||||
string page_size = 3;
|
||||
repeated PageData pages = 4;
|
||||
Notebook notebook = 1;
|
||||
}
|
||||
|
||||
message PageData {
|
||||
int64 page_id = 1;
|
||||
int32 page_number = 2;
|
||||
repeated StrokeData strokes = 3;
|
||||
}
|
||||
|
||||
message StrokeData {
|
||||
float pen_size = 1;
|
||||
int32 color = 2;
|
||||
string style = 3;
|
||||
bytes point_data = 4;
|
||||
int32 stroke_order = 5;
|
||||
}
|
||||
|
||||
message SyncNotebookResponse {
|
||||
int64 server_notebook_id = 1;
|
||||
google.protobuf.Timestamp synced_at = 2;
|
||||
}
|
||||
message SyncNotebookResponse {}
|
||||
|
||||
message DeleteNotebookRequest {
|
||||
int64 notebook_id = 1;
|
||||
int64 notebook_id = 1;
|
||||
}
|
||||
|
||||
message DeleteNotebookResponse {}
|
||||
@@ -49,46 +29,42 @@ message DeleteNotebookResponse {}
|
||||
message ListNotebooksRequest {}
|
||||
|
||||
message ListNotebooksResponse {
|
||||
repeated NotebookSummary notebooks = 1;
|
||||
repeated NotebookSummary notebooks = 1;
|
||||
}
|
||||
|
||||
message Notebook {
|
||||
int64 id = 1;
|
||||
string title = 2;
|
||||
string page_size = 3;
|
||||
google.protobuf.Timestamp created_at = 4;
|
||||
google.protobuf.Timestamp updated_at = 5;
|
||||
int64 last_page_id = 6;
|
||||
repeated Page pages = 7;
|
||||
}
|
||||
|
||||
message NotebookSummary {
|
||||
int64 server_id = 1;
|
||||
int64 remote_id = 2;
|
||||
string title = 3;
|
||||
string page_size = 4;
|
||||
int32 page_count = 5;
|
||||
google.protobuf.Timestamp synced_at = 6;
|
||||
int64 id = 1;
|
||||
string title = 2;
|
||||
string page_size = 3;
|
||||
google.protobuf.Timestamp updated_at = 4;
|
||||
int32 page_count = 5;
|
||||
}
|
||||
|
||||
message CreateShareLinkRequest {
|
||||
int64 notebook_id = 1;
|
||||
int64 expires_in_seconds = 2;
|
||||
message Page {
|
||||
int64 id = 1;
|
||||
int64 notebook_id = 2;
|
||||
int32 page_number = 3;
|
||||
google.protobuf.Timestamp created_at = 4;
|
||||
repeated Stroke strokes = 5;
|
||||
}
|
||||
|
||||
message CreateShareLinkResponse {
|
||||
string token = 1;
|
||||
string url = 2;
|
||||
google.protobuf.Timestamp expires_at = 3;
|
||||
}
|
||||
|
||||
message RevokeShareLinkRequest {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message RevokeShareLinkResponse {}
|
||||
|
||||
message ListShareLinksRequest {
|
||||
int64 notebook_id = 1;
|
||||
}
|
||||
|
||||
message ListShareLinksResponse {
|
||||
repeated ShareLinkInfo links = 1;
|
||||
}
|
||||
|
||||
message ShareLinkInfo {
|
||||
string token = 1;
|
||||
string url = 2;
|
||||
google.protobuf.Timestamp created_at = 3;
|
||||
google.protobuf.Timestamp expires_at = 4;
|
||||
message Stroke {
|
||||
int64 id = 1;
|
||||
int64 page_id = 2;
|
||||
float pen_size = 3;
|
||||
int32 color = 4;
|
||||
bytes point_data = 5;
|
||||
int32 stroke_order = 6;
|
||||
google.protobuf.Timestamp created_at = 7;
|
||||
string style = 8;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user