diff --git a/proto/api/v1/instance_service.proto b/proto/api/v1/instance_service.proto index fb92b96ce..aa3cdd7a2 100644 --- a/proto/api/v1/instance_service.proto +++ b/proto/api/v1/instance_service.proto @@ -9,6 +9,7 @@ import "google/api/field_behavior.proto"; import "google/api/resource.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; import "google/type/color.proto"; option go_package = "gen/api/v1"; @@ -41,6 +42,11 @@ service InstanceService { body: "*" }; } + + // GetInstanceStats returns resource usage statistics for the instance. Admin only. + rpc GetInstanceStats(GetInstanceStatsRequest) returns (InstanceStats) { + option (google.api.http) = {get: "/api/v1/instance/stats"}; + } } // Instance profile message containing basic instance information. @@ -272,3 +278,23 @@ message TestInstanceEmailSettingRequest { // Optional. Recipient email address. If omitted, the current user's email address is used. string recipient_email = 2 [(google.api.field_behavior) = OPTIONAL]; } + +// Request message for GetInstanceStats. +message GetInstanceStatsRequest {} + +// Resource usage statistics for the instance. +message InstanceStats { + DatabaseStats database = 1; + // Recursive size of the data directory in bytes. -1 if unavailable. + int64 local_storage_bytes = 2; + // Server-side timestamp when the snapshot was generated. + google.protobuf.Timestamp generated_time = 4; + + // Database size statistics. + message DatabaseStats { + // driver is one of "sqlite", "mysql", "postgres". + string driver = 1; + // size_bytes is the database size in bytes; -1 if unavailable. + int64 size_bytes = 2; + } +} diff --git a/proto/gen/api/v1/apiv1connect/instance_service.connect.go b/proto/gen/api/v1/apiv1connect/instance_service.connect.go index 0376c2c55..5cf6835ab 100644 --- a/proto/gen/api/v1/apiv1connect/instance_service.connect.go +++ b/proto/gen/api/v1/apiv1connect/instance_service.connect.go @@ -46,6 +46,9 @@ const ( // InstanceServiceTestInstanceEmailSettingProcedure is the fully-qualified name of the // InstanceService's TestInstanceEmailSetting RPC. InstanceServiceTestInstanceEmailSettingProcedure = "/memos.api.v1.InstanceService/TestInstanceEmailSetting" + // InstanceServiceGetInstanceStatsProcedure is the fully-qualified name of the InstanceService's + // GetInstanceStats RPC. + InstanceServiceGetInstanceStatsProcedure = "/memos.api.v1.InstanceService/GetInstanceStats" ) // InstanceServiceClient is a client for the memos.api.v1.InstanceService service. @@ -58,6 +61,8 @@ type InstanceServiceClient interface { UpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) // Tests notification email delivery with the provided or stored SMTP settings. TestInstanceEmailSetting(context.Context, *connect.Request[v1.TestInstanceEmailSettingRequest]) (*connect.Response[emptypb.Empty], error) + // GetInstanceStats returns resource usage statistics for the instance. Admin only. + GetInstanceStats(context.Context, *connect.Request[v1.GetInstanceStatsRequest]) (*connect.Response[v1.InstanceStats], error) } // NewInstanceServiceClient constructs a client for the memos.api.v1.InstanceService service. By @@ -95,6 +100,12 @@ func NewInstanceServiceClient(httpClient connect.HTTPClient, baseURL string, opt connect.WithSchema(instanceServiceMethods.ByName("TestInstanceEmailSetting")), connect.WithClientOptions(opts...), ), + getInstanceStats: connect.NewClient[v1.GetInstanceStatsRequest, v1.InstanceStats]( + httpClient, + baseURL+InstanceServiceGetInstanceStatsProcedure, + connect.WithSchema(instanceServiceMethods.ByName("GetInstanceStats")), + connect.WithClientOptions(opts...), + ), } } @@ -104,6 +115,7 @@ type instanceServiceClient struct { getInstanceSetting *connect.Client[v1.GetInstanceSettingRequest, v1.InstanceSetting] updateInstanceSetting *connect.Client[v1.UpdateInstanceSettingRequest, v1.InstanceSetting] testInstanceEmailSetting *connect.Client[v1.TestInstanceEmailSettingRequest, emptypb.Empty] + getInstanceStats *connect.Client[v1.GetInstanceStatsRequest, v1.InstanceStats] } // GetInstanceProfile calls memos.api.v1.InstanceService.GetInstanceProfile. @@ -126,6 +138,11 @@ func (c *instanceServiceClient) TestInstanceEmailSetting(ctx context.Context, re return c.testInstanceEmailSetting.CallUnary(ctx, req) } +// GetInstanceStats calls memos.api.v1.InstanceService.GetInstanceStats. +func (c *instanceServiceClient) GetInstanceStats(ctx context.Context, req *connect.Request[v1.GetInstanceStatsRequest]) (*connect.Response[v1.InstanceStats], error) { + return c.getInstanceStats.CallUnary(ctx, req) +} + // InstanceServiceHandler is an implementation of the memos.api.v1.InstanceService service. type InstanceServiceHandler interface { // Gets the instance profile. @@ -136,6 +153,8 @@ type InstanceServiceHandler interface { UpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) // Tests notification email delivery with the provided or stored SMTP settings. TestInstanceEmailSetting(context.Context, *connect.Request[v1.TestInstanceEmailSettingRequest]) (*connect.Response[emptypb.Empty], error) + // GetInstanceStats returns resource usage statistics for the instance. Admin only. + GetInstanceStats(context.Context, *connect.Request[v1.GetInstanceStatsRequest]) (*connect.Response[v1.InstanceStats], error) } // NewInstanceServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -169,6 +188,12 @@ func NewInstanceServiceHandler(svc InstanceServiceHandler, opts ...connect.Handl connect.WithSchema(instanceServiceMethods.ByName("TestInstanceEmailSetting")), connect.WithHandlerOptions(opts...), ) + instanceServiceGetInstanceStatsHandler := connect.NewUnaryHandler( + InstanceServiceGetInstanceStatsProcedure, + svc.GetInstanceStats, + connect.WithSchema(instanceServiceMethods.ByName("GetInstanceStats")), + connect.WithHandlerOptions(opts...), + ) return "/memos.api.v1.InstanceService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case InstanceServiceGetInstanceProfileProcedure: @@ -179,6 +204,8 @@ func NewInstanceServiceHandler(svc InstanceServiceHandler, opts ...connect.Handl instanceServiceUpdateInstanceSettingHandler.ServeHTTP(w, r) case InstanceServiceTestInstanceEmailSettingProcedure: instanceServiceTestInstanceEmailSettingHandler.ServeHTTP(w, r) + case InstanceServiceGetInstanceStatsProcedure: + instanceServiceGetInstanceStatsHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -203,3 +230,7 @@ func (UnimplementedInstanceServiceHandler) UpdateInstanceSetting(context.Context func (UnimplementedInstanceServiceHandler) TestInstanceEmailSetting(context.Context, *connect.Request[v1.TestInstanceEmailSettingRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.InstanceService.TestInstanceEmailSetting is not implemented")) } + +func (UnimplementedInstanceServiceHandler) GetInstanceStats(context.Context, *connect.Request[v1.GetInstanceStatsRequest]) (*connect.Response[v1.InstanceStats], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.InstanceService.GetInstanceStats is not implemented")) +} diff --git a/proto/gen/api/v1/instance_service.pb.go b/proto/gen/api/v1/instance_service.pb.go index 9fef356a9..593ae148b 100644 --- a/proto/gen/api/v1/instance_service.pb.go +++ b/proto/gen/api/v1/instance_service.pb.go @@ -13,6 +13,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -633,6 +634,106 @@ func (x *TestInstanceEmailSettingRequest) GetRecipientEmail() string { return "" } +// Request message for GetInstanceStats. +type GetInstanceStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetInstanceStatsRequest) Reset() { + *x = GetInstanceStatsRequest{} + mi := &file_api_v1_instance_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetInstanceStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetInstanceStatsRequest) ProtoMessage() {} + +func (x *GetInstanceStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_instance_service_proto_msgTypes[6] + 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 GetInstanceStatsRequest.ProtoReflect.Descriptor instead. +func (*GetInstanceStatsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_instance_service_proto_rawDescGZIP(), []int{6} +} + +// Resource usage statistics for the instance. +type InstanceStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + Database *InstanceStats_DatabaseStats `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` + // Recursive size of the data directory in bytes. -1 if unavailable. + LocalStorageBytes int64 `protobuf:"varint,2,opt,name=local_storage_bytes,json=localStorageBytes,proto3" json:"local_storage_bytes,omitempty"` + // Server-side timestamp when the snapshot was generated. + GeneratedTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=generated_time,json=generatedTime,proto3" json:"generated_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstanceStats) Reset() { + *x = InstanceStats{} + mi := &file_api_v1_instance_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstanceStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstanceStats) ProtoMessage() {} + +func (x *InstanceStats) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_instance_service_proto_msgTypes[7] + 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 InstanceStats.ProtoReflect.Descriptor instead. +func (*InstanceStats) Descriptor() ([]byte, []int) { + return file_api_v1_instance_service_proto_rawDescGZIP(), []int{7} +} + +func (x *InstanceStats) GetDatabase() *InstanceStats_DatabaseStats { + if x != nil { + return x.Database + } + return nil +} + +func (x *InstanceStats) GetLocalStorageBytes() int64 { + if x != nil { + return x.LocalStorageBytes + } + return 0 +} + +func (x *InstanceStats) GetGeneratedTime() *timestamppb.Timestamp { + if x != nil { + return x.GeneratedTime + } + return nil +} + // General instance settings configuration. type InstanceSetting_GeneralSetting struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -660,7 +761,7 @@ type InstanceSetting_GeneralSetting struct { func (x *InstanceSetting_GeneralSetting) Reset() { *x = InstanceSetting_GeneralSetting{} - mi := &file_api_v1_instance_service_proto_msgTypes[6] + mi := &file_api_v1_instance_service_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -672,7 +773,7 @@ func (x *InstanceSetting_GeneralSetting) String() string { func (*InstanceSetting_GeneralSetting) ProtoMessage() {} func (x *InstanceSetting_GeneralSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[6] + mi := &file_api_v1_instance_service_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -762,7 +863,7 @@ type InstanceSetting_StorageSetting struct { func (x *InstanceSetting_StorageSetting) Reset() { *x = InstanceSetting_StorageSetting{} - mi := &file_api_v1_instance_service_proto_msgTypes[7] + mi := &file_api_v1_instance_service_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -774,7 +875,7 @@ func (x *InstanceSetting_StorageSetting) String() string { func (*InstanceSetting_StorageSetting) ProtoMessage() {} func (x *InstanceSetting_StorageSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[7] + mi := &file_api_v1_instance_service_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -833,7 +934,7 @@ type InstanceSetting_MemoRelatedSetting struct { func (x *InstanceSetting_MemoRelatedSetting) Reset() { *x = InstanceSetting_MemoRelatedSetting{} - mi := &file_api_v1_instance_service_proto_msgTypes[8] + mi := &file_api_v1_instance_service_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -845,7 +946,7 @@ func (x *InstanceSetting_MemoRelatedSetting) String() string { func (*InstanceSetting_MemoRelatedSetting) ProtoMessage() {} func (x *InstanceSetting_MemoRelatedSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[8] + mi := &file_api_v1_instance_service_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -896,7 +997,7 @@ type InstanceSetting_TagMetadata struct { func (x *InstanceSetting_TagMetadata) Reset() { *x = InstanceSetting_TagMetadata{} - mi := &file_api_v1_instance_service_proto_msgTypes[9] + mi := &file_api_v1_instance_service_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -908,7 +1009,7 @@ func (x *InstanceSetting_TagMetadata) String() string { func (*InstanceSetting_TagMetadata) ProtoMessage() {} func (x *InstanceSetting_TagMetadata) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[9] + mi := &file_api_v1_instance_service_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -952,7 +1053,7 @@ type InstanceSetting_TagsSetting struct { func (x *InstanceSetting_TagsSetting) Reset() { *x = InstanceSetting_TagsSetting{} - mi := &file_api_v1_instance_service_proto_msgTypes[10] + mi := &file_api_v1_instance_service_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -964,7 +1065,7 @@ func (x *InstanceSetting_TagsSetting) String() string { func (*InstanceSetting_TagsSetting) ProtoMessage() {} func (x *InstanceSetting_TagsSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[10] + mi := &file_api_v1_instance_service_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -997,7 +1098,7 @@ type InstanceSetting_NotificationSetting struct { func (x *InstanceSetting_NotificationSetting) Reset() { *x = InstanceSetting_NotificationSetting{} - mi := &file_api_v1_instance_service_proto_msgTypes[11] + mi := &file_api_v1_instance_service_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1009,7 +1110,7 @@ func (x *InstanceSetting_NotificationSetting) String() string { func (*InstanceSetting_NotificationSetting) ProtoMessage() {} func (x *InstanceSetting_NotificationSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[11] + mi := &file_api_v1_instance_service_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1043,7 +1144,7 @@ type InstanceSetting_AISetting struct { func (x *InstanceSetting_AISetting) Reset() { *x = InstanceSetting_AISetting{} - mi := &file_api_v1_instance_service_proto_msgTypes[12] + mi := &file_api_v1_instance_service_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1055,7 +1156,7 @@ func (x *InstanceSetting_AISetting) String() string { func (*InstanceSetting_AISetting) ProtoMessage() {} func (x *InstanceSetting_AISetting) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[12] + mi := &file_api_v1_instance_service_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1097,7 +1198,7 @@ type InstanceSetting_AIProviderConfig struct { func (x *InstanceSetting_AIProviderConfig) Reset() { *x = InstanceSetting_AIProviderConfig{} - mi := &file_api_v1_instance_service_proto_msgTypes[13] + mi := &file_api_v1_instance_service_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1109,7 +1210,7 @@ func (x *InstanceSetting_AIProviderConfig) String() string { func (*InstanceSetting_AIProviderConfig) ProtoMessage() {} func (x *InstanceSetting_AIProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[13] + mi := &file_api_v1_instance_service_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1186,7 +1287,7 @@ type InstanceSetting_GeneralSetting_CustomProfile struct { func (x *InstanceSetting_GeneralSetting_CustomProfile) Reset() { *x = InstanceSetting_GeneralSetting_CustomProfile{} - mi := &file_api_v1_instance_service_proto_msgTypes[14] + mi := &file_api_v1_instance_service_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1198,7 +1299,7 @@ func (x *InstanceSetting_GeneralSetting_CustomProfile) String() string { func (*InstanceSetting_GeneralSetting_CustomProfile) ProtoMessage() {} func (x *InstanceSetting_GeneralSetting_CustomProfile) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[14] + mi := &file_api_v1_instance_service_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1251,7 +1352,7 @@ type InstanceSetting_StorageSetting_S3Config struct { func (x *InstanceSetting_StorageSetting_S3Config) Reset() { *x = InstanceSetting_StorageSetting_S3Config{} - mi := &file_api_v1_instance_service_proto_msgTypes[15] + mi := &file_api_v1_instance_service_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1263,7 +1364,7 @@ func (x *InstanceSetting_StorageSetting_S3Config) String() string { func (*InstanceSetting_StorageSetting_S3Config) ProtoMessage() {} func (x *InstanceSetting_StorageSetting_S3Config) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[15] + mi := &file_api_v1_instance_service_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1340,7 +1441,7 @@ type InstanceSetting_NotificationSetting_EmailSetting struct { func (x *InstanceSetting_NotificationSetting_EmailSetting) Reset() { *x = InstanceSetting_NotificationSetting_EmailSetting{} - mi := &file_api_v1_instance_service_proto_msgTypes[17] + mi := &file_api_v1_instance_service_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1352,7 +1453,7 @@ func (x *InstanceSetting_NotificationSetting_EmailSetting) String() string { func (*InstanceSetting_NotificationSetting_EmailSetting) ProtoMessage() {} func (x *InstanceSetting_NotificationSetting_EmailSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_instance_service_proto_msgTypes[17] + mi := &file_api_v1_instance_service_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1438,11 +1539,66 @@ func (x *InstanceSetting_NotificationSetting_EmailSetting) GetUseSsl() bool { return false } +// Database size statistics. +type InstanceStats_DatabaseStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + // driver is one of "sqlite", "mysql", "postgres". + Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"` + // size_bytes is the database size in bytes; -1 if unavailable. + SizeBytes int64 `protobuf:"varint,2,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstanceStats_DatabaseStats) Reset() { + *x = InstanceStats_DatabaseStats{} + mi := &file_api_v1_instance_service_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstanceStats_DatabaseStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstanceStats_DatabaseStats) ProtoMessage() {} + +func (x *InstanceStats_DatabaseStats) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_instance_service_proto_msgTypes[20] + 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 InstanceStats_DatabaseStats.ProtoReflect.Descriptor instead. +func (*InstanceStats_DatabaseStats) Descriptor() ([]byte, []int) { + return file_api_v1_instance_service_proto_rawDescGZIP(), []int{7, 0} +} + +func (x *InstanceStats_DatabaseStats) GetDriver() string { + if x != nil { + return x.Driver + } + return "" +} + +func (x *InstanceStats_DatabaseStats) GetSizeBytes() int64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + var File_api_v1_instance_service_proto protoreflect.FileDescriptor const file_api_v1_instance_service_proto_rawDesc = "" + "\n" + - "\x1dapi/v1/instance_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x17google/type/color.proto\"\xa4\x01\n" + + "\x1dapi/v1/instance_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17google/type/color.proto\"\xa4\x01\n" + "\x0fInstanceProfile\x12\x18\n" + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + "\x04demo\x18\x03 \x01(\bR\x04demo\x12!\n" + @@ -1552,12 +1708,22 @@ const file_api_v1_instance_service_proto_rawDesc = "" + "updateMask\"\xaa\x01\n" + "\x1fTestInstanceEmailSettingRequest\x12Y\n" + "\x05email\x18\x01 \x01(\v2>.memos.api.v1.InstanceSetting.NotificationSetting.EmailSettingB\x03\xe0A\x01R\x05email\x12,\n" + - "\x0frecipient_email\x18\x02 \x01(\tB\x03\xe0A\x01R\x0erecipientEmail2\xfc\x04\n" + + "\x0frecipient_email\x18\x02 \x01(\tB\x03\xe0A\x01R\x0erecipientEmail\"\x19\n" + + "\x17GetInstanceStatsRequest\"\x91\x02\n" + + "\rInstanceStats\x12E\n" + + "\bdatabase\x18\x01 \x01(\v2).memos.api.v1.InstanceStats.DatabaseStatsR\bdatabase\x12.\n" + + "\x13local_storage_bytes\x18\x02 \x01(\x03R\x11localStorageBytes\x12A\n" + + "\x0egenerated_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\rgeneratedTime\x1aF\n" + + "\rDatabaseStats\x12\x16\n" + + "\x06driver\x18\x01 \x01(\tR\x06driver\x12\x1d\n" + + "\n" + + "size_bytes\x18\x02 \x01(\x03R\tsizeBytes2\xf4\x05\n" + "\x0fInstanceService\x12~\n" + "\x12GetInstanceProfile\x12'.memos.api.v1.GetInstanceProfileRequest\x1a\x1d.memos.api.v1.InstanceProfile\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/api/v1/instance/profile\x12\x8f\x01\n" + "\x12GetInstanceSetting\x12'.memos.api.v1.GetInstanceSettingRequest\x1a\x1d.memos.api.v1.InstanceSetting\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$\x12\"/api/v1/{name=instance/settings/*}\x12\xb5\x01\n" + "\x15UpdateInstanceSetting\x12*.memos.api.v1.UpdateInstanceSettingRequest\x1a\x1d.memos.api.v1.InstanceSetting\"Q\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x025:\asetting2*/api/v1/{setting.name=instance/settings/*}\x12\x9e\x01\n" + - "\x18TestInstanceEmailSetting\x12-.memos.api.v1.TestInstanceEmailSettingRequest\x1a\x16.google.protobuf.Empty\";\x82\xd3\xe4\x93\x025:\x01*\"0/api/v1/instance/settings/notification:testEmailB\xac\x01\n" + + "\x18TestInstanceEmailSetting\x12-.memos.api.v1.TestInstanceEmailSettingRequest\x1a\x16.google.protobuf.Empty\";\x82\xd3\xe4\x93\x025:\x01*\"0/api/v1/instance/settings/notification:testEmail\x12v\n" + + "\x10GetInstanceStats\x12%.memos.api.v1.GetInstanceStatsRequest\x1a\x1b.memos.api.v1.InstanceStats\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/instance/statsB\xac\x01\n" + "\x10com.memos.api.v1B\x14InstanceServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" var ( @@ -1573,7 +1739,7 @@ func file_api_v1_instance_service_proto_rawDescGZIP() []byte { } var file_api_v1_instance_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_api_v1_instance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_api_v1_instance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_api_v1_instance_service_proto_goTypes = []any{ (InstanceSetting_Key)(0), // 0: memos.api.v1.InstanceSetting.Key (InstanceSetting_AIProviderType)(0), // 1: memos.api.v1.InstanceSetting.AIProviderType @@ -1584,56 +1750,64 @@ var file_api_v1_instance_service_proto_goTypes = []any{ (*GetInstanceSettingRequest)(nil), // 6: memos.api.v1.GetInstanceSettingRequest (*UpdateInstanceSettingRequest)(nil), // 7: memos.api.v1.UpdateInstanceSettingRequest (*TestInstanceEmailSettingRequest)(nil), // 8: memos.api.v1.TestInstanceEmailSettingRequest - (*InstanceSetting_GeneralSetting)(nil), // 9: memos.api.v1.InstanceSetting.GeneralSetting - (*InstanceSetting_StorageSetting)(nil), // 10: memos.api.v1.InstanceSetting.StorageSetting - (*InstanceSetting_MemoRelatedSetting)(nil), // 11: memos.api.v1.InstanceSetting.MemoRelatedSetting - (*InstanceSetting_TagMetadata)(nil), // 12: memos.api.v1.InstanceSetting.TagMetadata - (*InstanceSetting_TagsSetting)(nil), // 13: memos.api.v1.InstanceSetting.TagsSetting - (*InstanceSetting_NotificationSetting)(nil), // 14: memos.api.v1.InstanceSetting.NotificationSetting - (*InstanceSetting_AISetting)(nil), // 15: memos.api.v1.InstanceSetting.AISetting - (*InstanceSetting_AIProviderConfig)(nil), // 16: memos.api.v1.InstanceSetting.AIProviderConfig - (*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 17: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile - (*InstanceSetting_StorageSetting_S3Config)(nil), // 18: memos.api.v1.InstanceSetting.StorageSetting.S3Config - nil, // 19: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry - (*InstanceSetting_NotificationSetting_EmailSetting)(nil), // 20: memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting - (*User)(nil), // 21: memos.api.v1.User - (*fieldmaskpb.FieldMask)(nil), // 22: google.protobuf.FieldMask - (*color.Color)(nil), // 23: google.type.Color - (*emptypb.Empty)(nil), // 24: google.protobuf.Empty + (*GetInstanceStatsRequest)(nil), // 9: memos.api.v1.GetInstanceStatsRequest + (*InstanceStats)(nil), // 10: memos.api.v1.InstanceStats + (*InstanceSetting_GeneralSetting)(nil), // 11: memos.api.v1.InstanceSetting.GeneralSetting + (*InstanceSetting_StorageSetting)(nil), // 12: memos.api.v1.InstanceSetting.StorageSetting + (*InstanceSetting_MemoRelatedSetting)(nil), // 13: memos.api.v1.InstanceSetting.MemoRelatedSetting + (*InstanceSetting_TagMetadata)(nil), // 14: memos.api.v1.InstanceSetting.TagMetadata + (*InstanceSetting_TagsSetting)(nil), // 15: memos.api.v1.InstanceSetting.TagsSetting + (*InstanceSetting_NotificationSetting)(nil), // 16: memos.api.v1.InstanceSetting.NotificationSetting + (*InstanceSetting_AISetting)(nil), // 17: memos.api.v1.InstanceSetting.AISetting + (*InstanceSetting_AIProviderConfig)(nil), // 18: memos.api.v1.InstanceSetting.AIProviderConfig + (*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 19: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile + (*InstanceSetting_StorageSetting_S3Config)(nil), // 20: memos.api.v1.InstanceSetting.StorageSetting.S3Config + nil, // 21: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry + (*InstanceSetting_NotificationSetting_EmailSetting)(nil), // 22: memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting + (*InstanceStats_DatabaseStats)(nil), // 23: memos.api.v1.InstanceStats.DatabaseStats + (*User)(nil), // 24: memos.api.v1.User + (*fieldmaskpb.FieldMask)(nil), // 25: google.protobuf.FieldMask + (*timestamppb.Timestamp)(nil), // 26: google.protobuf.Timestamp + (*color.Color)(nil), // 27: google.type.Color + (*emptypb.Empty)(nil), // 28: google.protobuf.Empty } var file_api_v1_instance_service_proto_depIdxs = []int32{ - 21, // 0: memos.api.v1.InstanceProfile.admin:type_name -> memos.api.v1.User - 9, // 1: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting - 10, // 2: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting - 11, // 3: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting - 13, // 4: memos.api.v1.InstanceSetting.tags_setting:type_name -> memos.api.v1.InstanceSetting.TagsSetting - 14, // 5: memos.api.v1.InstanceSetting.notification_setting:type_name -> memos.api.v1.InstanceSetting.NotificationSetting - 15, // 6: memos.api.v1.InstanceSetting.ai_setting:type_name -> memos.api.v1.InstanceSetting.AISetting + 24, // 0: memos.api.v1.InstanceProfile.admin:type_name -> memos.api.v1.User + 11, // 1: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting + 12, // 2: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting + 13, // 3: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting + 15, // 4: memos.api.v1.InstanceSetting.tags_setting:type_name -> memos.api.v1.InstanceSetting.TagsSetting + 16, // 5: memos.api.v1.InstanceSetting.notification_setting:type_name -> memos.api.v1.InstanceSetting.NotificationSetting + 17, // 6: memos.api.v1.InstanceSetting.ai_setting:type_name -> memos.api.v1.InstanceSetting.AISetting 5, // 7: memos.api.v1.UpdateInstanceSettingRequest.setting:type_name -> memos.api.v1.InstanceSetting - 22, // 8: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask - 20, // 9: memos.api.v1.TestInstanceEmailSettingRequest.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting - 17, // 10: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile - 2, // 11: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType - 18, // 12: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config - 23, // 13: memos.api.v1.InstanceSetting.TagMetadata.background_color:type_name -> google.type.Color - 19, // 14: memos.api.v1.InstanceSetting.TagsSetting.tags:type_name -> memos.api.v1.InstanceSetting.TagsSetting.TagsEntry - 20, // 15: memos.api.v1.InstanceSetting.NotificationSetting.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting - 16, // 16: memos.api.v1.InstanceSetting.AISetting.providers:type_name -> memos.api.v1.InstanceSetting.AIProviderConfig - 1, // 17: memos.api.v1.InstanceSetting.AIProviderConfig.type:type_name -> memos.api.v1.InstanceSetting.AIProviderType - 12, // 18: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.InstanceSetting.TagMetadata - 4, // 19: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest - 6, // 20: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest - 7, // 21: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest - 8, // 22: memos.api.v1.InstanceService.TestInstanceEmailSetting:input_type -> memos.api.v1.TestInstanceEmailSettingRequest - 3, // 23: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile - 5, // 24: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting - 5, // 25: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting - 24, // 26: memos.api.v1.InstanceService.TestInstanceEmailSetting:output_type -> google.protobuf.Empty - 23, // [23:27] is the sub-list for method output_type - 19, // [19:23] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 25, // 8: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask + 22, // 9: memos.api.v1.TestInstanceEmailSettingRequest.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting + 23, // 10: memos.api.v1.InstanceStats.database:type_name -> memos.api.v1.InstanceStats.DatabaseStats + 26, // 11: memos.api.v1.InstanceStats.generated_time:type_name -> google.protobuf.Timestamp + 19, // 12: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile + 2, // 13: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType + 20, // 14: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config + 27, // 15: memos.api.v1.InstanceSetting.TagMetadata.background_color:type_name -> google.type.Color + 21, // 16: memos.api.v1.InstanceSetting.TagsSetting.tags:type_name -> memos.api.v1.InstanceSetting.TagsSetting.TagsEntry + 22, // 17: memos.api.v1.InstanceSetting.NotificationSetting.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting + 18, // 18: memos.api.v1.InstanceSetting.AISetting.providers:type_name -> memos.api.v1.InstanceSetting.AIProviderConfig + 1, // 19: memos.api.v1.InstanceSetting.AIProviderConfig.type:type_name -> memos.api.v1.InstanceSetting.AIProviderType + 14, // 20: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.InstanceSetting.TagMetadata + 4, // 21: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest + 6, // 22: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest + 7, // 23: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest + 8, // 24: memos.api.v1.InstanceService.TestInstanceEmailSetting:input_type -> memos.api.v1.TestInstanceEmailSettingRequest + 9, // 25: memos.api.v1.InstanceService.GetInstanceStats:input_type -> memos.api.v1.GetInstanceStatsRequest + 3, // 26: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile + 5, // 27: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting + 5, // 28: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting + 28, // 29: memos.api.v1.InstanceService.TestInstanceEmailSetting:output_type -> google.protobuf.Empty + 10, // 30: memos.api.v1.InstanceService.GetInstanceStats:output_type -> memos.api.v1.InstanceStats + 26, // [26:31] is the sub-list for method output_type + 21, // [21:26] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_api_v1_instance_service_proto_init() } @@ -1656,7 +1830,7 @@ func file_api_v1_instance_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_instance_service_proto_rawDesc), len(file_api_v1_instance_service_proto_rawDesc)), NumEnums: 3, - NumMessages: 18, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/gen/api/v1/instance_service.pb.gw.go b/proto/gen/api/v1/instance_service.pb.gw.go index 5c4e726db..fb7f909ad 100644 --- a/proto/gen/api/v1/instance_service.pb.gw.go +++ b/proto/gen/api/v1/instance_service.pb.gw.go @@ -203,6 +203,27 @@ func local_request_InstanceService_TestInstanceEmailSetting_0(ctx context.Contex return msg, metadata, err } +func request_InstanceService_GetInstanceStats_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetInstanceStatsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.GetInstanceStats(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_InstanceService_GetInstanceStats_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetInstanceStatsRequest + metadata runtime.ServerMetadata + ) + msg, err := server.GetInstanceStats(ctx, &protoReq) + return msg, metadata, err +} + // RegisterInstanceServiceHandlerServer registers the http handlers for service InstanceService to "mux". // UnaryRPC :call InstanceServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -289,6 +310,26 @@ func RegisterInstanceServiceHandlerServer(ctx context.Context, mux *runtime.Serv } forward_InstanceService_TestInstanceEmailSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.InstanceService/GetInstanceStats", runtime.WithHTTPPathPattern("/api/v1/instance/stats")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_InstanceService_GetInstanceStats_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_InstanceService_GetInstanceStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -397,6 +438,23 @@ func RegisterInstanceServiceHandlerClient(ctx context.Context, mux *runtime.Serv } forward_InstanceService_TestInstanceEmailSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceStats_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) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.InstanceService/GetInstanceStats", runtime.WithHTTPPathPattern("/api/v1/instance/stats")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_InstanceService_GetInstanceStats_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_InstanceService_GetInstanceStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -405,6 +463,7 @@ var ( pattern_InstanceService_GetInstanceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{"api", "v1", "instance", "settings", "name"}, "")) pattern_InstanceService_UpdateInstanceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{"api", "v1", "instance", "settings", "setting.name"}, "")) pattern_InstanceService_TestInstanceEmailSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "instance", "settings", "notification"}, "testEmail")) + pattern_InstanceService_GetInstanceStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "instance", "stats"}, "")) ) var ( @@ -412,4 +471,5 @@ var ( forward_InstanceService_GetInstanceSetting_0 = runtime.ForwardResponseMessage forward_InstanceService_UpdateInstanceSetting_0 = runtime.ForwardResponseMessage forward_InstanceService_TestInstanceEmailSetting_0 = runtime.ForwardResponseMessage + forward_InstanceService_GetInstanceStats_0 = runtime.ForwardResponseMessage ) diff --git a/proto/gen/api/v1/instance_service_grpc.pb.go b/proto/gen/api/v1/instance_service_grpc.pb.go index 5d6b0eeb3..25575dcec 100644 --- a/proto/gen/api/v1/instance_service_grpc.pb.go +++ b/proto/gen/api/v1/instance_service_grpc.pb.go @@ -24,6 +24,7 @@ const ( InstanceService_GetInstanceSetting_FullMethodName = "/memos.api.v1.InstanceService/GetInstanceSetting" InstanceService_UpdateInstanceSetting_FullMethodName = "/memos.api.v1.InstanceService/UpdateInstanceSetting" InstanceService_TestInstanceEmailSetting_FullMethodName = "/memos.api.v1.InstanceService/TestInstanceEmailSetting" + InstanceService_GetInstanceStats_FullMethodName = "/memos.api.v1.InstanceService/GetInstanceStats" ) // InstanceServiceClient is the client API for InstanceService service. @@ -38,6 +39,8 @@ type InstanceServiceClient interface { UpdateInstanceSetting(ctx context.Context, in *UpdateInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error) // Tests notification email delivery with the provided or stored SMTP settings. TestInstanceEmailSetting(ctx context.Context, in *TestInstanceEmailSettingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // GetInstanceStats returns resource usage statistics for the instance. Admin only. + GetInstanceStats(ctx context.Context, in *GetInstanceStatsRequest, opts ...grpc.CallOption) (*InstanceStats, error) } type instanceServiceClient struct { @@ -88,6 +91,16 @@ func (c *instanceServiceClient) TestInstanceEmailSetting(ctx context.Context, in return out, nil } +func (c *instanceServiceClient) GetInstanceStats(ctx context.Context, in *GetInstanceStatsRequest, opts ...grpc.CallOption) (*InstanceStats, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(InstanceStats) + err := c.cc.Invoke(ctx, InstanceService_GetInstanceStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // InstanceServiceServer is the server API for InstanceService service. // All implementations must embed UnimplementedInstanceServiceServer // for forward compatibility. @@ -100,6 +113,8 @@ type InstanceServiceServer interface { UpdateInstanceSetting(context.Context, *UpdateInstanceSettingRequest) (*InstanceSetting, error) // Tests notification email delivery with the provided or stored SMTP settings. TestInstanceEmailSetting(context.Context, *TestInstanceEmailSettingRequest) (*emptypb.Empty, error) + // GetInstanceStats returns resource usage statistics for the instance. Admin only. + GetInstanceStats(context.Context, *GetInstanceStatsRequest) (*InstanceStats, error) mustEmbedUnimplementedInstanceServiceServer() } @@ -122,6 +137,9 @@ func (UnimplementedInstanceServiceServer) UpdateInstanceSetting(context.Context, func (UnimplementedInstanceServiceServer) TestInstanceEmailSetting(context.Context, *TestInstanceEmailSettingRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method TestInstanceEmailSetting not implemented") } +func (UnimplementedInstanceServiceServer) GetInstanceStats(context.Context, *GetInstanceStatsRequest) (*InstanceStats, error) { + return nil, status.Error(codes.Unimplemented, "method GetInstanceStats not implemented") +} func (UnimplementedInstanceServiceServer) mustEmbedUnimplementedInstanceServiceServer() {} func (UnimplementedInstanceServiceServer) testEmbeddedByValue() {} @@ -215,6 +233,24 @@ func _InstanceService_TestInstanceEmailSetting_Handler(srv interface{}, ctx cont return interceptor(ctx, in, info, handler) } +func _InstanceService_GetInstanceStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetInstanceStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InstanceServiceServer).GetInstanceStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InstanceService_GetInstanceStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InstanceServiceServer).GetInstanceStats(ctx, req.(*GetInstanceStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // InstanceService_ServiceDesc is the grpc.ServiceDesc for InstanceService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -238,6 +274,10 @@ var InstanceService_ServiceDesc = grpc.ServiceDesc{ MethodName: "TestInstanceEmailSetting", Handler: _InstanceService_TestInstanceEmailSetting_Handler, }, + { + MethodName: "GetInstanceStats", + Handler: _InstanceService_GetInstanceStats_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/v1/instance_service.proto", diff --git a/proto/gen/openapi.yaml b/proto/gen/openapi.yaml index d622c06ff..ec16f65a5 100644 --- a/proto/gen/openapi.yaml +++ b/proto/gen/openapi.yaml @@ -498,6 +498,25 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' + /api/v1/instance/stats: + get: + tags: + - InstanceService + description: GetInstanceStats returns resource usage statistics for the instance. Admin only. + operationId: InstanceService_GetInstanceStats + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/InstanceStats' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' /api/v1/instance/{instance}/*: get: tags: @@ -2789,6 +2808,29 @@ components: so a single entry like "project/.*" matches all tags under that prefix. Exact tag names are also valid (they are trivially valid regex patterns). description: Tag metadata configuration. + InstanceStats: + type: object + properties: + database: + $ref: '#/components/schemas/InstanceStats_DatabaseStats' + localStorageBytes: + type: string + description: Recursive size of the data directory in bytes. -1 if unavailable. + generatedTime: + type: string + description: Server-side timestamp when the snapshot was generated. + format: date-time + description: Resource usage statistics for the instance. + InstanceStats_DatabaseStats: + type: object + properties: + driver: + type: string + description: driver is one of "sqlite", "mysql", "postgres". + sizeBytes: + type: string + description: size_bytes is the database size in bytes; -1 if unavailable. + description: Database size statistics. LinkMetadata: type: object properties: diff --git a/server/router/api/v1/connect_services.go b/server/router/api/v1/connect_services.go index 486b30761..60bc0566b 100644 --- a/server/router/api/v1/connect_services.go +++ b/server/router/api/v1/connect_services.go @@ -47,6 +47,14 @@ func (s *ConnectServiceHandler) TestInstanceEmailSetting(ctx context.Context, re return connect.NewResponse(resp), nil } +func (s *ConnectServiceHandler) GetInstanceStats(ctx context.Context, req *connect.Request[v1pb.GetInstanceStatsRequest]) (*connect.Response[v1pb.InstanceStats], error) { + resp, err := s.APIV1Service.GetInstanceStats(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + // AuthService // // Auth service methods need special handling for response headers (cookies). diff --git a/server/router/api/v1/instance_stats.go b/server/router/api/v1/instance_stats.go new file mode 100644 index 000000000..f0015e5b0 --- /dev/null +++ b/server/router/api/v1/instance_stats.go @@ -0,0 +1,165 @@ +package v1 + +import ( + "context" + "log/slog" + "os" + "path/filepath" + "sync" + "time" + + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/store" +) + +const instanceStatsCacheTTL = 60 * time.Second + +// instanceStatsCache is a single-value, mutex-guarded cache for InstanceStats. +type instanceStatsCache struct { + mu sync.Mutex + value *v1pb.InstanceStats + expiry time.Time +} + +func (c *instanceStatsCache) get() (*v1pb.InstanceStats, bool) { + c.mu.Lock() + defer c.mu.Unlock() + if c.value == nil || time.Now().After(c.expiry) { + return nil, false + } + return c.value, true +} + +func (c *instanceStatsCache) set(v *v1pb.InstanceStats, ttl time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + c.value = v + c.expiry = time.Now().Add(ttl) +} + +// GetInstanceStats returns resource usage statistics. Admin only. +func (s *APIV1Service) GetInstanceStats(ctx context.Context, _ *v1pb.GetInstanceStatsRequest) (*v1pb.InstanceStats, error) { + user, err := s.fetchCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + if user.Role != store.RoleAdmin { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + if cached, ok := s.instanceStatsCache.get(); ok { + return cached, nil + } + + stats, err := s.computeInstanceStats(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to compute instance stats: %v", err) + } + s.instanceStatsCache.set(stats, instanceStatsCacheTTL) + return stats, nil +} + +// computeInstanceStats runs all stat subqueries in parallel and assembles the result. +// Per-subtask failures degrade to -1 sentinel values; only a total failure (every +// subtask errored) is propagated as an error. +func (s *APIV1Service) computeInstanceStats(ctx context.Context) (*v1pb.InstanceStats, error) { + stats := &v1pb.InstanceStats{ + Database: &v1pb.InstanceStats_DatabaseStats{ + Driver: s.Profile.Driver, + SizeBytes: -1, + }, + LocalStorageBytes: -1, + GeneratedTime: timestamppb.Now(), + } + + type result struct { + name string + err error + } + var ( + mu sync.Mutex + results []result + record = func(name string, err error) { + mu.Lock() + results = append(results, result{name, err}) + mu.Unlock() + } + ) + + g, gctx := errgroup.WithContext(ctx) + + g.Go(func() error { + size, err := s.Store.GetDriver().GetDatabaseSize(gctx) + if err != nil { + record("database_size", err) + return nil + } + stats.Database.SizeBytes = size + return nil + }) + + g.Go(func() error { + size, err := walkLocalStorage(s.Profile.Data) + if err != nil { + record("local_storage", err) + return nil + } + stats.LocalStorageBytes = size + return nil + }) + + _ = g.Wait() + + for _, r := range results { + slog.Warn("instance stats subtask failed", slog.String("subtask", r.name), slog.String("err", r.err.Error())) + } + + const totalSubtasks = 2 + if len(results) == totalSubtasks { + return nil, errors.New("all instance stats subtasks failed") + } + return stats, nil +} + +// walkLocalStorage returns the recursive size of dir in bytes. +// Symlinks are not followed; per-entry errors below the root are ignored +// (the walk continues). An error accessing the root itself is returned. +func walkLocalStorage(dir string) (int64, error) { + if dir == "" { + return -1, errors.New("empty data directory") + } + var total int64 + err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + if path == dir { + // Root itself is inaccessible — abort the walk. + return walkErr + } + // Ignore per-entry errors (e.g. permission denied on a single file). + return nil + } + if entry.IsDir() { + return nil + } + info, err := entry.Info() + if err != nil { + // Ignore stat errors on individual entries; continue the walk. + return nil //nolint:nilerr + } + total += info.Size() + return nil + }) + if err != nil { + return -1, errors.Wrap(err, "walk failed") + } + return total, nil +} diff --git a/server/router/api/v1/instance_stats_test.go b/server/router/api/v1/instance_stats_test.go new file mode 100644 index 000000000..5414961f8 --- /dev/null +++ b/server/router/api/v1/instance_stats_test.go @@ -0,0 +1,34 @@ +package v1 + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWalkLocalStorage_SumsFileSizes(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "a.txt"), []byte("hello"), 0o600)) // 5 + require.NoError(t, os.WriteFile(filepath.Join(dir, "b.txt"), []byte("world!"), 0o600)) // 6 + sub := filepath.Join(dir, "sub") + require.NoError(t, os.Mkdir(sub, 0o700)) + require.NoError(t, os.WriteFile(filepath.Join(sub, "c.txt"), []byte("xx"), 0o600)) // 2 + + size, err := walkLocalStorage(dir) + require.NoError(t, err) + require.Equal(t, int64(13), size) +} + +func TestWalkLocalStorage_EmptyDir(t *testing.T) { + size, err := walkLocalStorage("") + require.Error(t, err) + require.Equal(t, int64(-1), size) +} + +func TestWalkLocalStorage_NonexistentDir(t *testing.T) { + size, err := walkLocalStorage(filepath.Join(t.TempDir(), "does-not-exist")) + require.Error(t, err) + require.Equal(t, int64(-1), size) +} diff --git a/server/router/api/v1/test/instance_stats_test.go b/server/router/api/v1/test/instance_stats_test.go new file mode 100644 index 000000000..071486bf4 --- /dev/null +++ b/server/router/api/v1/test/instance_stats_test.go @@ -0,0 +1,72 @@ +package test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" +) + +func TestGetInstanceStats_HappyPath(t *testing.T) { + ctx := context.Background() + ts := NewTestService(t) + defer ts.Cleanup() + + admin, err := ts.CreateHostUser(ctx, "admin1") + require.NoError(t, err) + adminCtx := ts.CreateUserContext(ctx, admin.ID) + + resp, err := ts.Service.GetInstanceStats(adminCtx, &v1pb.GetInstanceStatsRequest{}) + require.NoError(t, err) + require.NotNil(t, resp) + + require.NotNil(t, resp.Database) + require.Equal(t, "sqlite", resp.Database.Driver) + require.Greater(t, resp.Database.SizeBytes, int64(0)) + + require.GreaterOrEqual(t, resp.LocalStorageBytes, int64(0)) +} + +func TestGetInstanceStats_NonAdminDenied(t *testing.T) { + ctx := context.Background() + ts := NewTestService(t) + defer ts.Cleanup() + + // Need an admin to exist (otherwise instance is uninitialized). + admin, err := ts.CreateHostUser(ctx, "admin1") + require.NoError(t, err) + _ = admin + + regular, err := ts.CreateRegularUser(ctx, "alice") + require.NoError(t, err) + regularCtx := ts.CreateUserContext(ctx, regular.ID) + + _, err = ts.Service.GetInstanceStats(regularCtx, &v1pb.GetInstanceStatsRequest{}) + require.Error(t, err) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.PermissionDenied, st.Code()) +} + +func TestGetInstanceStats_Cache(t *testing.T) { + ctx := context.Background() + ts := NewTestService(t) + defer ts.Cleanup() + + admin, err := ts.CreateHostUser(ctx, "admin1") + require.NoError(t, err) + adminCtx := ts.CreateUserContext(ctx, admin.ID) + + first, err := ts.Service.GetInstanceStats(adminCtx, &v1pb.GetInstanceStatsRequest{}) + require.NoError(t, err) + + second, err := ts.Service.GetInstanceStats(adminCtx, &v1pb.GetInstanceStatsRequest{}) + require.NoError(t, err) + + // Cache hit: same pointer (the cache returns the stored *InstanceStats directly). + require.Same(t, first, second) +} diff --git a/server/router/api/v1/v1.go b/server/router/api/v1/v1.go index a402f2976..17269da21 100644 --- a/server/router/api/v1/v1.go +++ b/server/router/api/v1/v1.go @@ -42,6 +42,9 @@ type APIV1Service struct { // thumbnailSemaphore limits concurrent thumbnail generation to prevent memory exhaustion thumbnailSemaphore *semaphore.Weighted imageProcessingSemaphore *semaphore.Weighted + + // instanceStatsCache memoizes GetInstanceStats results for instanceStatsCacheTTL. + instanceStatsCache instanceStatsCache } func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store) *APIV1Service { diff --git a/server/router/frontend/frontend_test.go b/server/router/frontend/frontend_test.go index 912c48136..aa1959e1b 100644 --- a/server/router/frontend/frontend_test.go +++ b/server/router/frontend/frontend_test.go @@ -72,7 +72,7 @@ func TestFrontendService_SitemapXML(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) require.Contains(t, rec.Header().Get("Content-Type"), "application/xml") - require.Contains(t, rec.Body.String(), `https://demo.usememos.com/m/publicmemo`) + require.Contains(t, rec.Body.String(), `https://demo.usememos.com/memos/publicmemo`) require.NotContains(t, rec.Body.String(), "privatememo") } diff --git a/store/attachment.go b/store/attachment.go index d0b2742f3..69869a0b9 100644 --- a/store/attachment.go +++ b/store/attachment.go @@ -177,7 +177,7 @@ func (s *Store) DeleteAttachments(ctx context.Context, attachments []*Attachment if instanceStorageSettingErr != nil && AttachmentNeedsInstanceStorageSetting(attachment) { err = instanceStorageSettingErr } else { - err = s.deleteAttachmentStorage(ctx, attachment, instanceStorageSetting) + err = s.deleteAttachmentStorageImpl(ctx, attachment, instanceStorageSetting) } if err != nil { if attachment.StorageType == storepb.AttachmentStorageType_LOCAL { @@ -191,15 +191,15 @@ func (s *Store) DeleteAttachments(ctx context.Context, attachments []*Attachment } func (s *Store) DeleteAttachmentStorage(ctx context.Context, attachment *Attachment) error { - return s.deleteAttachmentStorage(ctx, attachment, nil) + return s.deleteAttachmentStorageImpl(ctx, attachment, nil) } // DeleteAttachmentStorageWithInstanceSetting deletes attachment storage using a preloaded instance storage setting. func (s *Store) DeleteAttachmentStorageWithInstanceSetting(ctx context.Context, attachment *Attachment, instanceStorageSetting *storepb.InstanceStorageSetting) error { - return s.deleteAttachmentStorage(ctx, attachment, instanceStorageSetting) + return s.deleteAttachmentStorageImpl(ctx, attachment, instanceStorageSetting) } -func (s *Store) deleteAttachmentStorage(ctx context.Context, attachment *Attachment, instanceStorageSetting *storepb.InstanceStorageSetting) error { +func (s *Store) deleteAttachmentStorageImpl(ctx context.Context, attachment *Attachment, instanceStorageSetting *storepb.InstanceStorageSetting) error { if attachment == nil { return nil } diff --git a/store/db/mysql/mysql.go b/store/db/mysql/mysql.go index 2fd3ddb9d..dee92d9d4 100644 --- a/store/db/mysql/mysql.go +++ b/store/db/mysql/mysql.go @@ -57,6 +57,18 @@ func (d *DB) IsInitialized(ctx context.Context) (bool, error) { return exists, nil } +// GetDatabaseSize returns the database size in bytes, or -1 if unavailable. +func (d *DB) GetDatabaseSize(ctx context.Context) (int64, error) { + var size int64 + const q = `SELECT COALESCE(SUM(data_length + index_length), 0) + FROM information_schema.tables + WHERE table_schema = DATABASE()` + if err := d.db.QueryRowContext(ctx, q).Scan(&size); err != nil { + return -1, errors.Wrap(err, "failed to query mysql database size") + } + return size, nil +} + func mergeDSN(baseDSN string) (string, error) { config, err := mysql.ParseDSN(baseDSN) if err != nil { diff --git a/store/db/postgres/postgres.go b/store/db/postgres/postgres.go index 1744a2cb5..73ccd0102 100644 --- a/store/db/postgres/postgres.go +++ b/store/db/postgres/postgres.go @@ -55,3 +55,13 @@ func (d *DB) IsInitialized(ctx context.Context) (bool, error) { } return exists, nil } + +// GetDatabaseSize returns the database size in bytes, or -1 if unavailable. +func (d *DB) GetDatabaseSize(ctx context.Context) (int64, error) { + var size int64 + const q = `SELECT pg_database_size(current_database())` + if err := d.db.QueryRowContext(ctx, q).Scan(&size); err != nil { + return -1, errors.Wrap(err, "failed to query postgres database size") + } + return size, nil +} diff --git a/store/db/sqlite/sqlite.go b/store/db/sqlite/sqlite.go index dcc979832..892918e6a 100644 --- a/store/db/sqlite/sqlite.go +++ b/store/db/sqlite/sqlite.go @@ -73,3 +73,15 @@ func (d *DB) IsInitialized(ctx context.Context) (bool, error) { } return exists, nil } + +// GetDatabaseSize returns the database size in bytes, or -1 if unavailable. +func (d *DB) GetDatabaseSize(ctx context.Context) (int64, error) { + var pageCount, pageSize int64 + if err := d.db.QueryRowContext(ctx, "PRAGMA page_count").Scan(&pageCount); err != nil { + return -1, errors.Wrap(err, "failed to read page_count") + } + if err := d.db.QueryRowContext(ctx, "PRAGMA page_size").Scan(&pageSize); err != nil { + return -1, errors.Wrap(err, "failed to read page_size") + } + return pageCount * pageSize, nil +} diff --git a/store/driver.go b/store/driver.go index 54816bbf2..f3d000c1e 100644 --- a/store/driver.go +++ b/store/driver.go @@ -13,6 +13,11 @@ type Driver interface { IsInitialized(ctx context.Context) (bool, error) + // GetDatabaseSize returns the database size in bytes, or -1 if unavailable. + // A non-nil error indicates a hard failure; -1 with nil error means the + // driver cannot report a size from the underlying database. + GetDatabaseSize(ctx context.Context) (int64, error) + // Attachment model related methods. CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error) ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error) diff --git a/store/test/database_size_test.go b/store/test/database_size_test.go new file mode 100644 index 000000000..6095c51f5 --- /dev/null +++ b/store/test/database_size_test.go @@ -0,0 +1,19 @@ +package test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetDatabaseSize(t *testing.T) { + t.Parallel() + ctx := context.Background() + ts := NewTestingStore(ctx, t) + defer ts.Close() + + size, err := ts.GetDriver().GetDatabaseSize(ctx) + require.NoError(t, err) + require.Greater(t, size, int64(0), "expected database size > 0") +} diff --git a/web/src/components/Settings/ResourceStatsSection.tsx b/web/src/components/Settings/ResourceStatsSection.tsx new file mode 100644 index 000000000..08308a5d9 --- /dev/null +++ b/web/src/components/Settings/ResourceStatsSection.tsx @@ -0,0 +1,104 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { RefreshCwIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { instanceKeys, useInstanceStats } from "@/hooks/useInstanceQueries"; +import { useTranslate } from "@/utils/i18n"; +import SettingGroup from "./SettingGroup"; +import { SettingList, SettingListItem, SettingPanel } from "./SettingList"; +import SettingSection from "./SettingSection"; + +const formatBytes = (bytes: number | bigint): string => { + const n = typeof bytes === "bigint" ? Number(bytes) : bytes; + if (n < 0) return "—"; + if (n === 0) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.min(units.length - 1, Math.floor(Math.log(n) / Math.log(1024))); + return `${(n / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`; +}; + +const formatRelativeTime = (date: Date): string => { + const seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000)); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + return `${hours}h ago`; +}; + +const renderBytes = (value: bigint | number | undefined, unknown: string): string => { + if (value === undefined) return unknown; + const n = typeof value === "bigint" ? Number(value) : value; + if (n < 0) return unknown; + return formatBytes(n); +}; + +const StatValue = ({ value }: { value: string }) => ( + {value} +); + +const StatRow = ({ label, value }: { label: string; value: string }) => ( + + + +); + +const ResourceStatsSection = () => { + const t = useTranslate(); + const queryClient = useQueryClient(); + const { data, isLoading, isError, isFetching } = useInstanceStats(); + + const unknown = t("setting.resource-stats.unknown"); + const generatedTime = data?.generatedTime + ? t("setting.resource-stats.last-updated", { + ago: formatRelativeTime(new Date(Number(data.generatedTime.seconds) * 1000)), + }) + : undefined; + + return ( + + {generatedTime ? {generatedTime} : null} + + + } + > + {isError ?
{t("setting.resource-stats.load-error")}
: null} + + {isLoading && !data ? ( + +
+
+ ) : null} + + {data ? ( + <> + + + + + + + + + + + + + + ) : null} +
+ ); +}; + +export default ResourceStatsSection; diff --git a/web/src/components/Settings/settingSections.ts b/web/src/components/Settings/settingSections.ts index f2242122e..d04e9fa92 100644 --- a/web/src/components/Settings/settingSections.ts +++ b/web/src/components/Settings/settingSections.ts @@ -1,4 +1,5 @@ import { + BarChart3Icon, CogIcon, DatabaseIcon, HeartHandshakeIcon, @@ -20,6 +21,7 @@ import MemoRelatedSettings from "@/components/Settings/MemoRelatedSettings"; import MyAccountSection from "@/components/Settings/MyAccountSection"; import NotificationSection from "@/components/Settings/NotificationSection"; import PreferencesSection from "@/components/Settings/PreferencesSection"; +import ResourceStatsSection from "@/components/Settings/ResourceStatsSection"; import SSOSection from "@/components/Settings/SSOSection"; import StorageSection from "@/components/Settings/StorageSection"; import TagsSection from "@/components/Settings/TagsSection"; @@ -37,7 +39,8 @@ export type SettingSectionKey = | "notification" | "sso" | "tags" - | "ai"; + | "ai" + | "resource-stats"; type SettingSectionScope = "basic" | "admin"; @@ -132,6 +135,13 @@ export const SETTINGS_SECTIONS: SettingSectionDefinition[] = [ component: AISection, preloadSettingKeys: [InstanceSetting_Key.AI], }, + { + key: "resource-stats", + scope: "admin", + labelKey: "setting.resource-stats.label", + icon: BarChart3Icon, + component: ResourceStatsSection, + }, ]; export const DEFAULT_SETTING_SECTION: SettingSectionKey = "my-account"; diff --git a/web/src/hooks/useInstanceQueries.ts b/web/src/hooks/useInstanceQueries.ts index 51124fc11..f48fba6e0 100644 --- a/web/src/hooks/useInstanceQueries.ts +++ b/web/src/hooks/useInstanceQueries.ts @@ -8,6 +8,7 @@ export const instanceKeys = { profile: () => [...instanceKeys.all, "profile"] as const, settings: () => [...instanceKeys.all, "settings"] as const, setting: (key: InstanceSetting_Key) => [...instanceKeys.settings(), key] as const, + stats: () => [...instanceKeys.all, "stats"] as const, }; // Build setting name from key @@ -16,6 +17,15 @@ const buildInstanceSettingName = (key: InstanceSetting_Key): string => { return `instance/settings/${keyName}`; }; +// Hook to fetch instance resource statistics. Admin only on the server side. +export function useInstanceStats() { + return useQuery({ + queryKey: instanceKeys.stats(), + queryFn: () => instanceServiceClient.getInstanceStats({}), + staleTime: 60_000, // 60s — matches server-side cache TTL + }); +} + // Hook to fetch instance profile export function useInstanceProfile() { return useQuery({ diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 671fede71..10156590d 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -744,6 +744,24 @@ "invalid-regex": "Invalid or unsafe regex pattern.", "used-count": "{{count}} memos", "using-default-color": "Using default color." + }, + "resource-stats": { + "label": "Resources", + "title": "Resource Statistics", + "description": "Resource usage for this instance.", + "database": { + "title": "Database", + "driver": "Driver", + "size": "Size" + }, + "local-storage": { + "title": "Local Storage", + "size": "Data directory size" + }, + "unknown": "Unknown", + "last-updated": "Last updated {{ago}}", + "refresh": "Refresh", + "load-error": "Failed to load statistics" } }, "tag": { diff --git a/web/src/types/proto/api/v1/instance_service_pb.ts b/web/src/types/proto/api/v1/instance_service_pb.ts index 9b2c38a1b..46a60a9a1 100644 --- a/web/src/types/proto/api/v1/instance_service_pb.ts +++ b/web/src/types/proto/api/v1/instance_service_pb.ts @@ -10,8 +10,8 @@ import { file_google_api_annotations } from "../../google/api/annotations_pb"; import { file_google_api_client } from "../../google/api/client_pb"; import { file_google_api_field_behavior } from "../../google/api/field_behavior_pb"; import { file_google_api_resource } from "../../google/api/resource_pb"; -import type { EmptySchema, FieldMask } from "@bufbuild/protobuf/wkt"; -import { file_google_protobuf_empty, file_google_protobuf_field_mask } from "@bufbuild/protobuf/wkt"; +import type { EmptySchema, FieldMask, Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import type { Color } from "../../google/type/color_pb"; import { file_google_type_color } from "../../google/type/color_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -20,7 +20,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file api/v1/instance_service.proto. */ export const file_api_v1_instance_service: GenFile = /*@__PURE__*/ - fileDesc("Ch1hcGkvdjEvaW5zdGFuY2Vfc2VydmljZS5wcm90bxIMbWVtb3MuYXBpLnYxInkKD0luc3RhbmNlUHJvZmlsZRIPCgd2ZXJzaW9uGAIgASgJEgwKBGRlbW8YAyABKAgSFAoMaW5zdGFuY2VfdXJsGAYgASgJEiEKBWFkbWluGAcgASgLMhIubWVtb3MuYXBpLnYxLlVzZXISDgoGY29tbWl0GAggASgJIhsKGUdldEluc3RhbmNlUHJvZmlsZVJlcXVlc3QiqxQKD0luc3RhbmNlU2V0dGluZxIRCgRuYW1lGAEgASgJQgPgQQgSRwoPZ2VuZXJhbF9zZXR0aW5nGAIgASgLMiwubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5HZW5lcmFsU2V0dGluZ0gAEkcKD3N0b3JhZ2Vfc2V0dGluZxgDIAEoCzIsLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmdIABJQChRtZW1vX3JlbGF0ZWRfc2V0dGluZxgEIAEoCzIwLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuTWVtb1JlbGF0ZWRTZXR0aW5nSAASQQoMdGFnc19zZXR0aW5nGAUgASgLMikubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5UYWdzU2V0dGluZ0gAElEKFG5vdGlmaWNhdGlvbl9zZXR0aW5nGAYgASgLMjEubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5Ob3RpZmljYXRpb25TZXR0aW5nSAASPQoKYWlfc2V0dGluZxgHIAEoCzInLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuQUlTZXR0aW5nSAAahwMKDkdlbmVyYWxTZXR0aW5nEiIKGmRpc2FsbG93X3VzZXJfcmVnaXN0cmF0aW9uGAIgASgIEh4KFmRpc2FsbG93X3Bhc3N3b3JkX2F1dGgYAyABKAgSGQoRYWRkaXRpb25hbF9zY3JpcHQYBCABKAkSGAoQYWRkaXRpb25hbF9zdHlsZRgFIAEoCRJSCg5jdXN0b21fcHJvZmlsZRgGIAEoCzI6Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuR2VuZXJhbFNldHRpbmcuQ3VzdG9tUHJvZmlsZRIdChV3ZWVrX3N0YXJ0X2RheV9vZmZzZXQYByABKAUSIAoYZGlzYWxsb3dfY2hhbmdlX3VzZXJuYW1lGAggASgIEiAKGGRpc2FsbG93X2NoYW5nZV9uaWNrbmFtZRgJIAEoCBpFCg1DdXN0b21Qcm9maWxlEg0KBXRpdGxlGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhAKCGxvZ29fdXJsGAMgASgJGr8DCg5TdG9yYWdlU2V0dGluZxJOCgxzdG9yYWdlX3R5cGUYASABKA4yOC5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLlN0b3JhZ2VTZXR0aW5nLlN0b3JhZ2VUeXBlEhkKEWZpbGVwYXRoX3RlbXBsYXRlGAIgASgJEhwKFHVwbG9hZF9zaXplX2xpbWl0X21iGAMgASgDEkgKCXMzX2NvbmZpZxgEIAEoCzI1Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmcuUzNDb25maWcaiwEKCFMzQ29uZmlnEhUKDWFjY2Vzc19rZXlfaWQYASABKAkSHgoRYWNjZXNzX2tleV9zZWNyZXQYAiABKAlCA+BBBBIQCghlbmRwb2ludBgDIAEoCRIOCgZyZWdpb24YBCABKAkSDgoGYnVja2V0GAUgASgJEhYKDnVzZV9wYXRoX3N0eWxlGAYgASgIIkwKC1N0b3JhZ2VUeXBlEhwKGFNUT1JBR0VfVFlQRV9VTlNQRUNJRklFRBAAEgwKCERBVEFCQVNFEAESCQoFTE9DQUwQAhIGCgJTMxADGocBChJNZW1vUmVsYXRlZFNldHRpbmcSHAoUY29udGVudF9sZW5ndGhfbGltaXQYAyABKAUSIAoYZW5hYmxlX2RvdWJsZV9jbGlja19lZGl0GAQgASgIEhEKCXJlYWN0aW9ucxgHIAMoCUoECAIQA1IYZGlzcGxheV93aXRoX3VwZGF0ZV90aW1lGlEKC1RhZ01ldGFkYXRhEiwKEGJhY2tncm91bmRfY29sb3IYASABKAsyEi5nb29nbGUudHlwZS5Db2xvchIUCgxibHVyX2NvbnRlbnQYAiABKAgaqAEKC1RhZ3NTZXR0aW5nEkEKBHRhZ3MYASADKAsyMy5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLlRhZ3NTZXR0aW5nLlRhZ3NFbnRyeRpWCglUYWdzRW50cnkSCwoDa2V5GAEgASgJEjgKBXZhbHVlGAIgASgLMikubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5UYWdNZXRhZGF0YToCOAEaugIKE05vdGlmaWNhdGlvblNldHRpbmcSTQoFZW1haWwYASABKAsyPi5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLk5vdGlmaWNhdGlvblNldHRpbmcuRW1haWxTZXR0aW5nGtMBCgxFbWFpbFNldHRpbmcSDwoHZW5hYmxlZBgBIAEoCBIRCglzbXRwX2hvc3QYAiABKAkSEQoJc210cF9wb3J0GAMgASgFEhUKDXNtdHBfdXNlcm5hbWUYBCABKAkSGgoNc210cF9wYXNzd29yZBgFIAEoCUID4EEEEhIKCmZyb21fZW1haWwYBiABKAkSEQoJZnJvbV9uYW1lGAcgASgJEhAKCHJlcGx5X3RvGAggASgJEg8KB3VzZV90bHMYCSABKAgSDwoHdXNlX3NzbBgKIAEoCBpOCglBSVNldHRpbmcSQQoJcHJvdmlkZXJzGAEgAygLMi4ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5BSVByb3ZpZGVyQ29uZmlnGsYBChBBSVByb3ZpZGVyQ29uZmlnEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEjoKBHR5cGUYAyABKA4yLC5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLkFJUHJvdmlkZXJUeXBlEhAKCGVuZHBvaW50GAQgASgJEhQKB2FwaV9rZXkYBSABKAlCA+BBBBIYCgthcGlfa2V5X3NldBgIIAEoCEID4EEDEhkKDGFwaV9rZXlfaGludBgJIAEoCUID4EEDImoKA0tleRITCg9LRVlfVU5TUEVDSUZJRUQQABILCgdHRU5FUkFMEAESCwoHU1RPUkFHRRACEhAKDE1FTU9fUkVMQVRFRBADEggKBFRBR1MQBBIQCgxOT1RJRklDQVRJT04QBRIGCgJBSRAGIkoKDkFJUHJvdmlkZXJUeXBlEiAKHEFJX1BST1ZJREVSX1RZUEVfVU5TUEVDSUZJRUQQABIKCgZPUEVOQUkQARIKCgZHRU1JTkkQAjph6kFeChxtZW1vcy5hcGkudjEvSW5zdGFuY2VTZXR0aW5nEhtpbnN0YW5jZS9zZXR0aW5ncy97c2V0dGluZ30qEGluc3RhbmNlU2V0dGluZ3MyD2luc3RhbmNlU2V0dGluZ0IHCgV2YWx1ZSJPChlHZXRJbnN0YW5jZVNldHRpbmdSZXF1ZXN0EjIKBG5hbWUYASABKAlCJOBBAvpBHgocbWVtb3MuYXBpLnYxL0luc3RhbmNlU2V0dGluZyKJAQocVXBkYXRlSW5zdGFuY2VTZXR0aW5nUmVxdWVzdBIzCgdzZXR0aW5nGAEgASgLMh0ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZ0ID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EEBIpMBCh9UZXN0SW5zdGFuY2VFbWFpbFNldHRpbmdSZXF1ZXN0ElIKBWVtYWlsGAEgASgLMj4ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5Ob3RpZmljYXRpb25TZXR0aW5nLkVtYWlsU2V0dGluZ0ID4EEBEhwKD3JlY2lwaWVudF9lbWFpbBgCIAEoCUID4EEBMvwECg9JbnN0YW5jZVNlcnZpY2USfgoSR2V0SW5zdGFuY2VQcm9maWxlEicubWVtb3MuYXBpLnYxLkdldEluc3RhbmNlUHJvZmlsZVJlcXVlc3QaHS5tZW1vcy5hcGkudjEuSW5zdGFuY2VQcm9maWxlIiCC0+STAhoSGC9hcGkvdjEvaW5zdGFuY2UvcHJvZmlsZRKPAQoSR2V0SW5zdGFuY2VTZXR0aW5nEicubWVtb3MuYXBpLnYxLkdldEluc3RhbmNlU2V0dGluZ1JlcXVlc3QaHS5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nIjHaQQRuYW1lgtPkkwIkEiIvYXBpL3YxL3tuYW1lPWluc3RhbmNlL3NldHRpbmdzLyp9ErUBChVVcGRhdGVJbnN0YW5jZVNldHRpbmcSKi5tZW1vcy5hcGkudjEuVXBkYXRlSW5zdGFuY2VTZXR0aW5nUmVxdWVzdBodLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmciUdpBE3NldHRpbmcsdXBkYXRlX21hc2uC0+STAjU6B3NldHRpbmcyKi9hcGkvdjEve3NldHRpbmcubmFtZT1pbnN0YW5jZS9zZXR0aW5ncy8qfRKeAQoYVGVzdEluc3RhbmNlRW1haWxTZXR0aW5nEi0ubWVtb3MuYXBpLnYxLlRlc3RJbnN0YW5jZUVtYWlsU2V0dGluZ1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiO4LT5JMCNToBKiIwL2FwaS92MS9pbnN0YW5jZS9zZXR0aW5ncy9ub3RpZmljYXRpb246dGVzdEVtYWlsQqwBChBjb20ubWVtb3MuYXBpLnYxQhRJbnN0YW5jZVNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z", [file_api_v1_user_service, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_type_color]); + fileDesc("Ch1hcGkvdjEvaW5zdGFuY2Vfc2VydmljZS5wcm90bxIMbWVtb3MuYXBpLnYxInkKD0luc3RhbmNlUHJvZmlsZRIPCgd2ZXJzaW9uGAIgASgJEgwKBGRlbW8YAyABKAgSFAoMaW5zdGFuY2VfdXJsGAYgASgJEiEKBWFkbWluGAcgASgLMhIubWVtb3MuYXBpLnYxLlVzZXISDgoGY29tbWl0GAggASgJIhsKGUdldEluc3RhbmNlUHJvZmlsZVJlcXVlc3QiqxQKD0luc3RhbmNlU2V0dGluZxIRCgRuYW1lGAEgASgJQgPgQQgSRwoPZ2VuZXJhbF9zZXR0aW5nGAIgASgLMiwubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5HZW5lcmFsU2V0dGluZ0gAEkcKD3N0b3JhZ2Vfc2V0dGluZxgDIAEoCzIsLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmdIABJQChRtZW1vX3JlbGF0ZWRfc2V0dGluZxgEIAEoCzIwLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuTWVtb1JlbGF0ZWRTZXR0aW5nSAASQQoMdGFnc19zZXR0aW5nGAUgASgLMikubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5UYWdzU2V0dGluZ0gAElEKFG5vdGlmaWNhdGlvbl9zZXR0aW5nGAYgASgLMjEubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5Ob3RpZmljYXRpb25TZXR0aW5nSAASPQoKYWlfc2V0dGluZxgHIAEoCzInLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuQUlTZXR0aW5nSAAahwMKDkdlbmVyYWxTZXR0aW5nEiIKGmRpc2FsbG93X3VzZXJfcmVnaXN0cmF0aW9uGAIgASgIEh4KFmRpc2FsbG93X3Bhc3N3b3JkX2F1dGgYAyABKAgSGQoRYWRkaXRpb25hbF9zY3JpcHQYBCABKAkSGAoQYWRkaXRpb25hbF9zdHlsZRgFIAEoCRJSCg5jdXN0b21fcHJvZmlsZRgGIAEoCzI6Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuR2VuZXJhbFNldHRpbmcuQ3VzdG9tUHJvZmlsZRIdChV3ZWVrX3N0YXJ0X2RheV9vZmZzZXQYByABKAUSIAoYZGlzYWxsb3dfY2hhbmdlX3VzZXJuYW1lGAggASgIEiAKGGRpc2FsbG93X2NoYW5nZV9uaWNrbmFtZRgJIAEoCBpFCg1DdXN0b21Qcm9maWxlEg0KBXRpdGxlGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhAKCGxvZ29fdXJsGAMgASgJGr8DCg5TdG9yYWdlU2V0dGluZxJOCgxzdG9yYWdlX3R5cGUYASABKA4yOC5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLlN0b3JhZ2VTZXR0aW5nLlN0b3JhZ2VUeXBlEhkKEWZpbGVwYXRoX3RlbXBsYXRlGAIgASgJEhwKFHVwbG9hZF9zaXplX2xpbWl0X21iGAMgASgDEkgKCXMzX2NvbmZpZxgEIAEoCzI1Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmcuUzNDb25maWcaiwEKCFMzQ29uZmlnEhUKDWFjY2Vzc19rZXlfaWQYASABKAkSHgoRYWNjZXNzX2tleV9zZWNyZXQYAiABKAlCA+BBBBIQCghlbmRwb2ludBgDIAEoCRIOCgZyZWdpb24YBCABKAkSDgoGYnVja2V0GAUgASgJEhYKDnVzZV9wYXRoX3N0eWxlGAYgASgIIkwKC1N0b3JhZ2VUeXBlEhwKGFNUT1JBR0VfVFlQRV9VTlNQRUNJRklFRBAAEgwKCERBVEFCQVNFEAESCQoFTE9DQUwQAhIGCgJTMxADGocBChJNZW1vUmVsYXRlZFNldHRpbmcSHAoUY29udGVudF9sZW5ndGhfbGltaXQYAyABKAUSIAoYZW5hYmxlX2RvdWJsZV9jbGlja19lZGl0GAQgASgIEhEKCXJlYWN0aW9ucxgHIAMoCUoECAIQA1IYZGlzcGxheV93aXRoX3VwZGF0ZV90aW1lGlEKC1RhZ01ldGFkYXRhEiwKEGJhY2tncm91bmRfY29sb3IYASABKAsyEi5nb29nbGUudHlwZS5Db2xvchIUCgxibHVyX2NvbnRlbnQYAiABKAgaqAEKC1RhZ3NTZXR0aW5nEkEKBHRhZ3MYASADKAsyMy5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLlRhZ3NTZXR0aW5nLlRhZ3NFbnRyeRpWCglUYWdzRW50cnkSCwoDa2V5GAEgASgJEjgKBXZhbHVlGAIgASgLMikubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5UYWdNZXRhZGF0YToCOAEaugIKE05vdGlmaWNhdGlvblNldHRpbmcSTQoFZW1haWwYASABKAsyPi5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLk5vdGlmaWNhdGlvblNldHRpbmcuRW1haWxTZXR0aW5nGtMBCgxFbWFpbFNldHRpbmcSDwoHZW5hYmxlZBgBIAEoCBIRCglzbXRwX2hvc3QYAiABKAkSEQoJc210cF9wb3J0GAMgASgFEhUKDXNtdHBfdXNlcm5hbWUYBCABKAkSGgoNc210cF9wYXNzd29yZBgFIAEoCUID4EEEEhIKCmZyb21fZW1haWwYBiABKAkSEQoJZnJvbV9uYW1lGAcgASgJEhAKCHJlcGx5X3RvGAggASgJEg8KB3VzZV90bHMYCSABKAgSDwoHdXNlX3NzbBgKIAEoCBpOCglBSVNldHRpbmcSQQoJcHJvdmlkZXJzGAEgAygLMi4ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5BSVByb3ZpZGVyQ29uZmlnGsYBChBBSVByb3ZpZGVyQ29uZmlnEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEjoKBHR5cGUYAyABKA4yLC5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLkFJUHJvdmlkZXJUeXBlEhAKCGVuZHBvaW50GAQgASgJEhQKB2FwaV9rZXkYBSABKAlCA+BBBBIYCgthcGlfa2V5X3NldBgIIAEoCEID4EEDEhkKDGFwaV9rZXlfaGludBgJIAEoCUID4EEDImoKA0tleRITCg9LRVlfVU5TUEVDSUZJRUQQABILCgdHRU5FUkFMEAESCwoHU1RPUkFHRRACEhAKDE1FTU9fUkVMQVRFRBADEggKBFRBR1MQBBIQCgxOT1RJRklDQVRJT04QBRIGCgJBSRAGIkoKDkFJUHJvdmlkZXJUeXBlEiAKHEFJX1BST1ZJREVSX1RZUEVfVU5TUEVDSUZJRUQQABIKCgZPUEVOQUkQARIKCgZHRU1JTkkQAjph6kFeChxtZW1vcy5hcGkudjEvSW5zdGFuY2VTZXR0aW5nEhtpbnN0YW5jZS9zZXR0aW5ncy97c2V0dGluZ30qEGluc3RhbmNlU2V0dGluZ3MyD2luc3RhbmNlU2V0dGluZ0IHCgV2YWx1ZSJPChlHZXRJbnN0YW5jZVNldHRpbmdSZXF1ZXN0EjIKBG5hbWUYASABKAlCJOBBAvpBHgocbWVtb3MuYXBpLnYxL0luc3RhbmNlU2V0dGluZyKJAQocVXBkYXRlSW5zdGFuY2VTZXR0aW5nUmVxdWVzdBIzCgdzZXR0aW5nGAEgASgLMh0ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZ0ID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EEBIpMBCh9UZXN0SW5zdGFuY2VFbWFpbFNldHRpbmdSZXF1ZXN0ElIKBWVtYWlsGAEgASgLMj4ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5Ob3RpZmljYXRpb25TZXR0aW5nLkVtYWlsU2V0dGluZ0ID4EEBEhwKD3JlY2lwaWVudF9lbWFpbBgCIAEoCUID4EEBIhkKF0dldEluc3RhbmNlU3RhdHNSZXF1ZXN0ItIBCg1JbnN0YW5jZVN0YXRzEjsKCGRhdGFiYXNlGAEgASgLMikubWVtb3MuYXBpLnYxLkluc3RhbmNlU3RhdHMuRGF0YWJhc2VTdGF0cxIbChNsb2NhbF9zdG9yYWdlX2J5dGVzGAIgASgDEjIKDmdlbmVyYXRlZF90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBozCg1EYXRhYmFzZVN0YXRzEg4KBmRyaXZlchgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgDMvQFCg9JbnN0YW5jZVNlcnZpY2USfgoSR2V0SW5zdGFuY2VQcm9maWxlEicubWVtb3MuYXBpLnYxLkdldEluc3RhbmNlUHJvZmlsZVJlcXVlc3QaHS5tZW1vcy5hcGkudjEuSW5zdGFuY2VQcm9maWxlIiCC0+STAhoSGC9hcGkvdjEvaW5zdGFuY2UvcHJvZmlsZRKPAQoSR2V0SW5zdGFuY2VTZXR0aW5nEicubWVtb3MuYXBpLnYxLkdldEluc3RhbmNlU2V0dGluZ1JlcXVlc3QaHS5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nIjHaQQRuYW1lgtPkkwIkEiIvYXBpL3YxL3tuYW1lPWluc3RhbmNlL3NldHRpbmdzLyp9ErUBChVVcGRhdGVJbnN0YW5jZVNldHRpbmcSKi5tZW1vcy5hcGkudjEuVXBkYXRlSW5zdGFuY2VTZXR0aW5nUmVxdWVzdBodLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmciUdpBE3NldHRpbmcsdXBkYXRlX21hc2uC0+STAjU6B3NldHRpbmcyKi9hcGkvdjEve3NldHRpbmcubmFtZT1pbnN0YW5jZS9zZXR0aW5ncy8qfRKeAQoYVGVzdEluc3RhbmNlRW1haWxTZXR0aW5nEi0ubWVtb3MuYXBpLnYxLlRlc3RJbnN0YW5jZUVtYWlsU2V0dGluZ1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiO4LT5JMCNToBKiIwL2FwaS92MS9pbnN0YW5jZS9zZXR0aW5ncy9ub3RpZmljYXRpb246dGVzdEVtYWlsEnYKEEdldEluc3RhbmNlU3RhdHMSJS5tZW1vcy5hcGkudjEuR2V0SW5zdGFuY2VTdGF0c1JlcXVlc3QaGy5tZW1vcy5hcGkudjEuSW5zdGFuY2VTdGF0cyIegtPkkwIYEhYvYXBpL3YxL2luc3RhbmNlL3N0YXRzQqwBChBjb20ubWVtb3MuYXBpLnYxQhRJbnN0YW5jZVNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z", [file_api_v1_user_service, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp, file_google_type_color]); /** * Instance profile message containing basic instance information. @@ -791,6 +791,82 @@ export type TestInstanceEmailSettingRequest = Message<"memos.api.v1.TestInstance export const TestInstanceEmailSettingRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_api_v1_instance_service, 5); +/** + * Request message for GetInstanceStats. + * + * @generated from message memos.api.v1.GetInstanceStatsRequest + */ +export type GetInstanceStatsRequest = Message<"memos.api.v1.GetInstanceStatsRequest"> & { +}; + +/** + * Describes the message memos.api.v1.GetInstanceStatsRequest. + * Use `create(GetInstanceStatsRequestSchema)` to create a new message. + */ +export const GetInstanceStatsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_instance_service, 6); + +/** + * Resource usage statistics for the instance. + * + * @generated from message memos.api.v1.InstanceStats + */ +export type InstanceStats = Message<"memos.api.v1.InstanceStats"> & { + /** + * @generated from field: memos.api.v1.InstanceStats.DatabaseStats database = 1; + */ + database?: InstanceStats_DatabaseStats | undefined; + + /** + * Recursive size of the data directory in bytes. -1 if unavailable. + * + * @generated from field: int64 local_storage_bytes = 2; + */ + localStorageBytes: bigint; + + /** + * Server-side timestamp when the snapshot was generated. + * + * @generated from field: google.protobuf.Timestamp generated_time = 4; + */ + generatedTime?: Timestamp | undefined; +}; + +/** + * Describes the message memos.api.v1.InstanceStats. + * Use `create(InstanceStatsSchema)` to create a new message. + */ +export const InstanceStatsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_instance_service, 7); + +/** + * Database size statistics. + * + * @generated from message memos.api.v1.InstanceStats.DatabaseStats + */ +export type InstanceStats_DatabaseStats = Message<"memos.api.v1.InstanceStats.DatabaseStats"> & { + /** + * driver is one of "sqlite", "mysql", "postgres". + * + * @generated from field: string driver = 1; + */ + driver: string; + + /** + * size_bytes is the database size in bytes; -1 if unavailable. + * + * @generated from field: int64 size_bytes = 2; + */ + sizeBytes: bigint; +}; + +/** + * Describes the message memos.api.v1.InstanceStats.DatabaseStats. + * Use `create(InstanceStats_DatabaseStatsSchema)` to create a new message. + */ +export const InstanceStats_DatabaseStatsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_instance_service, 7, 0); + /** * @generated from service memos.api.v1.InstanceService */ @@ -835,6 +911,16 @@ export const InstanceService: GenService<{ input: typeof TestInstanceEmailSettingRequestSchema; output: typeof EmptySchema; }, + /** + * GetInstanceStats returns resource usage statistics for the instance. Admin only. + * + * @generated from rpc memos.api.v1.InstanceService.GetInstanceStats + */ + getInstanceStats: { + methodKind: "unary"; + input: typeof GetInstanceStatsRequestSchema; + output: typeof InstanceStatsSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_api_v1_instance_service, 0);