diff --git a/api/v2/acl.go b/api/v2/acl.go index 9c6498c1..e8a5b053 100644 --- a/api/v2/acl.go +++ b/api/v2/acl.go @@ -27,6 +27,40 @@ const ( usernameContextKey ContextKey = iota ) +// Used to set modified context of ServerStream. +type WrappedStream struct { + ctx context.Context + stream grpc.ServerStream +} + +func (w *WrappedStream) RecvMsg(m any) error { + return w.stream.RecvMsg(m) +} + +func (w *WrappedStream) SendMsg(m any) error { + return w.stream.SendMsg(m) +} + +func (w *WrappedStream) SendHeader(md metadata.MD) error { + return w.stream.SendHeader(md) +} + +func (w *WrappedStream) SetHeader(md metadata.MD) error { + return w.stream.SetHeader(md) +} + +func (w *WrappedStream) SetTrailer(md metadata.MD) { + w.stream.SetTrailer(md) +} + +func (w *WrappedStream) Context() context.Context { + return w.ctx +} + +func newWrappedStream(ctx context.Context, stream grpc.ServerStream) grpc.ServerStream { + return &WrappedStream{ctx, stream} +} + // GRPCAuthInterceptor is the auth interceptor for gRPC server. type GRPCAuthInterceptor struct { Store *store.Store @@ -80,6 +114,45 @@ func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, re return handler(childCtx, request) } +func (in *GRPCAuthInterceptor) StreamAuthenticationInterceptor(srv any, stream grpc.ServerStream, serverInfo *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + md, ok := metadata.FromIncomingContext(stream.Context()) + if !ok { + return status.Errorf(codes.Unauthenticated, "failed to parse metadata from incoming context") + } + accessToken, err := getTokenFromMetadata(md) + if err != nil { + return status.Errorf(codes.Unauthenticated, err.Error()) + } + + username, err := in.authenticate(stream.Context(), accessToken) + if err != nil { + if isUnauthorizeAllowedMethod(serverInfo.FullMethod) { + return handler(stream.Context(), stream) + } + return err + } + user, err := in.Store.GetUser(stream.Context(), &store.FindUser{ + Username: &username, + }) + if err != nil { + return errors.Wrap(err, "failed to get user") + } + if user == nil { + return errors.Errorf("user %q not exists", username) + } + if user.RowStatus == store.Archived { + return errors.Errorf("user %q is archived", username) + } + if isOnlyForAdminAllowedMethod(serverInfo.FullMethod) && user.Role != store.RoleHost && user.Role != store.RoleAdmin { + return errors.Errorf("user %q is not admin", username) + } + + // Stores userID into context. + childCtx := context.WithValue(stream.Context(), usernameContextKey, username) + + return handler(srv, newWrappedStream(childCtx, stream)) +} + func (in *GRPCAuthInterceptor) authenticate(ctx context.Context, accessToken string) (string, error) { if accessToken == "" { return "", status.Errorf(codes.Unauthenticated, "access token not found") diff --git a/api/v2/memo_service.go b/api/v2/memo_service.go index 3889dda4..bf689708 100644 --- a/api/v2/memo_service.go +++ b/api/v2/memo_service.go @@ -1,6 +1,8 @@ package v2 import ( + "archive/zip" + "bytes" "context" "encoding/json" "fmt" @@ -32,6 +34,7 @@ import ( const ( DefaultPageSize = 10 MaxContentLength = 8 * 1024 + ChunkSize = 64 * 1024 // 64 KiB ) func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMemoRequest) (*apiv2pb.CreateMemoResponse, error) { @@ -100,84 +103,9 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe } func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemosRequest) (*apiv2pb.ListMemosResponse, error) { - memoFind := &store.FindMemo{ - // Exclude comments by default. - ExcludeComments: true, - } - if request.Filter != "" { - filter, err := parseListMemosFilter(request.Filter) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) - } - if len(filter.ContentSearch) > 0 { - memoFind.ContentSearch = filter.ContentSearch - } - if len(filter.Visibilities) > 0 { - memoFind.VisibilityList = filter.Visibilities - } - if filter.OrderByPinned { - memoFind.OrderByPinned = filter.OrderByPinned - } - if filter.DisplayTimeAfter != nil { - displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") - } - if displayWithUpdatedTs { - memoFind.UpdatedTsAfter = filter.DisplayTimeAfter - } else { - memoFind.CreatedTsAfter = filter.DisplayTimeAfter - } - } - if filter.DisplayTimeBefore != nil { - displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") - } - if displayWithUpdatedTs { - memoFind.UpdatedTsBefore = filter.DisplayTimeBefore - } else { - memoFind.CreatedTsBefore = filter.DisplayTimeBefore - } - } - if filter.Creator != nil { - username, err := ExtractUsernameFromName(*filter.Creator) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid creator name") - } - user, err := s.Store.GetUser(ctx, &store.FindUser{ - Username: &username, - }) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get user") - } - if user == nil { - return nil, status.Errorf(codes.NotFound, "user not found") - } - memoFind.CreatorID = &user.ID - } - if filter.RowStatus != nil { - memoFind.RowStatus = filter.RowStatus - } - } else { - return nil, status.Errorf(codes.InvalidArgument, "filter is required") - } - - user, _ := getCurrentUser(ctx, s.Store) - // If the user is not authenticated, only public memos are visible. - if user == nil { - memoFind.VisibilityList = []store.Visibility{store.Public} - } - if user != nil && memoFind.CreatorID != nil && *memoFind.CreatorID != user.ID { - memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} - } - - displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) + memoFind, err := s.buildFindMemosWithFilter(ctx, request.Filter, true) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") - } - if displayWithUpdatedTs { - memoFind.OrderByUpdatedTs = true + return nil, err } var limit, offset int @@ -621,6 +549,61 @@ func (s *APIV2Service) GetUserMemosStats(ctx context.Context, request *apiv2pb.G return response, nil } +func (s *APIV2Service) ExportMemos(request *apiv2pb.ExportMemosRequest, srv apiv2pb.MemoService_ExportMemosServer) error { + ctx := srv.Context() + fmt.Printf("%+v\n", ctx) + memoFind, err := s.buildFindMemosWithFilter(ctx, request.Filter, true) + if err != nil { + return err + } + + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return err + } + + buf := new(bytes.Buffer) + writer := zip.NewWriter(buf) + + for _, memo := range memos { + memoMessage, err := s.convertMemoFromStore(ctx, memo) + log.Info(memoMessage.Content) + if err != nil { + return errors.Wrap(err, "failed to convert memo") + } + file, err := writer.Create(time.Unix(memo.CreatedTs, 0).Format(time.RFC3339) + ".md") + if err != nil { + return status.Errorf(codes.Internal, "Failed to create memo file") + } + _, err = file.Write([]byte(memoMessage.Content)) + if err != nil { + return status.Errorf(codes.Internal, "Failed to write to memo file") + } + } + + err = writer.Close() + if err != nil { + return status.Errorf(codes.Internal, "Failed to close zip file writer") + } + + exportChunk := &apiv2pb.ExportMemosResponse{} + sizeOfFile := len(buf.Bytes()) + for currentByte := 0; currentByte < sizeOfFile; currentByte += ChunkSize { + if currentByte+ChunkSize > sizeOfFile { + exportChunk.File = buf.Bytes()[currentByte:sizeOfFile] + } else { + exportChunk.File = buf.Bytes()[currentByte : currentByte+ChunkSize] + } + + err := srv.Send(exportChunk) + if err != nil { + return status.Error(codes.Internal, "Unable to stream ExportMemosResponse chunk") + } + } + + return nil +} + func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*apiv2pb.Memo, error) { rawNodes, err := parser.Parse(tokenizer.Tokenize(memo.Content)) if err != nil { @@ -847,6 +830,90 @@ func (s *APIV2Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *api return nil } +func (s *APIV2Service) buildFindMemosWithFilter(ctx context.Context, filter string, excludeComments bool) (*store.FindMemo, error) { + memoFind := &store.FindMemo{ + // Exclude comments by default. + ExcludeComments: excludeComments, + } + if filter != "" { + filter, err := parseListMemosFilter(filter) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) + } + if len(filter.ContentSearch) > 0 { + memoFind.ContentSearch = filter.ContentSearch + } + if len(filter.Visibilities) > 0 { + memoFind.VisibilityList = filter.Visibilities + } + if filter.OrderByPinned { + memoFind.OrderByPinned = filter.OrderByPinned + } + if filter.DisplayTimeAfter != nil { + displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") + } + if displayWithUpdatedTs { + memoFind.UpdatedTsAfter = filter.DisplayTimeAfter + } else { + memoFind.CreatedTsAfter = filter.DisplayTimeAfter + } + } + if filter.DisplayTimeBefore != nil { + displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") + } + if displayWithUpdatedTs { + memoFind.UpdatedTsBefore = filter.DisplayTimeBefore + } else { + memoFind.CreatedTsBefore = filter.DisplayTimeBefore + } + } + if filter.Creator != nil { + username, err := ExtractUsernameFromName(*filter.Creator) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid creator name") + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &username, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + memoFind.CreatorID = &user.ID + } + if filter.RowStatus != nil { + memoFind.RowStatus = filter.RowStatus + } + } else { + return nil, status.Errorf(codes.InvalidArgument, "filter is required") + } + + user, _ := getCurrentUser(ctx, s.Store) + // If the user is not authenticated, only public memos are visible. + if user == nil { + memoFind.VisibilityList = []store.Visibility{store.Public} + } + if user != nil && memoFind.CreatorID != nil && *memoFind.CreatorID != user.ID { + memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} + } + + displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") + } + if displayWithUpdatedTs { + memoFind.OrderByUpdatedTs = true + } + + return memoFind, nil +} + func convertMemoToWebhookPayload(memo *apiv2pb.Memo) *webhook.WebhookPayload { return &webhook.WebhookPayload{ CreatorID: memo.CreatorId, diff --git a/api/v2/v2.go b/api/v2/v2.go index 9a54a72b..b4ec4160 100644 --- a/api/v2/v2.go +++ b/api/v2/v2.go @@ -47,6 +47,9 @@ func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store grpc.ChainUnaryInterceptor( authProvider.AuthenticationInterceptor, ), + grpc.ChainStreamInterceptor( + authProvider.StreamAuthenticationInterceptor, + ), ) apiv2Service := &APIV2Service{ Secret: secret, diff --git a/proto/api/v2/memo_service.proto b/proto/api/v2/memo_service.proto index dfda692d..988fd369 100644 --- a/proto/api/v2/memo_service.proto +++ b/proto/api/v2/memo_service.proto @@ -90,6 +90,10 @@ service MemoService { option (google.api.http) = {get: "/api/v2/memos/stats"}; option (google.api.method_signature) = "username"; } + + rpc ExportMemos(ExportMemosRequest) returns (stream ExportMemosResponse) { + option (google.api.http) = {get: "/api/v2/memos/export"}; + } } enum Visibility { @@ -273,3 +277,12 @@ message GetUserMemosStatsResponse { // key is the year-month-day string. e.g. "2020-01-01". map stats = 1; } + +message ExportMemosRequest { + // Same as ListMemosRequest.filter + string filter = 1; +} + +message ExportMemosResponse { + bytes file = 1; +} diff --git a/proto/gen/api/v2/README.md b/proto/gen/api/v2/README.md index 05f1bfc9..6a1a4fc5 100644 --- a/proto/gen/api/v2/README.md +++ b/proto/gen/api/v2/README.md @@ -142,6 +142,8 @@ - [CreateMemoResponse](#memos-api-v2-CreateMemoResponse) - [DeleteMemoRequest](#memos-api-v2-DeleteMemoRequest) - [DeleteMemoResponse](#memos-api-v2-DeleteMemoResponse) + - [ExportMemosRequest](#memos-api-v2-ExportMemosRequest) + - [ExportMemosResponse](#memos-api-v2-ExportMemosResponse) - [GetMemoByNameRequest](#memos-api-v2-GetMemoByNameRequest) - [GetMemoByNameResponse](#memos-api-v2-GetMemoByNameResponse) - [GetMemoRequest](#memos-api-v2-GetMemoRequest) @@ -2066,6 +2068,36 @@ Used internally for obfuscating the page token. + + +### ExportMemosRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| filter | [string](#string) | | Same as ListMemosRequest.filter | + + + + + + + + +### ExportMemosResponse + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| file | [bytes](#bytes) | | | + + + + + + ### GetMemoByNameRequest @@ -2450,6 +2482,7 @@ Used internally for obfuscating the page token. | CreateMemoComment | [CreateMemoCommentRequest](#memos-api-v2-CreateMemoCommentRequest) | [CreateMemoCommentResponse](#memos-api-v2-CreateMemoCommentResponse) | CreateMemoComment creates a comment for a memo. | | ListMemoComments | [ListMemoCommentsRequest](#memos-api-v2-ListMemoCommentsRequest) | [ListMemoCommentsResponse](#memos-api-v2-ListMemoCommentsResponse) | ListMemoComments lists comments for a memo. | | GetUserMemosStats | [GetUserMemosStatsRequest](#memos-api-v2-GetUserMemosStatsRequest) | [GetUserMemosStatsResponse](#memos-api-v2-GetUserMemosStatsResponse) | GetUserMemosStats gets stats of memos for a user. | +| ExportMemos | [ExportMemosRequest](#memos-api-v2-ExportMemosRequest) | [ExportMemosResponse](#memos-api-v2-ExportMemosResponse) stream | | diff --git a/proto/gen/api/v2/memo_service.pb.go b/proto/gen/api/v2/memo_service.pb.go index cd3e5cc7..89a30d24 100644 --- a/proto/gen/api/v2/memo_service.pb.go +++ b/proto/gen/api/v2/memo_service.pb.go @@ -1537,6 +1537,101 @@ func (x *GetUserMemosStatsResponse) GetStats() map[string]int32 { return nil } +type ExportMemosRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Same as ListMemosRequest.filter + Filter string `protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"` +} + +func (x *ExportMemosRequest) Reset() { + *x = ExportMemosRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_v2_memo_service_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExportMemosRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportMemosRequest) ProtoMessage() {} + +func (x *ExportMemosRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_memo_service_proto_msgTypes[27] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportMemosRequest.ProtoReflect.Descriptor instead. +func (*ExportMemosRequest) Descriptor() ([]byte, []int) { + return file_api_v2_memo_service_proto_rawDescGZIP(), []int{27} +} + +func (x *ExportMemosRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +type ExportMemosResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + File []byte `protobuf:"bytes,1,opt,name=file,proto3" json:"file,omitempty"` +} + +func (x *ExportMemosResponse) Reset() { + *x = ExportMemosResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_v2_memo_service_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExportMemosResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportMemosResponse) ProtoMessage() {} + +func (x *ExportMemosResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_memo_service_proto_msgTypes[28] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportMemosResponse.ProtoReflect.Descriptor instead. +func (*ExportMemosResponse) Descriptor() ([]byte, []int) { + return file_api_v2_memo_service_proto_rawDescGZIP(), []int{28} +} + +func (x *ExportMemosResponse) GetFile() []byte { + if x != nil { + return x.File + } + return nil +} + var File_api_v2_memo_service_proto protoreflect.FileDescriptor var file_api_v2_memo_service_proto_rawDesc = []byte{ @@ -1719,131 +1814,144 @@ var file_api_v2_memo_service_proto_rawDesc = []byte{ 0x74, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x50, 0x0a, 0x0a, - 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x1a, 0x0a, 0x16, 0x56, 0x49, - 0x53, 0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x49, 0x56, 0x41, 0x54, - 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, 0x4f, 0x54, 0x45, 0x43, 0x54, 0x45, 0x44, - 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x03, 0x32, 0xb0, - 0x0d, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x69, - 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, - 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, - 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x63, 0x0a, 0x09, 0x4c, 0x69, 0x73, - 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x1e, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, - 0x0d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x67, - 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1c, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, - 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, - 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x7d, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4d, 0x65, - 0x6d, 0x6f, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x42, - 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, - 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, - 0x65, 0x6d, 0x6f, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x23, 0xda, 0x41, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, - 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, - 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x80, 0x01, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x05, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x2c, 0x0a, 0x12, + 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x29, 0x0a, 0x13, 0x45, 0x78, + 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x66, 0x69, 0x6c, 0x65, 0x2a, 0x50, 0x0a, 0x0a, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x79, 0x12, 0x1a, 0x0a, 0x16, 0x56, 0x49, 0x53, 0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, + 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x49, 0x56, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, + 0x50, 0x52, 0x4f, 0x54, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, + 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x03, 0x32, 0xa4, 0x0e, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x69, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, + 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2f, 0xda, 0x41, 0x0f, 0x69, 0x64, 0x2c, - 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x32, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, - 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x70, 0x0a, 0x0a, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, - 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, - 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0xda, 0x41, 0x02, 0x69, - 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x2a, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, - 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x8f, 0x01, 0x0a, 0x10, - 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, - 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x2c, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, - 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, - 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x8f, 0x01, - 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, - 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, - 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, - 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, - 0x8f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, - 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, - 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, - 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, - 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, - 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, - 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x12, 0x8e, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, - 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, - 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, - 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0xda, 0x41, 0x02, 0x69, - 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, - 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x12, 0x8b, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, - 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, - 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, - 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, + 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, + 0x6f, 0x73, 0x12, 0x63, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x12, + 0x1e, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, + 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x67, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4d, 0x65, + 0x6d, 0x6f, 0x12, 0x1c, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, + 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x1f, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, + 0x12, 0x7d, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x42, 0x79, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, + 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x42, 0x79, 0x4e, 0x61, + 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, 0xda, 0x41, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, + 0x80, 0x01, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, + 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x2f, 0xda, 0x41, 0x0f, 0x69, 0x64, 0x2c, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x32, 0x12, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, + 0x64, 0x7d, 0x12, 0x70, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, + 0x12, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x1f, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, + 0x2a, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, + 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x8f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, + 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, - 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, - 0x74, 0x73, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x65, - 0x6d, 0x6f, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, - 0x65, 0x6d, 0x6f, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, + 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, + 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, + 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x6d, + 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0xda, + 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, + 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e, + 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, + 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, + 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, + 0x2f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, + 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, + 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x29, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, + 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8e, 0x01, 0x0a, + 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, + 0x6e, 0x74, 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, + 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x28, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, + 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, + 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x8b, 0x01, + 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, + 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, + 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x28, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x1b, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, + 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0xda, 0x41, 0x08, 0x75, 0x73, - 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x73, 0x74, 0x61, 0x74, - 0x73, 0x42, 0xa8, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x10, 0x4d, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, - 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03, 0x4d, - 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56, - 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, - 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0x5c, - 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x65, - 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, + 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x53, 0x74, 0x61, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, + 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x26, 0xda, 0x41, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, + 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x72, 0x0a, 0x0b, 0x45, 0x78, + 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, + 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x4d, + 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6d, 0x65, + 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, + 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, + 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x30, 0x01, 0x42, 0xa8, + 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x32, 0x42, 0x10, 0x4d, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, + 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03, 0x4d, 0x41, 0x58, 0xaa, + 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, + 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, + 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x65, 0x6d, 0x6f, 0x73, + 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -1859,7 +1967,7 @@ func file_api_v2_memo_service_proto_rawDescGZIP() []byte { } var file_api_v2_memo_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_api_v2_memo_service_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_api_v2_memo_service_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_api_v2_memo_service_proto_goTypes = []interface{}{ (Visibility)(0), // 0: memos.api.v2.Visibility (*Memo)(nil), // 1: memos.api.v2.Memo @@ -1889,39 +1997,41 @@ var file_api_v2_memo_service_proto_goTypes = []interface{}{ (*ListMemoCommentsResponse)(nil), // 25: memos.api.v2.ListMemoCommentsResponse (*GetUserMemosStatsRequest)(nil), // 26: memos.api.v2.GetUserMemosStatsRequest (*GetUserMemosStatsResponse)(nil), // 27: memos.api.v2.GetUserMemosStatsResponse - nil, // 28: memos.api.v2.GetUserMemosStatsResponse.StatsEntry - (RowStatus)(0), // 29: memos.api.v2.RowStatus - (*timestamppb.Timestamp)(nil), // 30: google.protobuf.Timestamp - (*Node)(nil), // 31: memos.api.v2.Node - (*Resource)(nil), // 32: memos.api.v2.Resource - (*MemoRelation)(nil), // 33: memos.api.v2.MemoRelation - (*fieldmaskpb.FieldMask)(nil), // 34: google.protobuf.FieldMask + (*ExportMemosRequest)(nil), // 28: memos.api.v2.ExportMemosRequest + (*ExportMemosResponse)(nil), // 29: memos.api.v2.ExportMemosResponse + nil, // 30: memos.api.v2.GetUserMemosStatsResponse.StatsEntry + (RowStatus)(0), // 31: memos.api.v2.RowStatus + (*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp + (*Node)(nil), // 33: memos.api.v2.Node + (*Resource)(nil), // 34: memos.api.v2.Resource + (*MemoRelation)(nil), // 35: memos.api.v2.MemoRelation + (*fieldmaskpb.FieldMask)(nil), // 36: google.protobuf.FieldMask } var file_api_v2_memo_service_proto_depIdxs = []int32{ - 29, // 0: memos.api.v2.Memo.row_status:type_name -> memos.api.v2.RowStatus - 30, // 1: memos.api.v2.Memo.create_time:type_name -> google.protobuf.Timestamp - 30, // 2: memos.api.v2.Memo.update_time:type_name -> google.protobuf.Timestamp - 30, // 3: memos.api.v2.Memo.display_time:type_name -> google.protobuf.Timestamp - 31, // 4: memos.api.v2.Memo.nodes:type_name -> memos.api.v2.Node + 31, // 0: memos.api.v2.Memo.row_status:type_name -> memos.api.v2.RowStatus + 32, // 1: memos.api.v2.Memo.create_time:type_name -> google.protobuf.Timestamp + 32, // 2: memos.api.v2.Memo.update_time:type_name -> google.protobuf.Timestamp + 32, // 3: memos.api.v2.Memo.display_time:type_name -> google.protobuf.Timestamp + 33, // 4: memos.api.v2.Memo.nodes:type_name -> memos.api.v2.Node 0, // 5: memos.api.v2.Memo.visibility:type_name -> memos.api.v2.Visibility - 32, // 6: memos.api.v2.Memo.resources:type_name -> memos.api.v2.Resource - 33, // 7: memos.api.v2.Memo.relations:type_name -> memos.api.v2.MemoRelation + 34, // 6: memos.api.v2.Memo.resources:type_name -> memos.api.v2.Resource + 35, // 7: memos.api.v2.Memo.relations:type_name -> memos.api.v2.MemoRelation 0, // 8: memos.api.v2.CreateMemoRequest.visibility:type_name -> memos.api.v2.Visibility 1, // 9: memos.api.v2.CreateMemoResponse.memo:type_name -> memos.api.v2.Memo 1, // 10: memos.api.v2.ListMemosResponse.memos:type_name -> memos.api.v2.Memo 1, // 11: memos.api.v2.GetMemoResponse.memo:type_name -> memos.api.v2.Memo 1, // 12: memos.api.v2.GetMemoByNameResponse.memo:type_name -> memos.api.v2.Memo 1, // 13: memos.api.v2.UpdateMemoRequest.memo:type_name -> memos.api.v2.Memo - 34, // 14: memos.api.v2.UpdateMemoRequest.update_mask:type_name -> google.protobuf.FieldMask + 36, // 14: memos.api.v2.UpdateMemoRequest.update_mask:type_name -> google.protobuf.FieldMask 1, // 15: memos.api.v2.UpdateMemoResponse.memo:type_name -> memos.api.v2.Memo - 32, // 16: memos.api.v2.SetMemoResourcesRequest.resources:type_name -> memos.api.v2.Resource - 32, // 17: memos.api.v2.ListMemoResourcesResponse.resources:type_name -> memos.api.v2.Resource - 33, // 18: memos.api.v2.SetMemoRelationsRequest.relations:type_name -> memos.api.v2.MemoRelation - 33, // 19: memos.api.v2.ListMemoRelationsResponse.relations:type_name -> memos.api.v2.MemoRelation + 34, // 16: memos.api.v2.SetMemoResourcesRequest.resources:type_name -> memos.api.v2.Resource + 34, // 17: memos.api.v2.ListMemoResourcesResponse.resources:type_name -> memos.api.v2.Resource + 35, // 18: memos.api.v2.SetMemoRelationsRequest.relations:type_name -> memos.api.v2.MemoRelation + 35, // 19: memos.api.v2.ListMemoRelationsResponse.relations:type_name -> memos.api.v2.MemoRelation 2, // 20: memos.api.v2.CreateMemoCommentRequest.create:type_name -> memos.api.v2.CreateMemoRequest 1, // 21: memos.api.v2.CreateMemoCommentResponse.memo:type_name -> memos.api.v2.Memo 1, // 22: memos.api.v2.ListMemoCommentsResponse.memos:type_name -> memos.api.v2.Memo - 28, // 23: memos.api.v2.GetUserMemosStatsResponse.stats:type_name -> memos.api.v2.GetUserMemosStatsResponse.StatsEntry + 30, // 23: memos.api.v2.GetUserMemosStatsResponse.stats:type_name -> memos.api.v2.GetUserMemosStatsResponse.StatsEntry 2, // 24: memos.api.v2.MemoService.CreateMemo:input_type -> memos.api.v2.CreateMemoRequest 4, // 25: memos.api.v2.MemoService.ListMemos:input_type -> memos.api.v2.ListMemosRequest 6, // 26: memos.api.v2.MemoService.GetMemo:input_type -> memos.api.v2.GetMemoRequest @@ -1935,21 +2045,23 @@ var file_api_v2_memo_service_proto_depIdxs = []int32{ 22, // 34: memos.api.v2.MemoService.CreateMemoComment:input_type -> memos.api.v2.CreateMemoCommentRequest 24, // 35: memos.api.v2.MemoService.ListMemoComments:input_type -> memos.api.v2.ListMemoCommentsRequest 26, // 36: memos.api.v2.MemoService.GetUserMemosStats:input_type -> memos.api.v2.GetUserMemosStatsRequest - 3, // 37: memos.api.v2.MemoService.CreateMemo:output_type -> memos.api.v2.CreateMemoResponse - 5, // 38: memos.api.v2.MemoService.ListMemos:output_type -> memos.api.v2.ListMemosResponse - 7, // 39: memos.api.v2.MemoService.GetMemo:output_type -> memos.api.v2.GetMemoResponse - 9, // 40: memos.api.v2.MemoService.GetMemoByName:output_type -> memos.api.v2.GetMemoByNameResponse - 11, // 41: memos.api.v2.MemoService.UpdateMemo:output_type -> memos.api.v2.UpdateMemoResponse - 13, // 42: memos.api.v2.MemoService.DeleteMemo:output_type -> memos.api.v2.DeleteMemoResponse - 15, // 43: memos.api.v2.MemoService.SetMemoResources:output_type -> memos.api.v2.SetMemoResourcesResponse - 17, // 44: memos.api.v2.MemoService.ListMemoResources:output_type -> memos.api.v2.ListMemoResourcesResponse - 19, // 45: memos.api.v2.MemoService.SetMemoRelations:output_type -> memos.api.v2.SetMemoRelationsResponse - 21, // 46: memos.api.v2.MemoService.ListMemoRelations:output_type -> memos.api.v2.ListMemoRelationsResponse - 23, // 47: memos.api.v2.MemoService.CreateMemoComment:output_type -> memos.api.v2.CreateMemoCommentResponse - 25, // 48: memos.api.v2.MemoService.ListMemoComments:output_type -> memos.api.v2.ListMemoCommentsResponse - 27, // 49: memos.api.v2.MemoService.GetUserMemosStats:output_type -> memos.api.v2.GetUserMemosStatsResponse - 37, // [37:50] is the sub-list for method output_type - 24, // [24:37] is the sub-list for method input_type + 28, // 37: memos.api.v2.MemoService.ExportMemos:input_type -> memos.api.v2.ExportMemosRequest + 3, // 38: memos.api.v2.MemoService.CreateMemo:output_type -> memos.api.v2.CreateMemoResponse + 5, // 39: memos.api.v2.MemoService.ListMemos:output_type -> memos.api.v2.ListMemosResponse + 7, // 40: memos.api.v2.MemoService.GetMemo:output_type -> memos.api.v2.GetMemoResponse + 9, // 41: memos.api.v2.MemoService.GetMemoByName:output_type -> memos.api.v2.GetMemoByNameResponse + 11, // 42: memos.api.v2.MemoService.UpdateMemo:output_type -> memos.api.v2.UpdateMemoResponse + 13, // 43: memos.api.v2.MemoService.DeleteMemo:output_type -> memos.api.v2.DeleteMemoResponse + 15, // 44: memos.api.v2.MemoService.SetMemoResources:output_type -> memos.api.v2.SetMemoResourcesResponse + 17, // 45: memos.api.v2.MemoService.ListMemoResources:output_type -> memos.api.v2.ListMemoResourcesResponse + 19, // 46: memos.api.v2.MemoService.SetMemoRelations:output_type -> memos.api.v2.SetMemoRelationsResponse + 21, // 47: memos.api.v2.MemoService.ListMemoRelations:output_type -> memos.api.v2.ListMemoRelationsResponse + 23, // 48: memos.api.v2.MemoService.CreateMemoComment:output_type -> memos.api.v2.CreateMemoCommentResponse + 25, // 49: memos.api.v2.MemoService.ListMemoComments:output_type -> memos.api.v2.ListMemoCommentsResponse + 27, // 50: memos.api.v2.MemoService.GetUserMemosStats:output_type -> memos.api.v2.GetUserMemosStatsResponse + 29, // 51: memos.api.v2.MemoService.ExportMemos:output_type -> memos.api.v2.ExportMemosResponse + 38, // [38:52] is the sub-list for method output_type + 24, // [24:38] is the sub-list for method input_type 24, // [24:24] is the sub-list for extension type_name 24, // [24:24] is the sub-list for extension extendee 0, // [0:24] is the sub-list for field type_name @@ -2289,6 +2401,30 @@ func file_api_v2_memo_service_proto_init() { return nil } } + file_api_v2_memo_service_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExportMemosRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_v2_memo_service_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExportMemosResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_api_v2_memo_service_proto_msgTypes[0].OneofWrappers = []interface{}{} type x struct{} @@ -2297,7 +2433,7 @@ func file_api_v2_memo_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_api_v2_memo_service_proto_rawDesc, NumEnums: 1, - NumMessages: 28, + NumMessages: 30, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/gen/api/v2/memo_service.pb.gw.go b/proto/gen/api/v2/memo_service.pb.gw.go index 0940a8f5..d46ad1f0 100644 --- a/proto/gen/api/v2/memo_service.pb.gw.go +++ b/proto/gen/api/v2/memo_service.pb.gw.go @@ -723,6 +723,34 @@ func local_request_MemoService_GetUserMemosStats_0(ctx context.Context, marshale } +var ( + filter_MemoService_ExportMemos_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_MemoService_ExportMemos_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (MemoService_ExportMemosClient, runtime.ServerMetadata, error) { + var protoReq ExportMemosRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ExportMemos_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.ExportMemos(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + // RegisterMemoServiceHandlerServer registers the http handlers for service MemoService to "mux". // UnaryRPC :call MemoServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -1054,6 +1082,13 @@ func RegisterMemoServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux }) + mux.Handle("GET", pattern_MemoService_ExportMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + return nil } @@ -1381,6 +1416,28 @@ func RegisterMemoServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux }) + mux.Handle("GET", pattern_MemoService_ExportMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/memos.api.v2.MemoService/ExportMemos", runtime.WithHTTPPathPattern("/api/v2/memos/export")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_ExportMemos_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_MemoService_ExportMemos_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -1410,6 +1467,8 @@ var ( pattern_MemoService_ListMemoComments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v2", "memos", "id", "comments"}, "")) pattern_MemoService_GetUserMemosStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v2", "memos", "stats"}, "")) + + pattern_MemoService_ExportMemos_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v2", "memos", "export"}, "")) ) var ( @@ -1438,4 +1497,6 @@ var ( forward_MemoService_ListMemoComments_0 = runtime.ForwardResponseMessage forward_MemoService_GetUserMemosStats_0 = runtime.ForwardResponseMessage + + forward_MemoService_ExportMemos_0 = runtime.ForwardResponseStream ) diff --git a/proto/gen/api/v2/memo_service_grpc.pb.go b/proto/gen/api/v2/memo_service_grpc.pb.go index a9639bfd..380e3721 100644 --- a/proto/gen/api/v2/memo_service_grpc.pb.go +++ b/proto/gen/api/v2/memo_service_grpc.pb.go @@ -32,6 +32,7 @@ const ( MemoService_CreateMemoComment_FullMethodName = "/memos.api.v2.MemoService/CreateMemoComment" MemoService_ListMemoComments_FullMethodName = "/memos.api.v2.MemoService/ListMemoComments" MemoService_GetUserMemosStats_FullMethodName = "/memos.api.v2.MemoService/GetUserMemosStats" + MemoService_ExportMemos_FullMethodName = "/memos.api.v2.MemoService/ExportMemos" ) // MemoServiceClient is the client API for MemoService service. @@ -64,6 +65,7 @@ type MemoServiceClient interface { ListMemoComments(ctx context.Context, in *ListMemoCommentsRequest, opts ...grpc.CallOption) (*ListMemoCommentsResponse, error) // GetUserMemosStats gets stats of memos for a user. GetUserMemosStats(ctx context.Context, in *GetUserMemosStatsRequest, opts ...grpc.CallOption) (*GetUserMemosStatsResponse, error) + ExportMemos(ctx context.Context, in *ExportMemosRequest, opts ...grpc.CallOption) (MemoService_ExportMemosClient, error) } type memoServiceClient struct { @@ -191,6 +193,38 @@ func (c *memoServiceClient) GetUserMemosStats(ctx context.Context, in *GetUserMe return out, nil } +func (c *memoServiceClient) ExportMemos(ctx context.Context, in *ExportMemosRequest, opts ...grpc.CallOption) (MemoService_ExportMemosClient, error) { + stream, err := c.cc.NewStream(ctx, &MemoService_ServiceDesc.Streams[0], MemoService_ExportMemos_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &memoServiceExportMemosClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type MemoService_ExportMemosClient interface { + Recv() (*ExportMemosResponse, error) + grpc.ClientStream +} + +type memoServiceExportMemosClient struct { + grpc.ClientStream +} + +func (x *memoServiceExportMemosClient) Recv() (*ExportMemosResponse, error) { + m := new(ExportMemosResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // MemoServiceServer is the server API for MemoService service. // All implementations must embed UnimplementedMemoServiceServer // for forward compatibility @@ -221,6 +255,7 @@ type MemoServiceServer interface { ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error) // GetUserMemosStats gets stats of memos for a user. GetUserMemosStats(context.Context, *GetUserMemosStatsRequest) (*GetUserMemosStatsResponse, error) + ExportMemos(*ExportMemosRequest, MemoService_ExportMemosServer) error mustEmbedUnimplementedMemoServiceServer() } @@ -267,6 +302,9 @@ func (UnimplementedMemoServiceServer) ListMemoComments(context.Context, *ListMem func (UnimplementedMemoServiceServer) GetUserMemosStats(context.Context, *GetUserMemosStatsRequest) (*GetUserMemosStatsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetUserMemosStats not implemented") } +func (UnimplementedMemoServiceServer) ExportMemos(*ExportMemosRequest, MemoService_ExportMemosServer) error { + return status.Errorf(codes.Unimplemented, "method ExportMemos not implemented") +} func (UnimplementedMemoServiceServer) mustEmbedUnimplementedMemoServiceServer() {} // UnsafeMemoServiceServer may be embedded to opt out of forward compatibility for this service. @@ -514,6 +552,27 @@ func _MemoService_GetUserMemosStats_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _MemoService_ExportMemos_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(ExportMemosRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(MemoServiceServer).ExportMemos(m, &memoServiceExportMemosServer{stream}) +} + +type MemoService_ExportMemosServer interface { + Send(*ExportMemosResponse) error + grpc.ServerStream +} + +type memoServiceExportMemosServer struct { + grpc.ServerStream +} + +func (x *memoServiceExportMemosServer) Send(m *ExportMemosResponse) error { + return x.ServerStream.SendMsg(m) +} + // MemoService_ServiceDesc is the grpc.ServiceDesc for MemoService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -574,6 +633,12 @@ var MemoService_ServiceDesc = grpc.ServiceDesc{ Handler: _MemoService_GetUserMemosStats_Handler, }, }, - Streams: []grpc.StreamDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "ExportMemos", + Handler: _MemoService_ExportMemos_Handler, + ServerStreams: true, + }, + }, Metadata: "api/v2/memo_service.proto", } diff --git a/web/src/components/Settings/MyAccountSection.tsx b/web/src/components/Settings/MyAccountSection.tsx index 53c4ae34..5830b6fc 100644 --- a/web/src/components/Settings/MyAccountSection.tsx +++ b/web/src/components/Settings/MyAccountSection.tsx @@ -1,4 +1,6 @@ import { Button } from "@mui/joy"; +import { memoServiceClient } from "@/grpcweb"; +import { downloadFileFromUrl } from "@/helpers/utils"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useTranslate } from "@/utils/i18n"; import showChangePasswordDialog from "../ChangePasswordDialog"; @@ -10,6 +12,17 @@ const MyAccountSection = () => { const t = useTranslate(); const user = useCurrentUser(); + const downloadExportedMemos = async (user: any) => { + const chunks = []; + for await (const response of memoServiceClient.exportMemos({ filter: `creator == "${user.name}"` })) { + chunks.push(response.file.buffer); + } + const blob = new Blob(chunks); + const downloadUrl = window.URL.createObjectURL(blob); + downloadFileFromUrl(downloadUrl, "memos-export.zip"); + URL.revokeObjectURL(downloadUrl); + }; + return (

{t("setting.account-section.title")}

@@ -27,6 +40,9 @@ const MyAccountSection = () => { +
diff --git a/web/src/components/ShareMemoDialog.tsx b/web/src/components/ShareMemoDialog.tsx index e3aa1a44..45a26708 100644 --- a/web/src/components/ShareMemoDialog.tsx +++ b/web/src/components/ShareMemoDialog.tsx @@ -3,6 +3,7 @@ import copy from "copy-to-clipboard"; import React, { useEffect, useRef } from "react"; import { toast } from "react-hot-toast"; import { getDateTimeString } from "@/helpers/datetime"; +import { downloadFileFromUrl } from "@/helpers/utils"; import useLoading from "@/hooks/useLoading"; import toImage from "@/labs/html2image"; import { useUserStore, extractUsernameFromName } from "@/store/v1"; @@ -51,6 +52,7 @@ const ShareMemoDialog: React.FC = (props: Props) => { .then((url) => { downloadFileFromUrl(url, `memos-${getDateTimeString(Date.now())}.png`); downloadingImageState.setFinish(); + URL.revokeObjectURL(url); }) .catch((err) => { console.error(err); @@ -59,14 +61,9 @@ const ShareMemoDialog: React.FC = (props: Props) => { const handleDownloadTextFileBtnClick = () => { const blob = new Blob([memo.content], { type: "text/plain;charset=utf-8" }); - downloadFileFromUrl(URL.createObjectURL(blob), `memos-${getDateTimeString(Date.now())}.md`); - }; - - const downloadFileFromUrl = (url: string, filename: string) => { - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); + const url = URL.createObjectURL(blob); + downloadFileFromUrl(url, `memos-${getDateTimeString(Date.now())}.md`); + URL.revokeObjectURL(url); }; const handleCopyLinkBtnClick = () => { diff --git a/web/src/helpers/utils.ts b/web/src/helpers/utils.ts index 8ed11e6d..56795382 100644 --- a/web/src/helpers/utils.ts +++ b/web/src/helpers/utils.ts @@ -92,3 +92,11 @@ export const isValidUrl = (url: string): boolean => { return false; } }; + +export const downloadFileFromUrl = (url: string, filename: string) => { + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + a.remove(); +}; diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 41773d29..d9af7ccd 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -177,6 +177,7 @@ "email-note": "Optional", "update-information": "Update Information", "change-password": "Change password", + "export-memos": "Export Memos", "reset-api": "Reset API", "openapi-title": "OpenAPI", "openapi-reset": "Reset OpenAPI Key", diff --git a/web/src/locales/es.json b/web/src/locales/es.json index 82c6e027..c1cbc0a6 100644 --- a/web/src/locales/es.json +++ b/web/src/locales/es.json @@ -92,7 +92,8 @@ "account-section": { "title": "Información de la Cuenta", "update-information": "Actualizar Información", - "change-password": "Cambiar Contraseña" + "change-password": "Cambiar Contraseña", + "export-memos": "Exportar Notas" }, "preference-section": { "theme": "Tema",