mirror of
https://github.com/usememos/memos.git
synced 2026-05-02 18:52:32 +00:00
feat(stats): admin instance resource statistics
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(), `<loc>https://demo.usememos.com/m/publicmemo</loc>`)
|
||||
require.Contains(t, rec.Body.String(), `<loc>https://demo.usememos.com/memos/publicmemo</loc>`)
|
||||
require.NotContains(t, rec.Body.String(), "privatememo")
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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 }) => (
|
||||
<span className="block min-w-0 max-w-full break-all text-right font-mono text-sm tabular-nums text-foreground">{value}</span>
|
||||
);
|
||||
|
||||
const StatRow = ({ label, value }: { label: string; value: string }) => (
|
||||
<SettingListItem label={label} controlClassName="w-full justify-end sm:w-auto">
|
||||
<StatValue value={value} />
|
||||
</SettingListItem>
|
||||
);
|
||||
|
||||
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 (
|
||||
<SettingSection
|
||||
title={t("setting.resource-stats.title")}
|
||||
description={t("setting.resource-stats.description")}
|
||||
actions={
|
||||
<>
|
||||
{generatedTime ? <span className="text-xs text-muted-foreground">{generatedTime}</span> : null}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isFetching}
|
||||
onClick={() => void queryClient.invalidateQueries({ queryKey: instanceKeys.stats() })}
|
||||
>
|
||||
<RefreshCwIcon className="mr-1 size-4" />
|
||||
{t("setting.resource-stats.refresh")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{isError ? <div className="text-destructive text-sm">{t("setting.resource-stats.load-error")}</div> : null}
|
||||
|
||||
{isLoading && !data ? (
|
||||
<SettingPanel>
|
||||
<div className="px-3 py-3 text-sm text-muted-foreground">…</div>
|
||||
</SettingPanel>
|
||||
) : null}
|
||||
|
||||
{data ? (
|
||||
<>
|
||||
<SettingGroup title={t("setting.resource-stats.database.title")}>
|
||||
<SettingList>
|
||||
<StatRow label={t("setting.resource-stats.database.driver")} value={data.database?.driver || unknown} />
|
||||
<StatRow label={t("setting.resource-stats.database.size")} value={renderBytes(data.database?.sizeBytes, unknown)} />
|
||||
</SettingList>
|
||||
</SettingGroup>
|
||||
|
||||
<SettingGroup title={t("setting.resource-stats.local-storage.title")} showSeparator>
|
||||
<SettingList>
|
||||
<StatRow label={t("setting.resource-stats.local-storage.size")} value={renderBytes(data.localStorageBytes, unknown)} />
|
||||
</SettingList>
|
||||
</SettingGroup>
|
||||
</>
|
||||
) : null}
|
||||
</SettingSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceStatsSection;
|
||||
@@ -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";
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user