diff --git a/AGENTS.md b/AGENTS.md index f6c961a6a83a5..94644eee12c10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,8 +24,9 @@ Memos is a self-hosted note-taking app. ## Documentation - ADRs and their companion glossary have been retired. Do not recreate them unless explicitly requested. -- Design documents in `docs/design/` are frozen after implementation. Do not update implemented designs to track later code changes. -- New and unimplemented designs may still evolve. Frozen documents record the original design; verify current behavior in source code and tests. +- Design documents in `docs/design/` stay editable until the feature ships in a tagged release; keep a merged-but-unreleased design in step + with its implementation. After release the body is frozen: record later design changes in a dated `## Amendments` section at the end, never by + rewriting the body. See `docs/design/README.md`. Verify current behavior in source code and tests, not in design documents. - Brand copy (tagline, descriptions, README intro) comes verbatim from [`BRAND.md`](https://github.com/usememos/.github/blob/main/BRAND.md) in `usememos/.github`. Change it there first; do not keep brand guidelines in this repo. - Keep contributor guidance in this file and package-local `README.md` or `doc.go` files current when relevant code changes. diff --git a/core/usagelimit/usagelimit.go b/core/usagelimit/usagelimit.go index f871939b0b38b..68f14a1b1a207 100644 --- a/core/usagelimit/usagelimit.go +++ b/core/usagelimit/usagelimit.go @@ -1,17 +1,19 @@ -// Package usagelimit resolves the usage limits that apply to a user and -// decides whether creating or editing a memo, or uploading a file, stays within them. +// Package usagelimit resolves the limit set that applies to a user and decides +// whether creating or editing a memo, or uploading a file, stays within it. // -// A usage limit sets one value for one metric, for every user (a default) or -// for one user. For each metric, the user's own limit applies if there is -// one, otherwise the default, otherwise the metric is unlimited. A value of 0 -// is unlimited. Usage is counted live on every check, so there are no stored -// counters that can drift; concurrent requests can therefore overshoot a limit -// by the number of requests in flight. +// The usage limits setting is an ordered list of entries. Each entry names the +// users it applies to, or applies to every user, and carries an optional limit +// per metric. The first entry that names a user is that user's limit set, +// whole; the last entry applies to every user and is the default. An unset +// limit is unlimited, as is 0. Usage is counted live on every check, so there +// are no stored counters that can drift; concurrent requests can therefore +// overshoot a limit by the number of requests in flight. package usagelimit import ( "context" "fmt" + "slices" "github.com/pkg/errors" @@ -19,14 +21,14 @@ import ( ) // Metric is what a usage limit counts. -type Metric = storepb.InstanceUsageLimitsSetting_Metric +type Metric string // The metrics a usage limit can set. const ( - MetricMemoCount = storepb.InstanceUsageLimitsSetting_MEMO_COUNT - MetricStorageBytes = storepb.InstanceUsageLimitsSetting_STORAGE_BYTES - MetricUploadBytes = storepb.InstanceUsageLimitsSetting_UPLOAD_BYTES - MetricMemoContentBytes = storepb.InstanceUsageLimitsSetting_MEMO_CONTENT_BYTES + MetricMemoCount Metric = "MEMO_COUNT" + MetricMemoContentBytes Metric = "MEMO_CONTENT_BYTES" + MetricStorageBytes Metric = "STORAGE_BYTES" + MetricUploadBytes Metric = "UPLOAD_BYTES" ) // Store is the data the checks read. *store.Store implements it. @@ -36,88 +38,45 @@ type Store interface { GetAttachmentStorageUsage(ctx context.Context, creatorID int32) (int64, error) } -// Limit is the limit in effect for one metric. -type Limit struct { - // Value is the limit. 0 means unlimited. - Value int64 - // Source is the ID of the usage limit the value comes from, or empty when - // no usage limit applies. - Source string -} - -// Limited reports whether the limit restricts anything. -func (l Limit) Limited() bool { - return l.Value > 0 -} - -// Effective is the limit in effect for each metric of one user. +// Effective is the limit set in effect for one user. type Effective struct { - MemoCount Limit - StorageBytes Limit - UploadBytes Limit - MemoContentBytes Limit + // Entry is the usage limits entry that applies, or nil when none does, in + // which case every metric is unlimited. Its getters are nil-safe and return + // 0, unlimited, for an unset value. + Entry *storepb.InstanceUsageLimitsSetting_UsageLimit + // Index is the position of Entry in the setting, or -1 when Entry is nil. + Index int } -// Resolve returns the limits in effect for userID: for each metric, the user's -// own limit, otherwise the default, otherwise unlimited. +// Resolve returns the limit set in effect for userID: the first entry that +// names the user, else the first entry for all users, else no entry. func Resolve(setting *storepb.InstanceUsageLimitsSetting, userID int32) Effective { - var effective Effective - for _, limit := range setting.GetLimits() { + for i, limit := range setting.GetLimits() { if limit == nil { continue } - selected := effective.get(limit.Metric) - if selected == nil { - continue - } - switch target := limit.Target.(type) { - case *storepb.InstanceUsageLimitsSetting_UsageLimit_AllUsers: - // A user's own limit replaces the default, whatever the order. - if target == nil || target.AllUsers == nil || selected.Source != "" { - continue - } - case *storepb.InstanceUsageLimitsSetting_UsageLimit_UserId: - if target == nil || target.UserId <= 0 || target.UserId != userID { - continue - } - default: - continue + if limit.AllUsers || slices.Contains(limit.UserIds, userID) { + return Effective{Entry: limit, Index: i} } - *selected = Limit{Value: limit.Value, Source: limit.Uid} - } - return effective -} - -func (e *Effective) get(metric Metric) *Limit { - switch metric { - case MetricMemoCount: - return &e.MemoCount - case MetricStorageBytes: - return &e.StorageBytes - case MetricUploadBytes: - return &e.UploadBytes - case MetricMemoContentBytes: - return &e.MemoContentBytes - default: - return nil } + return Effective{Index: -1} } // Refusal is returned when an action would exceed a usage limit. type Refusal struct { Metric Metric - // Limit is the value of the usage limit that applied. + // Limit is the value of the limit that applied. Limit int64 // Usage is the current usage: the memo count, the stored bytes, or for // UPLOAD_BYTES the size of the refused file, or for MEMO_CONTENT_BYTES the // size of the refused memo content. Usage int64 - // Source is the ID of the usage limit that applied. - Source string + // Index is the position of the entry that applied in the setting. + Index int } func (r *Refusal) Error() string { - return fmt.Sprintf("usage limit %s exceeded: %s limit %d, usage %d", r.Source, r.Metric, r.Limit, r.Usage) + return fmt.Sprintf("usage limit exceeded: %s limit %d, usage %d (entry %d)", r.Metric, r.Limit, r.Usage, r.Index) } // AsRefusal returns the refusal in err's chain, or nil. @@ -138,6 +97,10 @@ func EffectiveFor(ctx context.Context, s Store, userID int32) (Effective, error) return Resolve(setting, userID), nil } +func (e Effective) refuse(metric Metric, limit, usage int64) error { + return &Refusal{Metric: metric, Limit: limit, Usage: usage, Index: e.Index} +} + // CheckMemoCreate returns a *Refusal when userID has reached the memo limit. // With no memo limit in effect it runs no count query. func CheckMemoCreate(ctx context.Context, s Store, userID int32) error { @@ -145,15 +108,16 @@ func CheckMemoCreate(ctx context.Context, s Store, userID int32) error { if err != nil { return err } - if !effective.MemoCount.Limited() { + limit := effective.Entry.GetMemoCount() + if limit <= 0 { return nil } count, err := s.CountUserMemos(ctx, userID) if err != nil { return errors.Wrap(err, "failed to count memos") } - if count >= effective.MemoCount.Value { - return &Refusal{Metric: MetricMemoCount, Limit: effective.MemoCount.Value, Usage: count, Source: effective.MemoCount.Source} + if count >= limit { + return effective.refuse(MetricMemoCount, limit, count) } return nil } @@ -175,15 +139,15 @@ func CheckMemoContentSize(ctx context.Context, s Store, userID int32, size int64 if err != nil { return err } - if content := effective.MemoContentBytes; content.Limited() && size > content.Value { - return &Refusal{Metric: MetricMemoContentBytes, Limit: content.Value, Usage: size, Source: content.Source} + if limit := effective.Entry.GetMemoContentBytes(); limit > 0 && size > limit { + return effective.refuse(MetricMemoContentBytes, limit, size) } return nil } func (e Effective) checkFileSize(size int64) error { - if upload := e.UploadBytes; upload.Limited() && size > upload.Value { - return &Refusal{Metric: MetricUploadBytes, Limit: upload.Value, Usage: size, Source: upload.Source} + if limit := e.Entry.GetUploadBytes(); limit > 0 && size > limit { + return e.refuse(MetricUploadBytes, limit, size) } return nil } @@ -199,20 +163,21 @@ func CheckUpload(ctx context.Context, s Store, userID int32, size int64) error { if err := effective.checkFileSize(size); err != nil { return err } - if !effective.StorageBytes.Limited() { + limit := effective.Entry.GetStorageBytes() + if limit <= 0 { return nil } used, err := s.GetAttachmentStorageUsage(ctx, userID) if err != nil { return errors.Wrap(err, "failed to sum storage usage") } - if used+size > effective.StorageBytes.Value { - return &Refusal{Metric: MetricStorageBytes, Limit: effective.StorageBytes.Value, Usage: used, Source: effective.StorageBytes.Source} + if used+size > limit { + return effective.refuse(MetricStorageBytes, limit, used) } return nil } -// Usage is a user's usage and the limits in effect for the user. +// Usage is a user's usage and the limit set in effect for the user. type Usage struct { MemoCount int64 StorageBytes int64 diff --git a/core/usagelimit/usagelimit_test.go b/core/usagelimit/usagelimit_test.go index 7e5aa8c58065c..75a20deb2a146 100644 --- a/core/usagelimit/usagelimit_test.go +++ b/core/usagelimit/usagelimit_test.go @@ -6,7 +6,6 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/emptypb" storepb "github.com/usememos/memos/proto/gen/store" ) @@ -41,43 +40,41 @@ func (f *fakeStore) GetAttachmentStorageUsage(context.Context, int32) (int64, er return f.storage, f.countingErr } -func limit(uid string, metric Metric, userID int32, value int64) *storepb.InstanceUsageLimitsSetting_UsageLimit { - limit := &storepb.InstanceUsageLimitsSetting_UsageLimit{ - Uid: uid, Metric: metric, Value: value, - Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_AllUsers{AllUsers: &emptypb.Empty{}}, - } - if userID != 0 { - limit.Target = &storepb.InstanceUsageLimitsSetting_UsageLimit_UserId{UserId: userID} - } - return limit +type entry = storepb.InstanceUsageLimitsSetting_UsageLimit + +func setting(limits ...*entry) *storepb.InstanceUsageLimitsSetting { + return &storepb.InstanceUsageLimitsSetting{Limits: limits} } func TestResolve(t *testing.T) { - const alice, bob = int32(7), int32(8) - setting := &storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{ - // A user's limit listed before the default still wins. - limit("alice-storage", MetricStorageBytes, alice, 5<<30), - limit("default-storage", MetricStorageBytes, 0, 1<<30), - limit("default-memos", MetricMemoCount, 0, 1000), - limit("alice-memos", MetricMemoCount, alice, 0), - limit("bob-upload", MetricUploadBytes, bob, 10<<20), - }} - - require.Equal(t, Effective{ - MemoCount: Limit{Value: 0, Source: "alice-memos"}, - StorageBytes: Limit{Value: 5 << 30, Source: "alice-storage"}, - }, Resolve(setting, alice), "a user's own limit, even unlimited, replaces the default") - require.Equal(t, Effective{ - MemoCount: Limit{Value: 1000, Source: "default-memos"}, - StorageBytes: Limit{Value: 1 << 30, Source: "default-storage"}, - UploadBytes: Limit{Value: 10 << 20, Source: "bob-upload"}, - }, Resolve(setting, bob)) - require.Equal(t, Effective{}, Resolve(&storepb.InstanceUsageLimitsSetting{}, alice), "no limits means unlimited") - invalid := &storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{ - {Uid: "missing-target", Metric: MetricMemoCount, Value: 1}, - {Uid: "zero-user", Metric: MetricStorageBytes, Value: 1, Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_UserId{UserId: 0}}, - }} - require.Equal(t, Effective{}, Resolve(invalid, alice), "an absent target or user ID zero never becomes an all-users rule") + const alice, bob, carol = int32(7), int32(8), int32(9) + aliceEntry := &entry{UserIds: []int32{alice}, StorageBytes: new(int64(5 << 30)), MemoCount: new(int64(0))} + pairEntry := &entry{UserIds: []int32{bob, alice}, StorageBytes: new(int64(2 << 30)), MemoCount: new(int64(10))} + defaultEntry := &entry{AllUsers: true, StorageBytes: new(int64(1 << 30)), MemoCount: new(int64(1000)), UploadBytes: new(int64(10 << 20))} + s := setting(aliceEntry, pairEntry, defaultEntry) + + got := Resolve(s, alice) + require.Same(t, aliceEntry, got.Entry, "the first entry naming the user applies") + require.Equal(t, 0, got.Index) + require.Equal(t, int64(5<<30), got.Entry.GetStorageBytes()) + require.Zero(t, got.Entry.GetMemoCount(), "an explicit zero is unlimited") + require.Zero(t, got.Entry.GetUploadBytes(), "an unset value is unlimited; nothing is inherited from later entries") + + got = Resolve(s, bob) + require.Same(t, pairEntry, got.Entry) + require.Equal(t, 1, got.Index) + require.Equal(t, int64(10), got.Entry.GetMemoCount()) + + got = Resolve(s, carol) + require.Same(t, defaultEntry, got.Entry, "the all-users entry is the default") + require.Equal(t, 2, got.Index) + require.Equal(t, int64(10<<20), got.Entry.GetUploadBytes()) + + got = Resolve(&storepb.InstanceUsageLimitsSetting{}, alice) + require.Nil(t, got.Entry, "no entries means unlimited") + require.Equal(t, -1, got.Index) + require.Zero(t, got.Entry.GetStorageBytes(), "a nil entry reads as unlimited") + require.Nil(t, Resolve(setting(nil, &entry{UserIds: []int32{bob}}), alice).Entry, "a nil entry is skipped") } func TestCheckMemoCreate(t *testing.T) { @@ -87,13 +84,10 @@ func TestCheckMemoCreate(t *testing.T) { require.NoError(t, CheckMemoCreate(ctx, unlimited, 1)) require.Zero(t, unlimited.countCalls, "no count query runs without a memo limit") - limited := &fakeStore{memos: 2, setting: &storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{ - limit("default-memos", MetricMemoCount, 0, 3), - }}} + limited := &fakeStore{memos: 2, setting: setting(&entry{AllUsers: true, MemoCount: new(int64(3))})} require.NoError(t, CheckMemoCreate(ctx, limited, 1)) limited.memos = 3 - refusal := AsRefusal(CheckMemoCreate(ctx, limited, 1)) - require.Equal(t, &Refusal{Metric: MetricMemoCount, Limit: 3, Usage: 3, Source: "default-memos"}, refusal) + require.Equal(t, &Refusal{Metric: MetricMemoCount, Limit: 3, Usage: 3, Index: 0}, AsRefusal(CheckMemoCreate(ctx, limited, 1))) limited.countingErr = errors.New("database down") err := CheckMemoCreate(ctx, limited, 1) @@ -108,47 +102,57 @@ func TestCheckUpload(t *testing.T) { require.NoError(t, CheckUpload(ctx, unlimited, 1, 1<<40), "no limits means any file size") require.Zero(t, unlimited.sumCalls, "no sum query runs without a storage limit") - s := &fakeStore{storage: 900, setting: &storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{ - limit("default-storage", MetricStorageBytes, 0, 1000), - limit("default-upload", MetricUploadBytes, 0, 200), - limit("alice-upload", MetricUploadBytes, 2, 500), - }}} + s := &fakeStore{storage: 900, setting: setting( + &entry{UserIds: []int32{2}, UploadBytes: new(int64(500))}, + &entry{AllUsers: true, StorageBytes: new(int64(1000)), UploadBytes: new(int64(200))}, + )} require.NoError(t, CheckUpload(ctx, s, 1, 100), "exactly at the storage limit is allowed") - require.Equal(t, &Refusal{Metric: MetricStorageBytes, Limit: 1000, Usage: 900, Source: "default-storage"}, AsRefusal(CheckUpload(ctx, s, 1, 101))) - require.Equal(t, &Refusal{Metric: MetricUploadBytes, Limit: 200, Usage: 201, Source: "default-upload"}, AsRefusal(CheckUpload(ctx, s, 1, 201))) + require.Equal(t, &Refusal{Metric: MetricStorageBytes, Limit: 1000, Usage: 900, Index: 1}, AsRefusal(CheckUpload(ctx, s, 1, 101))) + require.Equal(t, &Refusal{Metric: MetricUploadBytes, Limit: 200, Usage: 201, Index: 1}, AsRefusal(CheckUpload(ctx, s, 1, 201))) - // A user's own per-file limit replaces the default, even when it is higher. - s.storage = 0 + // The user's own entry is the whole limit set: a higher per-file limit and + // no storage limit at all. require.NoError(t, CheckUpload(ctx, s, 2, 500)) - require.Equal(t, &Refusal{Metric: MetricUploadBytes, Limit: 500, Usage: 501, Source: "alice-upload"}, AsRefusal(CheckUpload(ctx, s, 2, 501))) + require.Equal(t, &Refusal{Metric: MetricUploadBytes, Limit: 500, Usage: 501, Index: 0}, AsRefusal(CheckUpload(ctx, s, 2, 501))) + s.sumCalls = 0 + require.NoError(t, CheckUpload(ctx, s, 2, 400)) + require.Zero(t, s.sumCalls, "no storage limit in the user's entry, so no sum query") } func TestCheckFileSize(t *testing.T) { ctx := context.Background() - s := &fakeStore{storage: 1 << 40, setting: &storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{ - limit("default-storage", MetricStorageBytes, 0, 1), - limit("default-upload", MetricUploadBytes, 0, 200), - }}} + s := &fakeStore{storage: 1 << 40, setting: setting(&entry{AllUsers: true, StorageBytes: new(int64(1)), UploadBytes: new(int64(200))})} require.NoError(t, CheckFileSize(ctx, s, 1, 200), "the storage limit is not checked") - require.Equal(t, &Refusal{Metric: MetricUploadBytes, Limit: 200, Usage: 201, Source: "default-upload"}, AsRefusal(CheckFileSize(ctx, s, 1, 201))) + require.Equal(t, &Refusal{Metric: MetricUploadBytes, Limit: 200, Usage: 201, Index: 0}, AsRefusal(CheckFileSize(ctx, s, 1, 201))) require.Zero(t, s.sumCalls, "no sum query runs for a per-file check") } func TestCheckMemoContentSize(t *testing.T) { ctx := context.Background() - s := &fakeStore{setting: &storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{ - limit("default-content", MetricMemoContentBytes, 0, 8192), - limit("alice-content", MetricMemoContentBytes, 2, 6), - limit("bob-content", MetricMemoContentBytes, 3, 0), - }}} + s := &fakeStore{setting: setting( + &entry{UserIds: []int32{2}, MemoContentBytes: new(int64(6))}, + &entry{UserIds: []int32{3}, MemoContentBytes: new(int64(0))}, + &entry{AllUsers: true, MemoContentBytes: new(int64(8192))}, + )} require.NoError(t, CheckMemoContentSize(ctx, s, 1, 8192)) - require.Equal(t, &Refusal{Metric: MetricMemoContentBytes, Limit: 8192, Usage: 8193, Source: "default-content"}, AsRefusal(CheckMemoContentSize(ctx, s, 1, 8193))) + require.Equal(t, &Refusal{Metric: MetricMemoContentBytes, Limit: 8192, Usage: 8193, Index: 2}, AsRefusal(CheckMemoContentSize(ctx, s, 1, 8193))) require.NoError(t, CheckMemoContentSize(ctx, s, 2, int64(len("中文")))) - require.Equal(t, &Refusal{Metric: MetricMemoContentBytes, Limit: 6, Usage: 9, Source: "alice-content"}, AsRefusal(CheckMemoContentSize(ctx, s, 2, int64(len("中文字"))))) - require.NoError(t, CheckMemoContentSize(ctx, s, 3, 1<<20), "a user's explicit zero overrides the default") - require.NoError(t, CheckMemoContentSize(ctx, &fakeStore{}, 1, 1<<20), "no content limit means unlimited") + require.Equal(t, &Refusal{Metric: MetricMemoContentBytes, Limit: 6, Usage: 9, Index: 0}, AsRefusal(CheckMemoContentSize(ctx, s, 2, int64(len("中文字"))))) + require.NoError(t, CheckMemoContentSize(ctx, s, 3, 1<<20), "an explicit zero is unlimited") + require.NoError(t, CheckMemoContentSize(ctx, &fakeStore{}, 1, 1<<20), "no entries means unlimited") require.Zero(t, s.countCalls) require.Zero(t, s.sumCalls) s.settingErr = errors.New("database down") require.Error(t, CheckMemoContentSize(ctx, s, 1, 1), "setting read failures do not bypass limits") } + +func TestUsageFor(t *testing.T) { + ctx := context.Background() + s := &fakeStore{memos: 4, storage: 99, setting: setting(&entry{AllUsers: true, MemoCount: new(int64(10))})} + usage, err := UsageFor(ctx, s, 1) + require.NoError(t, err) + require.Equal(t, int64(4), usage.MemoCount) + require.Equal(t, int64(99), usage.StorageBytes) + require.Equal(t, 0, usage.Limits.Index) + require.Equal(t, int64(10), usage.Limits.Entry.GetMemoCount()) +} diff --git a/docs/design/README.md b/docs/design/README.md index 3d2bb20142a18..963ce57eee89a 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -1,16 +1,27 @@ # Design Documents -Design documents may evolve while a feature is being designed and implemented. Once implementation is complete, set `Status: Implemented` and freeze the document. +A design document records a decision and the reasoning behind it. Source code, tests, and package `README.md` or `doc.go` files describe current +behavior; a design document does not have to. -Do not update implemented designs to track later code changes. They preserve the original design and rationale; source code and tests describe current -behavior. New and unimplemented designs may still be added or revised. +## Lifecycle -The following designs have been implemented and are frozen: +- **Draft.** A design that is not yet shipped in a tagged release may change freely, even after its code is merged. Keep it in step with the + implementation until the release, because the design itself often changes while a feature settles. +- **Implemented.** Once the feature ships in a release, set `Status: Implemented` and freeze the body. Do not edit it to track later code changes. +- **Amendments.** When a later release changes a frozen design, add or extend a dated `## Amendments` section at the end that states what changed and why. + Leave the original body intact, so the original rationale stays readable. A change large enough to need its own rationale gets a new design document + that links back to the one it replaces. + +## Index + +Drafts, merged but not yet released: + +- [User usage limits](user-usage-limits.md) + +Implemented and frozen: - [API abuse controls](api-abuse-controls.md) -- [Configuration provisioning](configuration-provisioning.md) +- [Configuration provisioning](configuration-provisioning.md), moved here from `docs/configuration-provisioning.md` after implementation - [Memos Export Format](memos-export-format.md), including the schemas under `memo-export/1.0/` - [Multi-Spaces](multi-spaces.md) - [Unique email](unique-email.md) - -The configuration provisioning design was moved here from `docs/configuration-provisioning.md` and frozen after implementation. diff --git a/docs/design/configuration-provisioning.md b/docs/design/configuration-provisioning.md index 2d606010e9e64..d0bc432c309c9 100644 --- a/docs/design/configuration-provisioning.md +++ b/docs/design/configuration-provisioning.md @@ -544,3 +544,13 @@ The implementation requires tests for: - [GitLab OpenID Connect configuration](https://docs.gitlab.com/administration/auth/oidc/) - [Keycloak startup import](https://www.keycloak.org/server/importExport) - [Grafana provisioning](https://grafana.com/docs/grafana/latest/administration/provisioning/) + +## Amendments + +### 2026-10-14: usage limits + +- `USAGE_LIMITS` is a deployment-configurable key, provided as `memos-instance-setting-usage-limits.json`. The file is the ordered entry list: every + entry lists positive `userIds` except the last, which sets `allUsers: true`. While the file is mounted, `UpdateInstanceSetting` for the key returns + `FAILED_PRECONDITION`. +- `STORAGE` no longer carries an upload size limit. The per-file cap is the `UPLOAD_BYTES` usage limit, and an absent `USAGE_LIMITS` setting means + the built-in defaults of 8 KiB per memo and 32 MiB per file. See [User usage limits](user-usage-limits.md). diff --git a/docs/design/user-usage-limits.md b/docs/design/user-usage-limits.md index 55f9a30a1bfa3..6834635329054 100644 --- a/docs/design/user-usage-limits.md +++ b/docs/design/user-usage-limits.md @@ -1,8 +1,8 @@ # User Usage Limits Design -Status: Draft +Status: Draft (merged, not yet released; revised 2026-10-14) -Date: 2026-10-11 +Date: 2026-10-11, revised 2026-10-14 Existing domain language: [Memos context](../../CONTEXT.md) @@ -10,24 +10,29 @@ Related: [API Abuse Controls Design](api-abuse-controls.md), [Configuration Prov ## Summary -Every system needs limits to stay fast and reliable. Without per-user limits, growth, mistakes, or abuse in one account can slow down or fill up a shared instance for everyone. Memos has only instance-wide limits: upload size and memo length. Users cannot see how close they are to any limit. +Every system needs limits to stay fast and reliable. Without per-user limits, growth, mistakes, or abuse in one account can slow down or fill up a shared instance for everyone. Before this design, Memos had only two instance-wide caps, the upload size in the storage setting and the memo content length in the memo setting, and users could not see how close they were to either. -Usage limits keep an instance fast and reliable for everyone. This design adds them for three metrics: memo count, total storage, and the size of one file. A limit applies to every user (a default) or to one user, and a user's own limit replaces the default. With no limits, Memos behaves as before. When a limit is reached, creating a memo or starting an upload is refused with one error contract. Users see their usage in Settings. Nothing is ever hidden or deleted. +Usage limits replace both caps with four per-user metrics: memo count, the content size of one memo, total storage, and the size of one file. The limits are one ordered list of entries. Each entry names the users it applies to, or applies to every user, and carries a value for each metric. The first entry that names a user is that user's limit set. The last entry applies to every user and is the default. A fresh instance starts with one entry, the default, that allows 8 KiB per memo and 32 MiB per file. When a limit is reached, the create, edit, or upload is refused with one error contract. Users see their usage in Settings. Nothing is ever hidden or deleted. + +This revision replaces the first merged shape, which stored one resource per metric and per user with its own create, update, and delete calls. That shape could give one user a bigger limit, but it could not say which users shared a limit set, and every per-user value was a separate resource. The feature is unreleased, so the shape changes without compatibility concerns. ## Goals -- An admin can set default limits and per-user limits from the API and the web app. -- Memo creation and uploads are refused once a limit is reached, on every transport. +- An admin can set the default limits and bigger limits for chosen users from the API and the web app, in one place. +- Memo creation, memo edits, and uploads are refused once a limit is reached, on every transport. - A refused memo save keeps the draft, and its uploaded files, in the editor. - A user can see their usage and the limits that apply to them. -- An instance with no limits runs no extra count or sum queries. +- An instance with no count or storage limit runs no extra count or sum queries. +- A deployment can provide the whole setting as a file. ## Non-goals - Plans, billing, or anything specific to a hosted service. -- Limits per role or group, for example all admins. -- Checking imports against limits. Import is not checked yet. +- Limits per role. Admins are limited like everyone else. +- Combining several entries for one user. The first match is the whole answer. +- Checking memo count or total storage on import. Import checks only the per-memo content size and the per-file size. - Emails when a user nears a limit. +- Labels. A deployment with thousands of users sharing one bigger limit needs a way to say "every user with attribute X" without listing ids. That is a planned, additive extension: a `labels` field on the entry, reserved as field 2, and an admin-only label list on the user. It is not part of this revision. ## Behavior @@ -35,67 +40,91 @@ Usage limits keep an instance fast and reliable for everyone. This design adds t | Metric | Counts | Checked on | | --- | --- | --- | -| `MEMOS` | Every memo row the user created: memos, comments, archived memos, and memos in Spaces | `CreateMemo` and `CreateMemoComment` | -| `STORAGE_BYTES` | The size of the user's uploads, including uploads not attached to a memo. External links do not count | `CreateAttachment`, and the start and finish of `UploadAttachment` | -| `UPLOAD_BYTES` | The size of one file. It applies only when it is lower than the instance upload limit | The same calls as storage | +| `memo_count` | Every memo row the user created: memos, comments, archived memos, and memos in Spaces | `CreateMemo` and `CreateMemoComment` | +| `memo_content_bytes` | The UTF-8 size of one memo's content | `CreateMemo`, `CreateMemoComment`, `UpdateMemo` when `content` changes, and each memo in `ImportMemos` | +| `storage_bytes` | The size of the user's uploads, including uploads not attached to a memo. External links do not count | `CreateAttachment`, and the start and finish of `UploadAttachment` | +| `upload_bytes` | The size of one file | The same calls as storage, and each attachment in `ImportMemos` | -**Resolution.** For each metric, the user's own limit applies if there is one, otherwise the default, otherwise the metric is unlimited. A value of 0 is unlimited, so an admin can exempt one user from a default. Admins are limited like everyone else. +**The list.** The setting is an ordered list of entries. An entry has `user_ids`, or `all_users`, and one optional value per metric. The list is scanned top to bottom. The first entry that lists the user, or that has `all_users` set, is the user's limit set, whole. Nothing from later entries applies. Exactly one entry has `all_users` set, and it is the last entry; it is the default. -**Counting.** Usage is counted live on every check. There are no stored counters that can drift. Parallel requests at a limit can go over it by the number of requests in flight. A failed count fails the request with `INTERNAL`, so a database problem cannot switch limits off. +**Values.** A set value is the limit. 0 is unlimited. An unset value is also unlimited, because an entry is a complete limit set and there is nothing to inherit from. The admin form pre-fills a new entry from the default so that leaving a field alone keeps the default's value rather than removing the limit. -**Lowering a limit** below a user's usage deletes nothing. New memos or uploads are refused until the user is under the limit. +**Built-in default.** When the setting does not exist, the instance behaves as if it held one entry: `all_users` with `memo_content_bytes` 8 KiB and `upload_bytes` 32 MiB, and no memo count or storage limit. The first write persists the setting. After that, the stored list is the whole truth. -## API +**Counting.** Usage is counted live on every check. There are no stored counters that can drift. Parallel requests at a limit can go over it by the number of requests in flight. A failed count fails the request with `INTERNAL`, so a database problem cannot switch limits off. Byte-size checks on one memo or one file compare a length and run no query. -Five admin-only calls on `InstanceService`, on the resource `usageLimits/{usage_limit}`: +**Lowering a limit** below a user's usage deletes nothing. New memos or uploads are refused until the user is under the limit. Editing an existing memo whose content is already over the content limit is refused until the content is shortened. -| Call | Route | -| --- | --- | -| `ListUsageLimits` | `GET /api/usageLimits`, with `filter` on `user` and `metric` | -| `GetUsageLimit` | `GET /api/usageLimits/{id}` | -| `CreateUsageLimit` | `POST /api/usageLimits` | -| `UpdateUsageLimit` | `PATCH /api/usageLimits/{id}`; only `value` can change | -| `DeleteUsageLimit` | `DELETE /api/usageLimits/{id}` | +## Storage -`metric` and `user` are immutable. A second limit for the same metric and user returns `ALREADY_EXISTS`. An instance holds at most 1,000 limits. +The list is stored in one `system_setting` row, key `USAGE_LIMITS`, as the store message `InstanceUsageLimitsSetting`. Entries store user IDs, so renaming a user keeps their entry. There is no new table. -`UserService.GetUserUsage` (`GET /api/users/{user}/usage`) returns the memo count, the stored bytes, and for each metric the limit in effect and the `usageLimits/…` name it comes from. The user and admins can call it. +```proto +message InstanceUsageLimitsSetting { + repeated UsageLimit limits = 1; -**Refusal contract.** Code `RESOURCE_EXHAUSTED` (HTTP 429), message `quota exceeded`, with: + message UsageLimit { + repeated int32 user_ids = 1; + // Field 2 is reserved for labels. + bool all_users = 3; + optional int64 memo_count = 4; + optional int64 memo_content_bytes = 5; + optional int64 storage_bytes = 6; + optional int64 upload_bytes = 7; + } +} +``` -- `google.rpc.ErrorInfo`: reason `QUOTA_EXCEEDED`, domain `memos.api`, and metadata `metric`, `limit`, `usage`, and `usage_limit`. For `UPLOAD_BYTES`, `usage` is the size of the refused file. -- `google.rpc.QuotaFailure`, naming the user as the subject. +Validation, applied to every stored list whether it comes from the API or a file: -There is no `RetryInfo` and no `Retry-After` header, because waiting does not lift a usage limit. +- At most 100 entries, and at most 1,000 user IDs across all entries. +- Every entry has at least one user ID or `all_users`, never both. +- Exactly one entry has `all_users`, and it is the last entry. +- User IDs are positive and not repeated within an entry. An ID that appears in two entries is allowed; the first wins. +- Values are not negative. -## Storage +Reads for checks skip the settings cache, as `ACCESS` does, so a change applies on every replica at the next check. Writes replace the whole row; there is no per-entry write. Entries naming a user that no longer exists are ignored on read and dropped on the next write. User deletion drops the user's ID from every entry; an entry left with no users and no `all_users` is dropped with it. -All limits are stored in one `system_setting` row, key `USAGE_LIMITS`, as the store message `InstanceUsageLimitsSetting`. Limits store the user ID, so renaming a user keeps their limits. There is no new table. +**Settings file.** A deployment can provide `memos-instance-setting-usage-limits.json` in the store format. The setting is then read-only through the API. + +## API -- **Writes** read the row, change it, and write it back only if it is unchanged since the read. A conflicting write retries up to 5 times, then returns `ABORTED`. Uniqueness and the cap are checked inside this step, so they hold across replicas. -- **Reads for checks** skip the settings cache, as `ACCESS` does, so a change applies on every replica at the next check. -- **User deletion** removes that user's limits. Writes also drop limits of users that no longer exist. -- **Settings file.** A deployment can provide `memos-instance-setting-usage-limits.json` in the store format. The write calls then return `FAILED_PRECONDITION`. +The setting is an ordinary instance setting. `GetInstanceSetting` and `UpdateInstanceSetting` on `instance/settings/USAGE_LIMITS` read and replace it. There are no per-entry calls; the API shape mirrors the store shape, with `users` as `users/{user}` names in place of IDs. The admin-only `usageLimits/{usage_limit}` resource and its five calls from the first merged shape are removed. -The general `UpdateInstanceSetting` call rejects the `USAGE_LIMITS` key, so the usage-limit calls are the only write path. +`UserService.GetUserUsage` (`GET /api/users/{user}/usage`) returns the memo count, the stored bytes, the resolved limit set, and the entry that produced it. The user and admins can call it. + +**Refusal contract.** Code `RESOURCE_EXHAUSTED` (HTTP 429), message `quota exceeded`, with: + +- `google.rpc.ErrorInfo`: reason `QUOTA_EXCEEDED`, domain `memos.api`, and metadata `metric`, `limit`, and `usage`. `metric` is one of `MEMO_COUNT`, `MEMO_CONTENT_BYTES`, `STORAGE_BYTES`, `UPLOAD_BYTES`. For `MEMO_COUNT` and `STORAGE_BYTES`, `usage` is the current count or stored bytes. For `UPLOAD_BYTES`, `usage` is the size of the refused file. For `MEMO_CONTENT_BYTES`, `usage` is the size of the refused content. +- `google.rpc.QuotaFailure`, naming the user as the subject. + +There is no `RetryInfo` and no `Retry-After` header, because waiting does not lift a usage limit. ## Schema change -Migration `26.10/00` adds indexes on `memo.creator_id` and `attachment.creator_id`, so the count and sum queries stay fast. The schema version moves to `26.10.1`. +Migration `26.10/00` adds an index on `attachment.creator_id` on every driver, and on `memo.creator_id` where it was missing, so the count and sum queries stay fast. ## Other changes +- **The instance-wide caps are removed.** `InstanceStorageSetting.upload_size_limit_mb` and `InstanceMemoRelatedSetting.content_length_limit` no longer exist in the API or the store. Old values are not migrated; the built-in default takes their place. - **Storage figures exclude external links.** `GetUserStats` and the storage check no longer count `EXTERNAL` attachments, which store no bytes. - **The web editor keeps uploaded files after a failed save.** Before, a refused create left the files as local files, so each retry uploaded them again. ## Web app - A refused memo save keeps the draft and shows a message that names the limit. A refused upload shows a storage or file-size message. -- Settings, My Account shows usage against the limits. It is hidden when no limit applies. -- Settings, Usage limits (admin) is one table. The first row, All users, holds the defaults. Each further row is a user with their own limits, which an admin adds, edits, or removes. A user's cell shows the all-users value in gray when the user has no limit of their own. In a user's form, an empty field uses the all-users limit and 0 means no limit. -- The form warns when a file-size limit is not below the instance upload limit. +- Settings, My Account shows usage against the user's limit set. It is hidden when no limit applies. +- Settings, Usage limits (admin) is the list, shown in its stored order with a position number on every row and a note that the first entry naming a user is the one that applies. Entries can be added, edited, moved up and down, and removed. The default entry is pinned last and cannot be removed. An entry's form has a user picker and four fields, pre-filled from the default. An entry that can never match, because every user it names is caught by an earlier entry, is flagged. Values are entered in KiB for content and MiB for storage and file size. ## Upgrade and rollback -- **Upgrade.** Existing instances have no limits, so behavior is unchanged apart from the external-link storage fix and the new indexes. +- **Upgrade.** Existing instances have no `USAGE_LIMITS` row, so the built-in default applies. An instance that had raised or lowered the old instance-wide caps must set the default entry again. Apart from that, the external-link storage fix, and the new indexes, behavior is unchanged. - **Rollback.** Restore the database from the backup taken before the upgrade, because older binaries refuse a newer schema. Remove a `memos-instance-setting-usage-limits.json` file first, because older binaries refuse to start with a settings file for a key they do not know. + +## Alternatives considered + +- **One resource per metric and user**, the first merged shape. Fine for a few exceptions; no way to express a shared limit set, and every value is its own resource with its own write path. Replaced by the list. +- **Named limit sets with a pointer on the user** (Joplin, Zulip, Forgejo). One extra concept for the same result; the list expresses the same thing with entry order and, later, labels. +- **A copied limit set per user** (Harbor, oCIS, Documenso). Every product that did this added a backfill job because editing the shared value did not propagate. +- **A resolver supplied by the deployment** (Nextcloud quota event, Synapse module). Hides the record outside the product, so the admin UI and the usage call cannot explain a number. Rejected before this design as well. +- **Merging several matching entries per field**, most permissive wins (Nextcloud group quota, Forgejo). Order-independent, but a rule to explain; first match needs none. An admin who wants two entries combined writes a third. diff --git a/proto/README.md b/proto/README.md index 8e7b047567c0d..cf782d0416836 100644 --- a/proto/README.md +++ b/proto/README.md @@ -18,6 +18,6 @@ buf format -w ## Deployment settings -Instance setting JSON files use the same `store.InstanceSetting` schema as storage. Usage limits explicitly set -`allUsers: {}` for an all-users default or `userId: ` for a user. A reload with invalid configuration -leaves the active configuration intact. +Instance setting JSON files use the same `store.InstanceSetting` schema as storage. A usage limits file is the ordered +entry list; the last entry sets `allUsers: true`, every other entry lists positive `userIds`. A reload with invalid +configuration leaves the active configuration intact. diff --git a/proto/api/CHANGELOG.md b/proto/api/CHANGELOG.md index bcb579c7a5045..b8c4863824a27 100644 --- a/proto/api/CHANGELOG.md +++ b/proto/api/CHANGELOG.md @@ -25,13 +25,14 @@ Add entries under a `## Unreleased` heading. After the release ships, rename it - Access tokens are now bound to the user's password hash and become invalid immediately after a password change or an administrator password reset. Pre-upgrade access tokens are also rejected; clients must refresh them or sign in again. The browser session that changes its own password can still refresh; other refresh sessions are revoked. -- Memo content size now uses the `MEMO_CONTENT_BYTES` usage limit, including creates, edits, comments, and imports. - The default is 8 KiB when the usage limits setting is absent. `MemoRelatedSetting.content_length_limit` is removed; - old values are not migrated. Exceeding the content limit returns `RESOURCE_EXHAUSTED` - with quota details. - `MemoRelatedSetting` fields are now `enable_double_click_edit = 1` and `reactions = 2`, matching the storage proto. -- The memo-count usage metric is now `MEMO_COUNT`, and `UserUsage.memos_limit` is now `memo_count_limit`. - Metric values are `MEMO_COUNT = 1`, `MEMO_CONTENT_BYTES = 2`, `STORAGE_BYTES = 3`, and `UPLOAD_BYTES = 4`. +- Usage limits replace the instance-wide upload size and memo content caps. `StorageSetting.upload_size_limit_mb` + and `MemoRelatedSetting.content_length_limit` are removed and old values are not migrated; `MemoRelatedSetting` + fields are now `enable_double_click_edit = 1` and `reactions = 2`. The limits are the `USAGE_LIMITS` instance + setting, `instance/settings/USAGE_LIMITS`, an ordered list of entries read and replaced whole through + `GetInstanceSetting` and `UpdateInstanceSetting`; the first entry naming a user applies, the last entry applies to + all users. The `usageLimits/{usage_limit}` resource and its five RPCs do not exist. `UserUsage` returns the limit + set in effect as `limits` plus `limit_index`. Exceeding a limit returns `RESOURCE_EXHAUSTED` with reason + `QUOTA_EXCEEDED` and metadata `metric`, `limit`, and `usage`. - The API no longer carries a `v1` version marker. Every client must update its paths: - REST routes move from `/api/v1/...` to `/api/...` (for example `GET /api/memos`). Server-sent events move from diff --git a/proto/api/instance_service.proto b/proto/api/instance_service.proto index a4016659a2a38..2b9268779cfde 100644 --- a/proto/api/instance_service.proto +++ b/proto/api/instance_service.proto @@ -59,47 +59,6 @@ service InstanceService { rpc GetInstanceStats(GetInstanceStatsRequest) returns (InstanceStats) { option (google.api.http) = {get: "/api/instance/stats"}; } - - // ListUsageLimits lists the usage limits: the defaults for every user and the - // limits for single users. Admin only. - rpc ListUsageLimits(ListUsageLimitsRequest) returns (ListUsageLimitsResponse) { - option (google.api.http) = {get: "/api/usageLimits"}; - } - - // GetUsageLimit gets a usage limit. Admin only. - rpc GetUsageLimit(GetUsageLimitRequest) returns (UsageLimit) { - option (google.api.http) = {get: "/api/{name=usageLimits/*}"}; - option (google.api.method_signature) = "name"; - } - - // CreateUsageLimit creates a usage limit. Admin only. There is at most one - // limit per metric and user, and one default per metric. Fails with - // FAILED_PRECONDITION when the usage limits are configured by the deployment. - rpc CreateUsageLimit(CreateUsageLimitRequest) returns (UsageLimit) { - option (google.api.http) = { - post: "/api/usageLimits" - body: "usage_limit" - }; - option (google.api.method_signature) = "usage_limit,usage_limit_id"; - } - - // UpdateUsageLimit updates the value of a usage limit. Admin only. The metric - // and user cannot change. - rpc UpdateUsageLimit(UpdateUsageLimitRequest) returns (UsageLimit) { - option (google.api.http) = { - patch: "/api/{usage_limit.name=usageLimits/*}" - body: "usage_limit" - }; - option (google.api.method_signature) = "usage_limit,update_mask"; - } - - // DeleteUsageLimit deletes a usage limit. Admin only. Deleting a user's limit - // returns the user to the default; deleting a default makes the metric - // unlimited for users without their own limit. - rpc DeleteUsageLimit(DeleteUsageLimitRequest) returns (google.protobuf.Empty) { - option (google.api.http) = {delete: "/api/{name=usageLimits/*}"}; - option (google.api.method_signature) = "name"; - } } // InstanceAccessMode controls whether unauthenticated users may access instance content. @@ -185,6 +144,8 @@ message InstanceSetting { AISetting ai_setting = 7; // The value of the ACCESS setting. AccessSetting access_setting = 8; + // The value of the USAGE_LIMITS setting. + UsageLimitsSetting usage_limits_setting = 9; } // Key identifies an instance setting. @@ -204,6 +165,8 @@ message InstanceSetting { AI = 6; // Instance access policy settings. ACCESS = 7; + // Usage limits. + USAGE_LIMITS = 8; } // StorageType is the backend of a configured storage. @@ -495,6 +458,42 @@ message InstanceSetting { // Whether unauthenticated users may access instance content. Must be PRIVATE or PUBLIC on update. InstanceAccessMode access_mode = 1; } + + // UsageLimitsSetting is an ordered list of limit sets. The first entry that + // names a user is that user's limit set, whole. The last entry has all_users + // set and is the default. The setting is read and replaced whole. Admins + // are limited like everyone else. + message UsageLimitsSetting { + // The entries, in the order they are checked. At most 100, naming at most + // 1,000 users in all. + repeated UsageLimit limits = 1; + + // UsageLimit is one entry: who it applies to and the limit for each + // metric. An unset limit is unlimited, as is 0. + message UsageLimit { + // The users the entry applies to. Not repeated within an entry. + // Format: users/{user} + repeated string users = 1 [(google.api.resource_reference) = {type: "memos.api/User"}]; + + // Whether the entry applies to every user. Exactly one entry has it, and + // it is the last entry. An entry has users or all_users, never both. + bool all_users = 3; + + // The number of memos the user may have created, comments and archived + // memos included. + optional int64 memo_count = 4; + + // The UTF-8 size in bytes of one memo's content. + optional int64 memo_content_bytes = 5; + + // The total size in bytes of the user's uploads. External links do not + // count. + optional int64 storage_bytes = 6; + + // The size in bytes of one uploaded file. + optional int64 upload_bytes = 7; + } + } } // Request message for InstanceService.GetInstanceSetting. @@ -565,123 +564,3 @@ message InstanceStats { // Request message for InstanceService.GetInstanceStats. message GetInstanceStatsRequest {} - -// UsageLimit sets one limit for every user, or for one user. When both exist -// for the same metric, the limit for the user applies to that user. -message UsageLimit { - option (google.api.resource) = { - type: "memos.api/UsageLimit" - pattern: "usageLimits/{usage_limit}" - singular: "usageLimit" - plural: "usageLimits" - }; - - // Identifier. The resource name of the usage limit. - // Format: usageLimits/{usage_limit} - string name = 1 [(google.api.field_behavior) = IDENTIFIER]; - - // Required. Immutable. What is limited. - Metric metric = 2 [ - (google.api.field_behavior) = REQUIRED, - (google.api.field_behavior) = IMMUTABLE - ]; - - // Optional. Immutable. The user the limit applies to. Empty means every user. - // Format: users/{user} - string user = 3 [ - (google.api.field_behavior) = OPTIONAL, - (google.api.field_behavior) = IMMUTABLE, - (google.api.resource_reference) = {type: "memos.api/User"} - ]; - - // Required. The limit. Must not be negative. 0 means unlimited. - int64 value = 4 [(google.api.field_behavior) = REQUIRED]; - - // Output only. The creation time. - google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; - - // Output only. The last update time. - google.protobuf.Timestamp update_time = 6 [(google.api.field_behavior) = OUTPUT_ONLY]; - - // Metric is what a usage limit counts. - enum Metric { - METRIC_UNSPECIFIED = 0; - // The number of memos the user created, including comments, archived - // memos, and memos in Spaces. - MEMO_COUNT = 1; - // The UTF-8 size in bytes of one memo's content. - MEMO_CONTENT_BYTES = 2; - // The total size in bytes of the user's uploads, including uploads not - // attached to a memo. External links do not count. - STORAGE_BYTES = 3; - // The size in bytes of one uploaded file. - UPLOAD_BYTES = 4; - } -} - -// Request message for InstanceService.ListUsageLimits. -message ListUsageLimitsRequest { - // Optional. The maximum number of usage limits to return. If unspecified, at - // most 50 are returned. The maximum value is 1000; values above 1000 are - // coerced to 1000. - int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL]; - - // Optional. A page token from a previous call. Provide it to retrieve the - // next page. - string page_token = 2 [(google.api.field_behavior) = OPTIONAL]; - - // Optional. A CEL filter on `user` and `metric`, combined with `&&`. - // `user == ""` selects the defaults for every user. - // Example: "user == 'users/steven' && metric == 'STORAGE_BYTES'" - string filter = 3 [(google.api.field_behavior) = OPTIONAL]; -} - -// Response message for InstanceService.ListUsageLimits. -message ListUsageLimitsResponse { - // The usage limits, defaults first, then by user and metric. - repeated UsageLimit usage_limits = 1; - - // A token to retrieve the next page. Empty when there are no more pages. - string next_page_token = 2; -} - -// Request message for InstanceService.GetUsageLimit. -message GetUsageLimitRequest { - // Required. The resource name of the usage limit. - // Format: usageLimits/{usage_limit} - string name = 1 [ - (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = {type: "memos.api/UsageLimit"} - ]; -} - -// Request message for InstanceService.CreateUsageLimit. -message CreateUsageLimitRequest { - // Required. The usage limit to create. - UsageLimit usage_limit = 1 [(google.api.field_behavior) = REQUIRED]; - - // Optional. The ID to use for the usage limit, which becomes the final - // segment of its name. Lowercase letters, digits and hyphens, at most 32 - // characters, starting with a letter. Generated when empty. - string usage_limit_id = 2 [(google.api.field_behavior) = OPTIONAL]; -} - -// Request message for InstanceService.UpdateUsageLimit. -message UpdateUsageLimitRequest { - // Required. The usage limit with its new value. - UsageLimit usage_limit = 1 [(google.api.field_behavior) = REQUIRED]; - - // Optional. The fields to update. Only `value` is supported, which is also - // the default. - google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = OPTIONAL]; -} - -// Request message for InstanceService.DeleteUsageLimit. -message DeleteUsageLimitRequest { - // Required. The resource name of the usage limit. - // Format: usageLimits/{usage_limit} - string name = 1 [ - (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = {type: "memos.api/UsageLimit"} - ]; -} diff --git a/proto/api/user_service.proto b/proto/api/user_service.proto index 53b102b0d7b99..bd9d1ae042034 100644 --- a/proto/api/user_service.proto +++ b/proto/api/user_service.proto @@ -684,7 +684,7 @@ message GetUserStatsRequest { string filter = 2 [(google.api.field_behavior) = OPTIONAL]; } -// UserUsage is a user's usage and the usage limits in effect for the user. +// UserUsage is a user's usage and the limit set in effect for the user. message UserUsage { option (google.api.resource) = { type: "memos.api/UserUsage" @@ -705,28 +705,27 @@ message UserUsage { // attached to a memo. External links do not count. int64 storage_bytes = 3; - // The limit on memo_count. - EffectiveLimit memo_count_limit = 4; + // The limit set in effect: the first USAGE_LIMITS entry that names the + // user, or the default. An unset limit is unlimited. + Limits limits = 4; - // The limit on storage_bytes. - EffectiveLimit storage_bytes_limit = 5; + // The position of the entry in effect in the USAGE_LIMITS setting, starting + // at 0. The default is the last entry. + int32 limit_index = 5; - // The limit on the size of one uploaded file. - EffectiveLimit upload_bytes_limit = 6; + // Limits is the limit for each metric. An unset limit is unlimited, as is 0. + message Limits { + // The number of memos the user may have created. + optional int64 memo_count = 1; - // The limit on the UTF-8 size in bytes of one memo's content. - EffectiveLimit memo_content_bytes_limit = 7; + // The UTF-8 size in bytes of one memo's content. + optional int64 memo_content_bytes = 2; - // EffectiveLimit is the limit in effect for one metric, and where it comes - // from. - message EffectiveLimit { - // The limit. 0 means unlimited. - int64 value = 1; + // The total size in bytes of the user's uploads. + optional int64 storage_bytes = 3; - // The usage limit the value comes from: the user's own limit, or else the - // default. Empty when no usage limit applies. - // Format: usageLimits/{usage_limit} - string usage_limit = 2 [(google.api.resource_reference) = {type: "memos.api/UsageLimit"}]; + // The size in bytes of one uploaded file. + optional int64 upload_bytes = 4; } } diff --git a/proto/gen/api/apiconnect/instance_service.connect.go b/proto/gen/api/apiconnect/instance_service.connect.go index e50711b056811..0bc173a52e7f4 100644 --- a/proto/gen/api/apiconnect/instance_service.connect.go +++ b/proto/gen/api/apiconnect/instance_service.connect.go @@ -52,21 +52,6 @@ const ( // InstanceServiceGetInstanceStatsProcedure is the fully-qualified name of the InstanceService's // GetInstanceStats RPC. InstanceServiceGetInstanceStatsProcedure = "/memos.api.InstanceService/GetInstanceStats" - // InstanceServiceListUsageLimitsProcedure is the fully-qualified name of the InstanceService's - // ListUsageLimits RPC. - InstanceServiceListUsageLimitsProcedure = "/memos.api.InstanceService/ListUsageLimits" - // InstanceServiceGetUsageLimitProcedure is the fully-qualified name of the InstanceService's - // GetUsageLimit RPC. - InstanceServiceGetUsageLimitProcedure = "/memos.api.InstanceService/GetUsageLimit" - // InstanceServiceCreateUsageLimitProcedure is the fully-qualified name of the InstanceService's - // CreateUsageLimit RPC. - InstanceServiceCreateUsageLimitProcedure = "/memos.api.InstanceService/CreateUsageLimit" - // InstanceServiceUpdateUsageLimitProcedure is the fully-qualified name of the InstanceService's - // UpdateUsageLimit RPC. - InstanceServiceUpdateUsageLimitProcedure = "/memos.api.InstanceService/UpdateUsageLimit" - // InstanceServiceDeleteUsageLimitProcedure is the fully-qualified name of the InstanceService's - // DeleteUsageLimit RPC. - InstanceServiceDeleteUsageLimitProcedure = "/memos.api.InstanceService/DeleteUsageLimit" ) // InstanceServiceClient is a client for the memos.api.InstanceService service. @@ -86,22 +71,6 @@ type InstanceServiceClient interface { TestInstanceEmailSetting(context.Context, *connect.Request[api.TestInstanceEmailSettingRequest]) (*connect.Response[emptypb.Empty], error) // GetInstanceStats gets resource usage statistics for the instance. Admin only. Results are cached for 60 seconds. GetInstanceStats(context.Context, *connect.Request[api.GetInstanceStatsRequest]) (*connect.Response[api.InstanceStats], error) - // ListUsageLimits lists the usage limits: the defaults for every user and the - // limits for single users. Admin only. - ListUsageLimits(context.Context, *connect.Request[api.ListUsageLimitsRequest]) (*connect.Response[api.ListUsageLimitsResponse], error) - // GetUsageLimit gets a usage limit. Admin only. - GetUsageLimit(context.Context, *connect.Request[api.GetUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) - // CreateUsageLimit creates a usage limit. Admin only. There is at most one - // limit per metric and user, and one default per metric. Fails with - // FAILED_PRECONDITION when the usage limits are configured by the deployment. - CreateUsageLimit(context.Context, *connect.Request[api.CreateUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) - // UpdateUsageLimit updates the value of a usage limit. Admin only. The metric - // and user cannot change. - UpdateUsageLimit(context.Context, *connect.Request[api.UpdateUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) - // DeleteUsageLimit deletes a usage limit. Admin only. Deleting a user's limit - // returns the user to the default; deleting a default makes the metric - // unlimited for users without their own limit. - DeleteUsageLimit(context.Context, *connect.Request[api.DeleteUsageLimitRequest]) (*connect.Response[emptypb.Empty], error) } // NewInstanceServiceClient constructs a client for the memos.api.InstanceService service. By @@ -151,36 +120,6 @@ func NewInstanceServiceClient(httpClient connect.HTTPClient, baseURL string, opt connect.WithSchema(instanceServiceMethods.ByName("GetInstanceStats")), connect.WithClientOptions(opts...), ), - listUsageLimits: connect.NewClient[api.ListUsageLimitsRequest, api.ListUsageLimitsResponse]( - httpClient, - baseURL+InstanceServiceListUsageLimitsProcedure, - connect.WithSchema(instanceServiceMethods.ByName("ListUsageLimits")), - connect.WithClientOptions(opts...), - ), - getUsageLimit: connect.NewClient[api.GetUsageLimitRequest, api.UsageLimit]( - httpClient, - baseURL+InstanceServiceGetUsageLimitProcedure, - connect.WithSchema(instanceServiceMethods.ByName("GetUsageLimit")), - connect.WithClientOptions(opts...), - ), - createUsageLimit: connect.NewClient[api.CreateUsageLimitRequest, api.UsageLimit]( - httpClient, - baseURL+InstanceServiceCreateUsageLimitProcedure, - connect.WithSchema(instanceServiceMethods.ByName("CreateUsageLimit")), - connect.WithClientOptions(opts...), - ), - updateUsageLimit: connect.NewClient[api.UpdateUsageLimitRequest, api.UsageLimit]( - httpClient, - baseURL+InstanceServiceUpdateUsageLimitProcedure, - connect.WithSchema(instanceServiceMethods.ByName("UpdateUsageLimit")), - connect.WithClientOptions(opts...), - ), - deleteUsageLimit: connect.NewClient[api.DeleteUsageLimitRequest, emptypb.Empty]( - httpClient, - baseURL+InstanceServiceDeleteUsageLimitProcedure, - connect.WithSchema(instanceServiceMethods.ByName("DeleteUsageLimit")), - connect.WithClientOptions(opts...), - ), } } @@ -192,11 +131,6 @@ type instanceServiceClient struct { updateInstanceSetting *connect.Client[api.UpdateInstanceSettingRequest, api.InstanceSetting] testInstanceEmailSetting *connect.Client[api.TestInstanceEmailSettingRequest, emptypb.Empty] getInstanceStats *connect.Client[api.GetInstanceStatsRequest, api.InstanceStats] - listUsageLimits *connect.Client[api.ListUsageLimitsRequest, api.ListUsageLimitsResponse] - getUsageLimit *connect.Client[api.GetUsageLimitRequest, api.UsageLimit] - createUsageLimit *connect.Client[api.CreateUsageLimitRequest, api.UsageLimit] - updateUsageLimit *connect.Client[api.UpdateUsageLimitRequest, api.UsageLimit] - deleteUsageLimit *connect.Client[api.DeleteUsageLimitRequest, emptypb.Empty] } // GetInstanceProfile calls memos.api.InstanceService.GetInstanceProfile. @@ -229,31 +163,6 @@ func (c *instanceServiceClient) GetInstanceStats(ctx context.Context, req *conne return c.getInstanceStats.CallUnary(ctx, req) } -// ListUsageLimits calls memos.api.InstanceService.ListUsageLimits. -func (c *instanceServiceClient) ListUsageLimits(ctx context.Context, req *connect.Request[api.ListUsageLimitsRequest]) (*connect.Response[api.ListUsageLimitsResponse], error) { - return c.listUsageLimits.CallUnary(ctx, req) -} - -// GetUsageLimit calls memos.api.InstanceService.GetUsageLimit. -func (c *instanceServiceClient) GetUsageLimit(ctx context.Context, req *connect.Request[api.GetUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) { - return c.getUsageLimit.CallUnary(ctx, req) -} - -// CreateUsageLimit calls memos.api.InstanceService.CreateUsageLimit. -func (c *instanceServiceClient) CreateUsageLimit(ctx context.Context, req *connect.Request[api.CreateUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) { - return c.createUsageLimit.CallUnary(ctx, req) -} - -// UpdateUsageLimit calls memos.api.InstanceService.UpdateUsageLimit. -func (c *instanceServiceClient) UpdateUsageLimit(ctx context.Context, req *connect.Request[api.UpdateUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) { - return c.updateUsageLimit.CallUnary(ctx, req) -} - -// DeleteUsageLimit calls memos.api.InstanceService.DeleteUsageLimit. -func (c *instanceServiceClient) DeleteUsageLimit(ctx context.Context, req *connect.Request[api.DeleteUsageLimitRequest]) (*connect.Response[emptypb.Empty], error) { - return c.deleteUsageLimit.CallUnary(ctx, req) -} - // InstanceServiceHandler is an implementation of the memos.api.InstanceService service. type InstanceServiceHandler interface { // GetInstanceProfile gets the instance profile. Available without authentication, including on a private instance. @@ -271,22 +180,6 @@ type InstanceServiceHandler interface { TestInstanceEmailSetting(context.Context, *connect.Request[api.TestInstanceEmailSettingRequest]) (*connect.Response[emptypb.Empty], error) // GetInstanceStats gets resource usage statistics for the instance. Admin only. Results are cached for 60 seconds. GetInstanceStats(context.Context, *connect.Request[api.GetInstanceStatsRequest]) (*connect.Response[api.InstanceStats], error) - // ListUsageLimits lists the usage limits: the defaults for every user and the - // limits for single users. Admin only. - ListUsageLimits(context.Context, *connect.Request[api.ListUsageLimitsRequest]) (*connect.Response[api.ListUsageLimitsResponse], error) - // GetUsageLimit gets a usage limit. Admin only. - GetUsageLimit(context.Context, *connect.Request[api.GetUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) - // CreateUsageLimit creates a usage limit. Admin only. There is at most one - // limit per metric and user, and one default per metric. Fails with - // FAILED_PRECONDITION when the usage limits are configured by the deployment. - CreateUsageLimit(context.Context, *connect.Request[api.CreateUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) - // UpdateUsageLimit updates the value of a usage limit. Admin only. The metric - // and user cannot change. - UpdateUsageLimit(context.Context, *connect.Request[api.UpdateUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) - // DeleteUsageLimit deletes a usage limit. Admin only. Deleting a user's limit - // returns the user to the default; deleting a default makes the metric - // unlimited for users without their own limit. - DeleteUsageLimit(context.Context, *connect.Request[api.DeleteUsageLimitRequest]) (*connect.Response[emptypb.Empty], error) } // NewInstanceServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -332,36 +225,6 @@ func NewInstanceServiceHandler(svc InstanceServiceHandler, opts ...connect.Handl connect.WithSchema(instanceServiceMethods.ByName("GetInstanceStats")), connect.WithHandlerOptions(opts...), ) - instanceServiceListUsageLimitsHandler := connect.NewUnaryHandler( - InstanceServiceListUsageLimitsProcedure, - svc.ListUsageLimits, - connect.WithSchema(instanceServiceMethods.ByName("ListUsageLimits")), - connect.WithHandlerOptions(opts...), - ) - instanceServiceGetUsageLimitHandler := connect.NewUnaryHandler( - InstanceServiceGetUsageLimitProcedure, - svc.GetUsageLimit, - connect.WithSchema(instanceServiceMethods.ByName("GetUsageLimit")), - connect.WithHandlerOptions(opts...), - ) - instanceServiceCreateUsageLimitHandler := connect.NewUnaryHandler( - InstanceServiceCreateUsageLimitProcedure, - svc.CreateUsageLimit, - connect.WithSchema(instanceServiceMethods.ByName("CreateUsageLimit")), - connect.WithHandlerOptions(opts...), - ) - instanceServiceUpdateUsageLimitHandler := connect.NewUnaryHandler( - InstanceServiceUpdateUsageLimitProcedure, - svc.UpdateUsageLimit, - connect.WithSchema(instanceServiceMethods.ByName("UpdateUsageLimit")), - connect.WithHandlerOptions(opts...), - ) - instanceServiceDeleteUsageLimitHandler := connect.NewUnaryHandler( - InstanceServiceDeleteUsageLimitProcedure, - svc.DeleteUsageLimit, - connect.WithSchema(instanceServiceMethods.ByName("DeleteUsageLimit")), - connect.WithHandlerOptions(opts...), - ) return "/memos.api.InstanceService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case InstanceServiceGetInstanceProfileProcedure: @@ -376,16 +239,6 @@ func NewInstanceServiceHandler(svc InstanceServiceHandler, opts ...connect.Handl instanceServiceTestInstanceEmailSettingHandler.ServeHTTP(w, r) case InstanceServiceGetInstanceStatsProcedure: instanceServiceGetInstanceStatsHandler.ServeHTTP(w, r) - case InstanceServiceListUsageLimitsProcedure: - instanceServiceListUsageLimitsHandler.ServeHTTP(w, r) - case InstanceServiceGetUsageLimitProcedure: - instanceServiceGetUsageLimitHandler.ServeHTTP(w, r) - case InstanceServiceCreateUsageLimitProcedure: - instanceServiceCreateUsageLimitHandler.ServeHTTP(w, r) - case InstanceServiceUpdateUsageLimitProcedure: - instanceServiceUpdateUsageLimitHandler.ServeHTTP(w, r) - case InstanceServiceDeleteUsageLimitProcedure: - instanceServiceDeleteUsageLimitHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -418,23 +271,3 @@ func (UnimplementedInstanceServiceHandler) TestInstanceEmailSetting(context.Cont func (UnimplementedInstanceServiceHandler) GetInstanceStats(context.Context, *connect.Request[api.GetInstanceStatsRequest]) (*connect.Response[api.InstanceStats], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.InstanceService.GetInstanceStats is not implemented")) } - -func (UnimplementedInstanceServiceHandler) ListUsageLimits(context.Context, *connect.Request[api.ListUsageLimitsRequest]) (*connect.Response[api.ListUsageLimitsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.InstanceService.ListUsageLimits is not implemented")) -} - -func (UnimplementedInstanceServiceHandler) GetUsageLimit(context.Context, *connect.Request[api.GetUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.InstanceService.GetUsageLimit is not implemented")) -} - -func (UnimplementedInstanceServiceHandler) CreateUsageLimit(context.Context, *connect.Request[api.CreateUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.InstanceService.CreateUsageLimit is not implemented")) -} - -func (UnimplementedInstanceServiceHandler) UpdateUsageLimit(context.Context, *connect.Request[api.UpdateUsageLimitRequest]) (*connect.Response[api.UsageLimit], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.InstanceService.UpdateUsageLimit is not implemented")) -} - -func (UnimplementedInstanceServiceHandler) DeleteUsageLimit(context.Context, *connect.Request[api.DeleteUsageLimitRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.InstanceService.DeleteUsageLimit is not implemented")) -} diff --git a/proto/gen/api/instance_service.pb.go b/proto/gen/api/instance_service.pb.go index da69bbb79562c..dd01474d31262 100644 --- a/proto/gen/api/instance_service.pb.go +++ b/proto/gen/api/instance_service.pb.go @@ -98,6 +98,8 @@ const ( InstanceSetting_AI InstanceSetting_Key = 6 // Instance access policy settings. InstanceSetting_ACCESS InstanceSetting_Key = 7 + // Usage limits. + InstanceSetting_USAGE_LIMITS InstanceSetting_Key = 8 ) // Enum value maps for InstanceSetting_Key. @@ -111,6 +113,7 @@ var ( 5: "NOTIFICATION", 6: "AI", 7: "ACCESS", + 8: "USAGE_LIMITS", } InstanceSetting_Key_value = map[string]int32{ "KEY_UNSPECIFIED": 0, @@ -121,6 +124,7 @@ var ( "NOTIFICATION": 5, "AI": 6, "ACCESS": 7, + "USAGE_LIMITS": 8, } ) @@ -315,68 +319,6 @@ func (InstanceSetting_StorageSetting_StorageType) EnumDescriptor() ([]byte, []in return file_api_instance_service_proto_rawDescGZIP(), []int{2, 1, 0} } -// Metric is what a usage limit counts. -type UsageLimit_Metric int32 - -const ( - UsageLimit_METRIC_UNSPECIFIED UsageLimit_Metric = 0 - // The number of memos the user created, including comments, archived - // memos, and memos in Spaces. - UsageLimit_MEMO_COUNT UsageLimit_Metric = 1 - // The UTF-8 size in bytes of one memo's content. - UsageLimit_MEMO_CONTENT_BYTES UsageLimit_Metric = 2 - // The total size in bytes of the user's uploads, including uploads not - // attached to a memo. External links do not count. - UsageLimit_STORAGE_BYTES UsageLimit_Metric = 3 - // The size in bytes of one uploaded file. - UsageLimit_UPLOAD_BYTES UsageLimit_Metric = 4 -) - -// Enum value maps for UsageLimit_Metric. -var ( - UsageLimit_Metric_name = map[int32]string{ - 0: "METRIC_UNSPECIFIED", - 1: "MEMO_COUNT", - 2: "MEMO_CONTENT_BYTES", - 3: "STORAGE_BYTES", - 4: "UPLOAD_BYTES", - } - UsageLimit_Metric_value = map[string]int32{ - "METRIC_UNSPECIFIED": 0, - "MEMO_COUNT": 1, - "MEMO_CONTENT_BYTES": 2, - "STORAGE_BYTES": 3, - "UPLOAD_BYTES": 4, - } -) - -func (x UsageLimit_Metric) Enum() *UsageLimit_Metric { - p := new(UsageLimit_Metric) - *p = x - return p -} - -func (x UsageLimit_Metric) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (UsageLimit_Metric) Descriptor() protoreflect.EnumDescriptor { - return file_api_instance_service_proto_enumTypes[5].Descriptor() -} - -func (UsageLimit_Metric) Type() protoreflect.EnumType { - return &file_api_instance_service_proto_enumTypes[5] -} - -func (x UsageLimit_Metric) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use UsageLimit_Metric.Descriptor instead. -func (UsageLimit_Metric) EnumDescriptor() ([]byte, []int) { - return file_api_instance_service_proto_rawDescGZIP(), []int{10, 0} -} - // InstanceProfile is the basic information about the instance. type InstanceProfile struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -545,6 +487,7 @@ type InstanceSetting struct { // *InstanceSetting_NotificationSetting_ // *InstanceSetting_AiSetting // *InstanceSetting_AccessSetting_ + // *InstanceSetting_UsageLimitsSetting_ Value isInstanceSetting_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -657,6 +600,15 @@ func (x *InstanceSetting) GetAccessSetting() *InstanceSetting_AccessSetting { return nil } +func (x *InstanceSetting) GetUsageLimitsSetting() *InstanceSetting_UsageLimitsSetting { + if x != nil { + if x, ok := x.Value.(*InstanceSetting_UsageLimitsSetting_); ok { + return x.UsageLimitsSetting + } + } + return nil +} + type isInstanceSetting_Value interface { isInstanceSetting_Value() } @@ -696,6 +648,11 @@ type InstanceSetting_AccessSetting_ struct { AccessSetting *InstanceSetting_AccessSetting `protobuf:"bytes,8,opt,name=access_setting,json=accessSetting,proto3,oneof"` } +type InstanceSetting_UsageLimitsSetting_ struct { + // The value of the USAGE_LIMITS setting. + UsageLimitsSetting *InstanceSetting_UsageLimitsSetting `protobuf:"bytes,9,opt,name=usage_limits_setting,json=usageLimitsSetting,proto3,oneof"` +} + func (*InstanceSetting_GeneralSetting_) isInstanceSetting_Value() {} func (*InstanceSetting_StorageSetting_) isInstanceSetting_Value() {} @@ -710,6 +667,8 @@ func (*InstanceSetting_AiSetting) isInstanceSetting_Value() {} func (*InstanceSetting_AccessSetting_) isInstanceSetting_Value() {} +func (*InstanceSetting_UsageLimitsSetting_) isInstanceSetting_Value() {} + // Request message for InstanceService.GetInstanceSetting. type GetInstanceSettingRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1062,431 +1021,6 @@ func (*GetInstanceStatsRequest) Descriptor() ([]byte, []int) { return file_api_instance_service_proto_rawDescGZIP(), []int{9} } -// UsageLimit sets one limit for every user, or for one user. When both exist -// for the same metric, the limit for the user applies to that user. -type UsageLimit struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Identifier. The resource name of the usage limit. - // Format: usageLimits/{usage_limit} - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Required. Immutable. What is limited. - Metric UsageLimit_Metric `protobuf:"varint,2,opt,name=metric,proto3,enum=memos.api.UsageLimit_Metric" json:"metric,omitempty"` - // Optional. Immutable. The user the limit applies to. Empty means every user. - // Format: users/{user} - User string `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` - // Required. The limit. Must not be negative. 0 means unlimited. - Value int64 `protobuf:"varint,4,opt,name=value,proto3" json:"value,omitempty"` - // Output only. The creation time. - CreateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` - // Output only. The last update time. - UpdateTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UsageLimit) Reset() { - *x = UsageLimit{} - mi := &file_api_instance_service_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UsageLimit) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UsageLimit) ProtoMessage() {} - -func (x *UsageLimit) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[10] - 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 UsageLimit.ProtoReflect.Descriptor instead. -func (*UsageLimit) Descriptor() ([]byte, []int) { - return file_api_instance_service_proto_rawDescGZIP(), []int{10} -} - -func (x *UsageLimit) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *UsageLimit) GetMetric() UsageLimit_Metric { - if x != nil { - return x.Metric - } - return UsageLimit_METRIC_UNSPECIFIED -} - -func (x *UsageLimit) GetUser() string { - if x != nil { - return x.User - } - return "" -} - -func (x *UsageLimit) GetValue() int64 { - if x != nil { - return x.Value - } - return 0 -} - -func (x *UsageLimit) GetCreateTime() *timestamppb.Timestamp { - if x != nil { - return x.CreateTime - } - return nil -} - -func (x *UsageLimit) GetUpdateTime() *timestamppb.Timestamp { - if x != nil { - return x.UpdateTime - } - return nil -} - -// Request message for InstanceService.ListUsageLimits. -type ListUsageLimitsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Optional. The maximum number of usage limits to return. If unspecified, at - // most 50 are returned. The maximum value is 1000; values above 1000 are - // coerced to 1000. - PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` - // Optional. A page token from a previous call. Provide it to retrieve the - // next page. - PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` - // Optional. A CEL filter on `user` and `metric`, combined with `&&`. - // `user == ""` selects the defaults for every user. - // Example: "user == 'users/steven' && metric == 'STORAGE_BYTES'" - Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListUsageLimitsRequest) Reset() { - *x = ListUsageLimitsRequest{} - mi := &file_api_instance_service_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListUsageLimitsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListUsageLimitsRequest) ProtoMessage() {} - -func (x *ListUsageLimitsRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[11] - 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 ListUsageLimitsRequest.ProtoReflect.Descriptor instead. -func (*ListUsageLimitsRequest) Descriptor() ([]byte, []int) { - return file_api_instance_service_proto_rawDescGZIP(), []int{11} -} - -func (x *ListUsageLimitsRequest) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *ListUsageLimitsRequest) GetPageToken() string { - if x != nil { - return x.PageToken - } - return "" -} - -func (x *ListUsageLimitsRequest) GetFilter() string { - if x != nil { - return x.Filter - } - return "" -} - -// Response message for InstanceService.ListUsageLimits. -type ListUsageLimitsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The usage limits, defaults first, then by user and metric. - UsageLimits []*UsageLimit `protobuf:"bytes,1,rep,name=usage_limits,json=usageLimits,proto3" json:"usage_limits,omitempty"` - // A token to retrieve the next page. Empty when there are no more pages. - NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListUsageLimitsResponse) Reset() { - *x = ListUsageLimitsResponse{} - mi := &file_api_instance_service_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListUsageLimitsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListUsageLimitsResponse) ProtoMessage() {} - -func (x *ListUsageLimitsResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[12] - 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 ListUsageLimitsResponse.ProtoReflect.Descriptor instead. -func (*ListUsageLimitsResponse) Descriptor() ([]byte, []int) { - return file_api_instance_service_proto_rawDescGZIP(), []int{12} -} - -func (x *ListUsageLimitsResponse) GetUsageLimits() []*UsageLimit { - if x != nil { - return x.UsageLimits - } - return nil -} - -func (x *ListUsageLimitsResponse) GetNextPageToken() string { - if x != nil { - return x.NextPageToken - } - return "" -} - -// Request message for InstanceService.GetUsageLimit. -type GetUsageLimitRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Required. The resource name of the usage limit. - // Format: usageLimits/{usage_limit} - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetUsageLimitRequest) Reset() { - *x = GetUsageLimitRequest{} - mi := &file_api_instance_service_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetUsageLimitRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetUsageLimitRequest) ProtoMessage() {} - -func (x *GetUsageLimitRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[13] - 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 GetUsageLimitRequest.ProtoReflect.Descriptor instead. -func (*GetUsageLimitRequest) Descriptor() ([]byte, []int) { - return file_api_instance_service_proto_rawDescGZIP(), []int{13} -} - -func (x *GetUsageLimitRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -// Request message for InstanceService.CreateUsageLimit. -type CreateUsageLimitRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Required. The usage limit to create. - UsageLimit *UsageLimit `protobuf:"bytes,1,opt,name=usage_limit,json=usageLimit,proto3" json:"usage_limit,omitempty"` - // Optional. The ID to use for the usage limit, which becomes the final - // segment of its name. Lowercase letters, digits and hyphens, at most 32 - // characters, starting with a letter. Generated when empty. - UsageLimitId string `protobuf:"bytes,2,opt,name=usage_limit_id,json=usageLimitId,proto3" json:"usage_limit_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateUsageLimitRequest) Reset() { - *x = CreateUsageLimitRequest{} - mi := &file_api_instance_service_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateUsageLimitRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateUsageLimitRequest) ProtoMessage() {} - -func (x *CreateUsageLimitRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[14] - 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 CreateUsageLimitRequest.ProtoReflect.Descriptor instead. -func (*CreateUsageLimitRequest) Descriptor() ([]byte, []int) { - return file_api_instance_service_proto_rawDescGZIP(), []int{14} -} - -func (x *CreateUsageLimitRequest) GetUsageLimit() *UsageLimit { - if x != nil { - return x.UsageLimit - } - return nil -} - -func (x *CreateUsageLimitRequest) GetUsageLimitId() string { - if x != nil { - return x.UsageLimitId - } - return "" -} - -// Request message for InstanceService.UpdateUsageLimit. -type UpdateUsageLimitRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Required. The usage limit with its new value. - UsageLimit *UsageLimit `protobuf:"bytes,1,opt,name=usage_limit,json=usageLimit,proto3" json:"usage_limit,omitempty"` - // Optional. The fields to update. Only `value` is supported, which is also - // the default. - UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateUsageLimitRequest) Reset() { - *x = UpdateUsageLimitRequest{} - mi := &file_api_instance_service_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateUsageLimitRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateUsageLimitRequest) ProtoMessage() {} - -func (x *UpdateUsageLimitRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[15] - 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 UpdateUsageLimitRequest.ProtoReflect.Descriptor instead. -func (*UpdateUsageLimitRequest) Descriptor() ([]byte, []int) { - return file_api_instance_service_proto_rawDescGZIP(), []int{15} -} - -func (x *UpdateUsageLimitRequest) GetUsageLimit() *UsageLimit { - if x != nil { - return x.UsageLimit - } - return nil -} - -func (x *UpdateUsageLimitRequest) GetUpdateMask() *fieldmaskpb.FieldMask { - if x != nil { - return x.UpdateMask - } - return nil -} - -// Request message for InstanceService.DeleteUsageLimit. -type DeleteUsageLimitRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Required. The resource name of the usage limit. - // Format: usageLimits/{usage_limit} - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteUsageLimitRequest) Reset() { - *x = DeleteUsageLimitRequest{} - mi := &file_api_instance_service_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteUsageLimitRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteUsageLimitRequest) ProtoMessage() {} - -func (x *DeleteUsageLimitRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[16] - 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 DeleteUsageLimitRequest.ProtoReflect.Descriptor instead. -func (*DeleteUsageLimitRequest) Descriptor() ([]byte, []int) { - return file_api_instance_service_proto_rawDescGZIP(), []int{16} -} - -func (x *DeleteUsageLimitRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - // Challenge describes a proof-of-humanity challenge widget. type InstanceProfile_Challenge struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1500,7 +1034,7 @@ type InstanceProfile_Challenge struct { func (x *InstanceProfile_Challenge) Reset() { *x = InstanceProfile_Challenge{} - mi := &file_api_instance_service_proto_msgTypes[17] + mi := &file_api_instance_service_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1512,7 +1046,7 @@ func (x *InstanceProfile_Challenge) String() string { func (*InstanceProfile_Challenge) ProtoMessage() {} func (x *InstanceProfile_Challenge) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[17] + mi := &file_api_instance_service_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1569,7 +1103,7 @@ type InstanceSetting_GeneralSetting struct { func (x *InstanceSetting_GeneralSetting) Reset() { *x = InstanceSetting_GeneralSetting{} - mi := &file_api_instance_service_proto_msgTypes[18] + mi := &file_api_instance_service_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1581,7 +1115,7 @@ func (x *InstanceSetting_GeneralSetting) String() string { func (*InstanceSetting_GeneralSetting) ProtoMessage() {} func (x *InstanceSetting_GeneralSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[18] + mi := &file_api_instance_service_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1673,7 +1207,7 @@ type InstanceSetting_StorageSetting struct { func (x *InstanceSetting_StorageSetting) Reset() { *x = InstanceSetting_StorageSetting{} - mi := &file_api_instance_service_proto_msgTypes[19] + mi := &file_api_instance_service_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1685,7 +1219,7 @@ func (x *InstanceSetting_StorageSetting) String() string { func (*InstanceSetting_StorageSetting) ProtoMessage() {} func (x *InstanceSetting_StorageSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[19] + mi := &file_api_instance_service_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1757,7 +1291,7 @@ type InstanceSetting_Storage struct { func (x *InstanceSetting_Storage) Reset() { *x = InstanceSetting_Storage{} - mi := &file_api_instance_service_proto_msgTypes[20] + mi := &file_api_instance_service_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1769,7 +1303,7 @@ func (x *InstanceSetting_Storage) String() string { func (*InstanceSetting_Storage) ProtoMessage() {} func (x *InstanceSetting_Storage) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[20] + mi := &file_api_instance_service_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1846,7 +1380,7 @@ type InstanceSetting_MemoRelatedSetting struct { func (x *InstanceSetting_MemoRelatedSetting) Reset() { *x = InstanceSetting_MemoRelatedSetting{} - mi := &file_api_instance_service_proto_msgTypes[21] + mi := &file_api_instance_service_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1858,7 +1392,7 @@ func (x *InstanceSetting_MemoRelatedSetting) String() string { func (*InstanceSetting_MemoRelatedSetting) ProtoMessage() {} func (x *InstanceSetting_MemoRelatedSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[21] + mi := &file_api_instance_service_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1903,7 +1437,7 @@ type InstanceSetting_TagsSetting struct { func (x *InstanceSetting_TagsSetting) Reset() { *x = InstanceSetting_TagsSetting{} - mi := &file_api_instance_service_proto_msgTypes[22] + mi := &file_api_instance_service_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1915,7 +1449,7 @@ func (x *InstanceSetting_TagsSetting) String() string { func (*InstanceSetting_TagsSetting) ProtoMessage() {} func (x *InstanceSetting_TagsSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[22] + mi := &file_api_instance_service_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1951,7 +1485,7 @@ type InstanceSetting_TagMetadata struct { func (x *InstanceSetting_TagMetadata) Reset() { *x = InstanceSetting_TagMetadata{} - mi := &file_api_instance_service_proto_msgTypes[23] + mi := &file_api_instance_service_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1963,7 +1497,7 @@ func (x *InstanceSetting_TagMetadata) String() string { func (*InstanceSetting_TagMetadata) ProtoMessage() {} func (x *InstanceSetting_TagMetadata) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[23] + mi := &file_api_instance_service_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2004,7 +1538,7 @@ type InstanceSetting_NotificationSetting struct { func (x *InstanceSetting_NotificationSetting) Reset() { *x = InstanceSetting_NotificationSetting{} - mi := &file_api_instance_service_proto_msgTypes[24] + mi := &file_api_instance_service_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2016,7 +1550,7 @@ func (x *InstanceSetting_NotificationSetting) String() string { func (*InstanceSetting_NotificationSetting) ProtoMessage() {} func (x *InstanceSetting_NotificationSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[24] + mi := &file_api_instance_service_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2054,7 +1588,7 @@ type InstanceSetting_AISetting struct { func (x *InstanceSetting_AISetting) Reset() { *x = InstanceSetting_AISetting{} - mi := &file_api_instance_service_proto_msgTypes[25] + mi := &file_api_instance_service_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2066,7 +1600,7 @@ func (x *InstanceSetting_AISetting) String() string { func (*InstanceSetting_AISetting) ProtoMessage() {} func (x *InstanceSetting_AISetting) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[25] + mi := &file_api_instance_service_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2119,7 +1653,7 @@ type InstanceSetting_AIProviderConfig struct { func (x *InstanceSetting_AIProviderConfig) Reset() { *x = InstanceSetting_AIProviderConfig{} - mi := &file_api_instance_service_proto_msgTypes[26] + mi := &file_api_instance_service_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2131,7 +1665,7 @@ func (x *InstanceSetting_AIProviderConfig) String() string { func (*InstanceSetting_AIProviderConfig) ProtoMessage() {} func (x *InstanceSetting_AIProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[26] + mi := &file_api_instance_service_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2216,7 +1750,7 @@ type InstanceSetting_TranscriptionConfig struct { func (x *InstanceSetting_TranscriptionConfig) Reset() { *x = InstanceSetting_TranscriptionConfig{} - mi := &file_api_instance_service_proto_msgTypes[27] + mi := &file_api_instance_service_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2228,7 +1762,7 @@ func (x *InstanceSetting_TranscriptionConfig) String() string { func (*InstanceSetting_TranscriptionConfig) ProtoMessage() {} func (x *InstanceSetting_TranscriptionConfig) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[27] + mi := &file_api_instance_service_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2283,7 +1817,7 @@ type InstanceSetting_AccessSetting struct { func (x *InstanceSetting_AccessSetting) Reset() { *x = InstanceSetting_AccessSetting{} - mi := &file_api_instance_service_proto_msgTypes[28] + mi := &file_api_instance_service_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2295,7 +1829,7 @@ func (x *InstanceSetting_AccessSetting) String() string { func (*InstanceSetting_AccessSetting) ProtoMessage() {} func (x *InstanceSetting_AccessSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[28] + mi := &file_api_instance_service_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2318,6 +1852,56 @@ func (x *InstanceSetting_AccessSetting) GetAccessMode() InstanceAccessMode { return InstanceAccessMode_INSTANCE_ACCESS_MODE_UNSPECIFIED } +// UsageLimitsSetting is an ordered list of limit sets. The first entry that +// names a user is that user's limit set, whole. The last entry has all_users +// set and is the default. The setting is read and replaced whole. Admins +// are limited like everyone else. +type InstanceSetting_UsageLimitsSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The entries, in the order they are checked. At most 100, naming at most + // 1,000 users in all. + Limits []*InstanceSetting_UsageLimitsSetting_UsageLimit `protobuf:"bytes,1,rep,name=limits,proto3" json:"limits,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstanceSetting_UsageLimitsSetting) Reset() { + *x = InstanceSetting_UsageLimitsSetting{} + mi := &file_api_instance_service_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstanceSetting_UsageLimitsSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstanceSetting_UsageLimitsSetting) ProtoMessage() {} + +func (x *InstanceSetting_UsageLimitsSetting) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_service_proto_msgTypes[22] + 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 InstanceSetting_UsageLimitsSetting.ProtoReflect.Descriptor instead. +func (*InstanceSetting_UsageLimitsSetting) Descriptor() ([]byte, []int) { + return file_api_instance_service_proto_rawDescGZIP(), []int{2, 11} +} + +func (x *InstanceSetting_UsageLimitsSetting) GetLimits() []*InstanceSetting_UsageLimitsSetting_UsageLimit { + if x != nil { + return x.Limits + } + return nil +} + // CustomProfile is the instance branding. type InstanceSetting_GeneralSetting_CustomProfile struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2333,7 +1917,7 @@ type InstanceSetting_GeneralSetting_CustomProfile struct { func (x *InstanceSetting_GeneralSetting_CustomProfile) Reset() { *x = InstanceSetting_GeneralSetting_CustomProfile{} - mi := &file_api_instance_service_proto_msgTypes[29] + mi := &file_api_instance_service_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2345,7 +1929,7 @@ func (x *InstanceSetting_GeneralSetting_CustomProfile) String() string { func (*InstanceSetting_GeneralSetting_CustomProfile) ProtoMessage() {} func (x *InstanceSetting_GeneralSetting_CustomProfile) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[29] + mi := &file_api_instance_service_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2408,7 +1992,7 @@ type InstanceSetting_StorageSetting_S3Config struct { func (x *InstanceSetting_StorageSetting_S3Config) Reset() { *x = InstanceSetting_StorageSetting_S3Config{} - mi := &file_api_instance_service_proto_msgTypes[30] + mi := &file_api_instance_service_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2420,7 +2004,7 @@ func (x *InstanceSetting_StorageSetting_S3Config) String() string { func (*InstanceSetting_StorageSetting_S3Config) ProtoMessage() {} func (x *InstanceSetting_StorageSetting_S3Config) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[30] + mi := &file_api_instance_service_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2511,7 +2095,7 @@ type InstanceSetting_Storage_S3Config struct { func (x *InstanceSetting_Storage_S3Config) Reset() { *x = InstanceSetting_Storage_S3Config{} - mi := &file_api_instance_service_proto_msgTypes[31] + mi := &file_api_instance_service_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2523,7 +2107,7 @@ func (x *InstanceSetting_Storage_S3Config) String() string { func (*InstanceSetting_Storage_S3Config) ProtoMessage() {} func (x *InstanceSetting_Storage_S3Config) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[31] + mi := &file_api_instance_service_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2618,7 +2202,7 @@ type InstanceSetting_NotificationSetting_EmailSetting struct { func (x *InstanceSetting_NotificationSetting_EmailSetting) Reset() { *x = InstanceSetting_NotificationSetting_EmailSetting{} - mi := &file_api_instance_service_proto_msgTypes[33] + mi := &file_api_instance_service_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2630,7 +2214,7 @@ func (x *InstanceSetting_NotificationSetting_EmailSetting) String() string { func (*InstanceSetting_NotificationSetting_EmailSetting) ProtoMessage() {} func (x *InstanceSetting_NotificationSetting_EmailSetting) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[33] + mi := &file_api_instance_service_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2716,6 +2300,102 @@ func (x *InstanceSetting_NotificationSetting_EmailSetting) GetReplyTo() string { return "" } +// UsageLimit is one entry: who it applies to and the limit for each +// metric. An unset limit is unlimited, as is 0. +type InstanceSetting_UsageLimitsSetting_UsageLimit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The users the entry applies to. Not repeated within an entry. + // Format: users/{user} + Users []string `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + // Whether the entry applies to every user. Exactly one entry has it, and + // it is the last entry. An entry has users or all_users, never both. + AllUsers bool `protobuf:"varint,3,opt,name=all_users,json=allUsers,proto3" json:"all_users,omitempty"` + // The number of memos the user may have created, comments and archived + // memos included. + MemoCount *int64 `protobuf:"varint,4,opt,name=memo_count,json=memoCount,proto3,oneof" json:"memo_count,omitempty"` + // The UTF-8 size in bytes of one memo's content. + MemoContentBytes *int64 `protobuf:"varint,5,opt,name=memo_content_bytes,json=memoContentBytes,proto3,oneof" json:"memo_content_bytes,omitempty"` + // The total size in bytes of the user's uploads. External links do not + // count. + StorageBytes *int64 `protobuf:"varint,6,opt,name=storage_bytes,json=storageBytes,proto3,oneof" json:"storage_bytes,omitempty"` + // The size in bytes of one uploaded file. + UploadBytes *int64 `protobuf:"varint,7,opt,name=upload_bytes,json=uploadBytes,proto3,oneof" json:"upload_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstanceSetting_UsageLimitsSetting_UsageLimit) Reset() { + *x = InstanceSetting_UsageLimitsSetting_UsageLimit{} + mi := &file_api_instance_service_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstanceSetting_UsageLimitsSetting_UsageLimit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstanceSetting_UsageLimitsSetting_UsageLimit) ProtoMessage() {} + +func (x *InstanceSetting_UsageLimitsSetting_UsageLimit) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_service_proto_msgTypes[28] + 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 InstanceSetting_UsageLimitsSetting_UsageLimit.ProtoReflect.Descriptor instead. +func (*InstanceSetting_UsageLimitsSetting_UsageLimit) Descriptor() ([]byte, []int) { + return file_api_instance_service_proto_rawDescGZIP(), []int{2, 11, 0} +} + +func (x *InstanceSetting_UsageLimitsSetting_UsageLimit) GetUsers() []string { + if x != nil { + return x.Users + } + return nil +} + +func (x *InstanceSetting_UsageLimitsSetting_UsageLimit) GetAllUsers() bool { + if x != nil { + return x.AllUsers + } + return false +} + +func (x *InstanceSetting_UsageLimitsSetting_UsageLimit) GetMemoCount() int64 { + if x != nil && x.MemoCount != nil { + return *x.MemoCount + } + return 0 +} + +func (x *InstanceSetting_UsageLimitsSetting_UsageLimit) GetMemoContentBytes() int64 { + if x != nil && x.MemoContentBytes != nil { + return *x.MemoContentBytes + } + return 0 +} + +func (x *InstanceSetting_UsageLimitsSetting_UsageLimit) GetStorageBytes() int64 { + if x != nil && x.StorageBytes != nil { + return *x.StorageBytes + } + return 0 +} + +func (x *InstanceSetting_UsageLimitsSetting_UsageLimit) GetUploadBytes() int64 { + if x != nil && x.UploadBytes != nil { + return *x.UploadBytes + } + return 0 +} + // DatabaseStats is the database size statistics. type InstanceStats_DatabaseStats struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2729,7 +2409,7 @@ type InstanceStats_DatabaseStats struct { func (x *InstanceStats_DatabaseStats) Reset() { *x = InstanceStats_DatabaseStats{} - mi := &file_api_instance_service_proto_msgTypes[34] + mi := &file_api_instance_service_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2741,7 +2421,7 @@ func (x *InstanceStats_DatabaseStats) String() string { func (*InstanceStats_DatabaseStats) ProtoMessage() {} func (x *InstanceStats_DatabaseStats) ProtoReflect() protoreflect.Message { - mi := &file_api_instance_service_proto_msgTypes[34] + mi := &file_api_instance_service_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2790,7 +2470,7 @@ const file_api_instance_service_proto_rawDesc = "" + "\tChallenge\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x19\n" + "\bsite_key\x18\x02 \x01(\tR\asiteKey\"\x1b\n" + - "\x19GetInstanceProfileRequest\"\x8d!\n" + + "\x19GetInstanceProfileRequest\"\xb5%\n" + "\x0fInstanceSetting\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12T\n" + "\x0fgeneral_setting\x18\x02 \x01(\v2).memos.api.InstanceSetting.GeneralSettingH\x00R\x0egeneralSetting\x12T\n" + @@ -2800,7 +2480,8 @@ const file_api_instance_service_proto_rawDesc = "" + "\x14notification_setting\x18\x06 \x01(\v2..memos.api.InstanceSetting.NotificationSettingH\x00R\x13notificationSetting\x12E\n" + "\n" + "ai_setting\x18\a \x01(\v2$.memos.api.InstanceSetting.AISettingH\x00R\taiSetting\x12Q\n" + - "\x0eaccess_setting\x18\b \x01(\v2(.memos.api.InstanceSetting.AccessSettingH\x00R\raccessSetting\x1a\xc7\x04\n" + + "\x0eaccess_setting\x18\b \x01(\v2(.memos.api.InstanceSetting.AccessSettingH\x00R\raccessSetting\x12a\n" + + "\x14usage_limits_setting\x18\t \x01(\v2-.memos.api.InstanceSetting.UsageLimitsSettingH\x00R\x12usageLimitsSetting\x1a\xc7\x04\n" + "\x0eGeneralSetting\x12^\n" + "\x0ecustom_profile\x18\x01 \x01(\v27.memos.api.InstanceSetting.GeneralSetting.CustomProfileR\rcustomProfile\x12<\n" + "\x1adisallow_user_registration\x18\x02 \x01(\bR\x18disallowUserRegistration\x124\n" + @@ -2893,7 +2574,23 @@ const file_api_instance_service_proto_rawDesc = "" + "\x06prompt\x18\x04 \x01(\tR\x06prompt\x1aO\n" + "\rAccessSetting\x12>\n" + "\vaccess_mode\x18\x01 \x01(\x0e2\x1d.memos.api.InstanceAccessModeR\n" + - "accessMode\"v\n" + + "accessMode\x1a\xaf\x03\n" + + "\x12UsageLimitsSetting\x12P\n" + + "\x06limits\x18\x01 \x03(\v28.memos.api.InstanceSetting.UsageLimitsSetting.UsageLimitR\x06limits\x1a\xc6\x02\n" + + "\n" + + "UsageLimit\x12)\n" + + "\x05users\x18\x01 \x03(\tB\x13\xfaA\x10\n" + + "\x0ememos.api/UserR\x05users\x12\x1b\n" + + "\tall_users\x18\x03 \x01(\bR\ballUsers\x12\"\n" + + "\n" + + "memo_count\x18\x04 \x01(\x03H\x00R\tmemoCount\x88\x01\x01\x121\n" + + "\x12memo_content_bytes\x18\x05 \x01(\x03H\x01R\x10memoContentBytes\x88\x01\x01\x12(\n" + + "\rstorage_bytes\x18\x06 \x01(\x03H\x02R\fstorageBytes\x88\x01\x01\x12&\n" + + "\fupload_bytes\x18\a \x01(\x03H\x03R\vuploadBytes\x88\x01\x01B\r\n" + + "\v_memo_countB\x15\n" + + "\x13_memo_content_bytesB\x10\n" + + "\x0e_storage_bytesB\x0f\n" + + "\r_upload_bytes\"\x88\x01\n" + "\x03Key\x12\x13\n" + "\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" + "\aGENERAL\x10\x01\x12\v\n" + @@ -2903,7 +2600,8 @@ const file_api_instance_service_proto_rawDesc = "" + "\fNOTIFICATION\x10\x05\x12\x06\n" + "\x02AI\x10\x06\x12\n" + "\n" + - "\x06ACCESS\x10\a\"L\n" + + "\x06ACCESS\x10\a\x12\x10\n" + + "\fUSAGE_LIMITS\x10\b\"L\n" + "\vStorageType\x12\x1c\n" + "\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" + "\bDATABASE\x10\x01\x12\t\n" + @@ -2940,66 +2638,18 @@ const file_api_instance_service_proto_rawDesc = "" + "\x06driver\x18\x01 \x01(\tR\x06driver\x12\x1d\n" + "\n" + "size_bytes\x18\x02 \x01(\x03R\tsizeBytes\"\x19\n" + - "\x17GetInstanceStatsRequest\"\xef\x03\n" + - "\n" + - "UsageLimit\x12\x17\n" + - "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12<\n" + - "\x06metric\x18\x02 \x01(\x0e2\x1c.memos.api.UsageLimit.MetricB\x06\xe0A\x02\xe0A\x05R\x06metric\x12-\n" + - "\x04user\x18\x03 \x01(\tB\x19\xe0A\x01\xe0A\x05\xfaA\x10\n" + - "\x0ememos.api/UserR\x04user\x12\x19\n" + - "\x05value\x18\x04 \x01(\x03B\x03\xe0A\x02R\x05value\x12@\n" + - "\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + - "createTime\x12@\n" + - "\vupdate_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + - "updateTime\"m\n" + - "\x06Metric\x12\x16\n" + - "\x12METRIC_UNSPECIFIED\x10\x00\x12\x0e\n" + - "\n" + - "MEMO_COUNT\x10\x01\x12\x16\n" + - "\x12MEMO_CONTENT_BYTES\x10\x02\x12\x11\n" + - "\rSTORAGE_BYTES\x10\x03\x12\x10\n" + - "\fUPLOAD_BYTES\x10\x04:M\xeaAJ\n" + - "\x14memos.api/UsageLimit\x12\x19usageLimits/{usage_limit}*\vusageLimits2\n" + - "usageLimit\"{\n" + - "\x16ListUsageLimitsRequest\x12 \n" + - "\tpage_size\x18\x01 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + - "\n" + - "page_token\x18\x02 \x01(\tB\x03\xe0A\x01R\tpageToken\x12\x1b\n" + - "\x06filter\x18\x03 \x01(\tB\x03\xe0A\x01R\x06filter\"{\n" + - "\x17ListUsageLimitsResponse\x128\n" + - "\fusage_limits\x18\x01 \x03(\v2\x15.memos.api.UsageLimitR\vusageLimits\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"H\n" + - "\x14GetUsageLimitRequest\x120\n" + - "\x04name\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\n" + - "\x14memos.api/UsageLimitR\x04name\"\x81\x01\n" + - "\x17CreateUsageLimitRequest\x12;\n" + - "\vusage_limit\x18\x01 \x01(\v2\x15.memos.api.UsageLimitB\x03\xe0A\x02R\n" + - "usageLimit\x12)\n" + - "\x0eusage_limit_id\x18\x02 \x01(\tB\x03\xe0A\x01R\fusageLimitId\"\x98\x01\n" + - "\x17UpdateUsageLimitRequest\x12;\n" + - "\vusage_limit\x18\x01 \x01(\v2\x15.memos.api.UsageLimitB\x03\xe0A\x02R\n" + - "usageLimit\x12@\n" + - "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x01R\n" + - "updateMask\"K\n" + - "\x17DeleteUsageLimitRequest\x120\n" + - "\x04name\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\n" + - "\x14memos.api/UsageLimitR\x04name*}\n" + + "\x17GetInstanceStatsRequest*}\n" + "\x12InstanceAccessMode\x12$\n" + " INSTANCE_ACCESS_MODE_UNSPECIFIED\x10\x00\x12 \n" + "\x1cINSTANCE_ACCESS_MODE_PRIVATE\x10\x01\x12\x1f\n" + - "\x1bINSTANCE_ACCESS_MODE_PUBLIC\x10\x022\xa2\f\n" + + "\x1bINSTANCE_ACCESS_MODE_PUBLIC\x10\x022\x87\a\n" + "\x0fInstanceService\x12u\n" + "\x12GetInstanceProfile\x12$.memos.api.GetInstanceProfileRequest\x1a\x1a.memos.api.InstanceProfile\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/api/instance/profile\x12\x86\x01\n" + "\x12GetInstanceSetting\x12$.memos.api.GetInstanceSettingRequest\x1a\x1a.memos.api.InstanceSetting\".\xdaA\x04name\x82\xd3\xe4\x93\x02!\x12\x1f/api/{name=instance/settings/*}\x12\x9f\x01\n" + "\x18BatchGetInstanceSettings\x12*.memos.api.BatchGetInstanceSettingsRequest\x1a+.memos.api.BatchGetInstanceSettingsResponse\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/api/instance/settings:batchGet\x12\xc7\x01\n" + "\x15UpdateInstanceSetting\x12'.memos.api.UpdateInstanceSettingRequest\x1a\x1a.memos.api.InstanceSetting\"i\xdaA\x1cinstance_setting,update_mask\x82\xd3\xe4\x93\x02D:\x10instance_setting20/api/{instance_setting.name=instance/settings/*}\x12\x98\x01\n" + "\x18TestInstanceEmailSetting\x12*.memos.api.TestInstanceEmailSettingRequest\x1a\x16.google.protobuf.Empty\"8\x82\xd3\xe4\x93\x022:\x01*\"-/api/instance/settings/notification:testEmail\x12m\n" + - "\x10GetInstanceStats\x12\".memos.api.GetInstanceStatsRequest\x1a\x18.memos.api.InstanceStats\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/instance/stats\x12r\n" + - "\x0fListUsageLimits\x12!.memos.api.ListUsageLimitsRequest\x1a\".memos.api.ListUsageLimitsResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/api/usageLimits\x12q\n" + - "\rGetUsageLimit\x12\x1f.memos.api.GetUsageLimitRequest\x1a\x15.memos.api.UsageLimit\"(\xdaA\x04name\x82\xd3\xe4\x93\x02\x1b\x12\x19/api/{name=usageLimits/*}\x12\x91\x01\n" + - "\x10CreateUsageLimit\x12\".memos.api.CreateUsageLimitRequest\x1a\x15.memos.api.UsageLimit\"B\xdaA\x1ausage_limit,usage_limit_id\x82\xd3\xe4\x93\x02\x1f:\vusage_limit\"\x10/api/usageLimits\x12\xa3\x01\n" + - "\x10UpdateUsageLimit\x12\".memos.api.UpdateUsageLimitRequest\x1a\x15.memos.api.UsageLimit\"T\xdaA\x17usage_limit,update_mask\x82\xd3\xe4\x93\x024:\vusage_limit2%/api/{usage_limit.name=usageLimits/*}\x12x\n" + - "\x10DeleteUsageLimit\x12\".memos.api.DeleteUsageLimitRequest\x1a\x16.google.protobuf.Empty\"(\xdaA\x04name\x82\xd3\xe4\x93\x02\x1b*\x19/api/{name=usageLimits/*}B\x93\x01\n" + + "\x10GetInstanceStats\x12\".memos.api.GetInstanceStatsRequest\x1a\x18.memos.api.InstanceStats\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/instance/statsB\x93\x01\n" + "\rcom.memos.apiB\x14InstanceServiceProtoP\x01Z'github.com/usememos/memos/proto/gen/api\xa2\x02\x03MAX\xaa\x02\tMemos.Api\xca\x02\tMemos\\Api\xe2\x02\x15Memos\\Api\\GPBMetadata\xea\x02\n" + "Memos::Apib\x06proto3" @@ -3015,121 +2665,100 @@ func file_api_instance_service_proto_rawDescGZIP() []byte { return file_api_instance_service_proto_rawDescData } -var file_api_instance_service_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_api_instance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 35) +var file_api_instance_service_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_api_instance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_api_instance_service_proto_goTypes = []any{ (InstanceAccessMode)(0), // 0: memos.api.InstanceAccessMode (InstanceSetting_Key)(0), // 1: memos.api.InstanceSetting.Key (InstanceSetting_StorageType)(0), // 2: memos.api.InstanceSetting.StorageType (InstanceSetting_AIProviderType)(0), // 3: memos.api.InstanceSetting.AIProviderType (InstanceSetting_StorageSetting_StorageType)(0), // 4: memos.api.InstanceSetting.StorageSetting.StorageType - (UsageLimit_Metric)(0), // 5: memos.api.UsageLimit.Metric - (*InstanceProfile)(nil), // 6: memos.api.InstanceProfile - (*GetInstanceProfileRequest)(nil), // 7: memos.api.GetInstanceProfileRequest - (*InstanceSetting)(nil), // 8: memos.api.InstanceSetting - (*GetInstanceSettingRequest)(nil), // 9: memos.api.GetInstanceSettingRequest - (*BatchGetInstanceSettingsRequest)(nil), // 10: memos.api.BatchGetInstanceSettingsRequest - (*BatchGetInstanceSettingsResponse)(nil), // 11: memos.api.BatchGetInstanceSettingsResponse - (*UpdateInstanceSettingRequest)(nil), // 12: memos.api.UpdateInstanceSettingRequest - (*TestInstanceEmailSettingRequest)(nil), // 13: memos.api.TestInstanceEmailSettingRequest - (*InstanceStats)(nil), // 14: memos.api.InstanceStats - (*GetInstanceStatsRequest)(nil), // 15: memos.api.GetInstanceStatsRequest - (*UsageLimit)(nil), // 16: memos.api.UsageLimit - (*ListUsageLimitsRequest)(nil), // 17: memos.api.ListUsageLimitsRequest - (*ListUsageLimitsResponse)(nil), // 18: memos.api.ListUsageLimitsResponse - (*GetUsageLimitRequest)(nil), // 19: memos.api.GetUsageLimitRequest - (*CreateUsageLimitRequest)(nil), // 20: memos.api.CreateUsageLimitRequest - (*UpdateUsageLimitRequest)(nil), // 21: memos.api.UpdateUsageLimitRequest - (*DeleteUsageLimitRequest)(nil), // 22: memos.api.DeleteUsageLimitRequest - (*InstanceProfile_Challenge)(nil), // 23: memos.api.InstanceProfile.Challenge - (*InstanceSetting_GeneralSetting)(nil), // 24: memos.api.InstanceSetting.GeneralSetting - (*InstanceSetting_StorageSetting)(nil), // 25: memos.api.InstanceSetting.StorageSetting - (*InstanceSetting_Storage)(nil), // 26: memos.api.InstanceSetting.Storage - (*InstanceSetting_MemoRelatedSetting)(nil), // 27: memos.api.InstanceSetting.MemoRelatedSetting - (*InstanceSetting_TagsSetting)(nil), // 28: memos.api.InstanceSetting.TagsSetting - (*InstanceSetting_TagMetadata)(nil), // 29: memos.api.InstanceSetting.TagMetadata - (*InstanceSetting_NotificationSetting)(nil), // 30: memos.api.InstanceSetting.NotificationSetting - (*InstanceSetting_AISetting)(nil), // 31: memos.api.InstanceSetting.AISetting - (*InstanceSetting_AIProviderConfig)(nil), // 32: memos.api.InstanceSetting.AIProviderConfig - (*InstanceSetting_TranscriptionConfig)(nil), // 33: memos.api.InstanceSetting.TranscriptionConfig - (*InstanceSetting_AccessSetting)(nil), // 34: memos.api.InstanceSetting.AccessSetting - (*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 35: memos.api.InstanceSetting.GeneralSetting.CustomProfile - (*InstanceSetting_StorageSetting_S3Config)(nil), // 36: memos.api.InstanceSetting.StorageSetting.S3Config - (*InstanceSetting_Storage_S3Config)(nil), // 37: memos.api.InstanceSetting.Storage.S3Config - nil, // 38: memos.api.InstanceSetting.TagsSetting.TagsEntry - (*InstanceSetting_NotificationSetting_EmailSetting)(nil), // 39: memos.api.InstanceSetting.NotificationSetting.EmailSetting - (*InstanceStats_DatabaseStats)(nil), // 40: memos.api.InstanceStats.DatabaseStats - (*User)(nil), // 41: memos.api.User - (*fieldmaskpb.FieldMask)(nil), // 42: google.protobuf.FieldMask - (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp - (*color.Color)(nil), // 44: google.type.Color - (*emptypb.Empty)(nil), // 45: google.protobuf.Empty + (*InstanceProfile)(nil), // 5: memos.api.InstanceProfile + (*GetInstanceProfileRequest)(nil), // 6: memos.api.GetInstanceProfileRequest + (*InstanceSetting)(nil), // 7: memos.api.InstanceSetting + (*GetInstanceSettingRequest)(nil), // 8: memos.api.GetInstanceSettingRequest + (*BatchGetInstanceSettingsRequest)(nil), // 9: memos.api.BatchGetInstanceSettingsRequest + (*BatchGetInstanceSettingsResponse)(nil), // 10: memos.api.BatchGetInstanceSettingsResponse + (*UpdateInstanceSettingRequest)(nil), // 11: memos.api.UpdateInstanceSettingRequest + (*TestInstanceEmailSettingRequest)(nil), // 12: memos.api.TestInstanceEmailSettingRequest + (*InstanceStats)(nil), // 13: memos.api.InstanceStats + (*GetInstanceStatsRequest)(nil), // 14: memos.api.GetInstanceStatsRequest + (*InstanceProfile_Challenge)(nil), // 15: memos.api.InstanceProfile.Challenge + (*InstanceSetting_GeneralSetting)(nil), // 16: memos.api.InstanceSetting.GeneralSetting + (*InstanceSetting_StorageSetting)(nil), // 17: memos.api.InstanceSetting.StorageSetting + (*InstanceSetting_Storage)(nil), // 18: memos.api.InstanceSetting.Storage + (*InstanceSetting_MemoRelatedSetting)(nil), // 19: memos.api.InstanceSetting.MemoRelatedSetting + (*InstanceSetting_TagsSetting)(nil), // 20: memos.api.InstanceSetting.TagsSetting + (*InstanceSetting_TagMetadata)(nil), // 21: memos.api.InstanceSetting.TagMetadata + (*InstanceSetting_NotificationSetting)(nil), // 22: memos.api.InstanceSetting.NotificationSetting + (*InstanceSetting_AISetting)(nil), // 23: memos.api.InstanceSetting.AISetting + (*InstanceSetting_AIProviderConfig)(nil), // 24: memos.api.InstanceSetting.AIProviderConfig + (*InstanceSetting_TranscriptionConfig)(nil), // 25: memos.api.InstanceSetting.TranscriptionConfig + (*InstanceSetting_AccessSetting)(nil), // 26: memos.api.InstanceSetting.AccessSetting + (*InstanceSetting_UsageLimitsSetting)(nil), // 27: memos.api.InstanceSetting.UsageLimitsSetting + (*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 28: memos.api.InstanceSetting.GeneralSetting.CustomProfile + (*InstanceSetting_StorageSetting_S3Config)(nil), // 29: memos.api.InstanceSetting.StorageSetting.S3Config + (*InstanceSetting_Storage_S3Config)(nil), // 30: memos.api.InstanceSetting.Storage.S3Config + nil, // 31: memos.api.InstanceSetting.TagsSetting.TagsEntry + (*InstanceSetting_NotificationSetting_EmailSetting)(nil), // 32: memos.api.InstanceSetting.NotificationSetting.EmailSetting + (*InstanceSetting_UsageLimitsSetting_UsageLimit)(nil), // 33: memos.api.InstanceSetting.UsageLimitsSetting.UsageLimit + (*InstanceStats_DatabaseStats)(nil), // 34: memos.api.InstanceStats.DatabaseStats + (*User)(nil), // 35: memos.api.User + (*fieldmaskpb.FieldMask)(nil), // 36: google.protobuf.FieldMask + (*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp + (*color.Color)(nil), // 38: google.type.Color + (*emptypb.Empty)(nil), // 39: google.protobuf.Empty } var file_api_instance_service_proto_depIdxs = []int32{ 0, // 0: memos.api.InstanceProfile.access_mode:type_name -> memos.api.InstanceAccessMode - 41, // 1: memos.api.InstanceProfile.admin:type_name -> memos.api.User - 23, // 2: memos.api.InstanceProfile.challenge:type_name -> memos.api.InstanceProfile.Challenge - 24, // 3: memos.api.InstanceSetting.general_setting:type_name -> memos.api.InstanceSetting.GeneralSetting - 25, // 4: memos.api.InstanceSetting.storage_setting:type_name -> memos.api.InstanceSetting.StorageSetting - 27, // 5: memos.api.InstanceSetting.memo_related_setting:type_name -> memos.api.InstanceSetting.MemoRelatedSetting - 28, // 6: memos.api.InstanceSetting.tags_setting:type_name -> memos.api.InstanceSetting.TagsSetting - 30, // 7: memos.api.InstanceSetting.notification_setting:type_name -> memos.api.InstanceSetting.NotificationSetting - 31, // 8: memos.api.InstanceSetting.ai_setting:type_name -> memos.api.InstanceSetting.AISetting - 34, // 9: memos.api.InstanceSetting.access_setting:type_name -> memos.api.InstanceSetting.AccessSetting - 8, // 10: memos.api.BatchGetInstanceSettingsResponse.instance_settings:type_name -> memos.api.InstanceSetting - 8, // 11: memos.api.UpdateInstanceSettingRequest.instance_setting:type_name -> memos.api.InstanceSetting - 42, // 12: memos.api.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask - 39, // 13: memos.api.TestInstanceEmailSettingRequest.email:type_name -> memos.api.InstanceSetting.NotificationSetting.EmailSetting - 40, // 14: memos.api.InstanceStats.database:type_name -> memos.api.InstanceStats.DatabaseStats - 43, // 15: memos.api.InstanceStats.generated_time:type_name -> google.protobuf.Timestamp - 5, // 16: memos.api.UsageLimit.metric:type_name -> memos.api.UsageLimit.Metric - 43, // 17: memos.api.UsageLimit.create_time:type_name -> google.protobuf.Timestamp - 43, // 18: memos.api.UsageLimit.update_time:type_name -> google.protobuf.Timestamp - 16, // 19: memos.api.ListUsageLimitsResponse.usage_limits:type_name -> memos.api.UsageLimit - 16, // 20: memos.api.CreateUsageLimitRequest.usage_limit:type_name -> memos.api.UsageLimit - 16, // 21: memos.api.UpdateUsageLimitRequest.usage_limit:type_name -> memos.api.UsageLimit - 42, // 22: memos.api.UpdateUsageLimitRequest.update_mask:type_name -> google.protobuf.FieldMask - 35, // 23: memos.api.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.InstanceSetting.GeneralSetting.CustomProfile - 26, // 24: memos.api.InstanceSetting.StorageSetting.storages:type_name -> memos.api.InstanceSetting.Storage - 4, // 25: memos.api.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.InstanceSetting.StorageSetting.StorageType - 36, // 26: memos.api.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.InstanceSetting.StorageSetting.S3Config - 2, // 27: memos.api.InstanceSetting.Storage.type:type_name -> memos.api.InstanceSetting.StorageType - 37, // 28: memos.api.InstanceSetting.Storage.s3_config:type_name -> memos.api.InstanceSetting.Storage.S3Config - 38, // 29: memos.api.InstanceSetting.TagsSetting.tags:type_name -> memos.api.InstanceSetting.TagsSetting.TagsEntry - 44, // 30: memos.api.InstanceSetting.TagMetadata.background_color:type_name -> google.type.Color - 39, // 31: memos.api.InstanceSetting.NotificationSetting.email:type_name -> memos.api.InstanceSetting.NotificationSetting.EmailSetting - 32, // 32: memos.api.InstanceSetting.AISetting.providers:type_name -> memos.api.InstanceSetting.AIProviderConfig - 33, // 33: memos.api.InstanceSetting.AISetting.transcription:type_name -> memos.api.InstanceSetting.TranscriptionConfig - 3, // 34: memos.api.InstanceSetting.AIProviderConfig.type:type_name -> memos.api.InstanceSetting.AIProviderType - 0, // 35: memos.api.InstanceSetting.AccessSetting.access_mode:type_name -> memos.api.InstanceAccessMode - 29, // 36: memos.api.InstanceSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.InstanceSetting.TagMetadata - 7, // 37: memos.api.InstanceService.GetInstanceProfile:input_type -> memos.api.GetInstanceProfileRequest - 9, // 38: memos.api.InstanceService.GetInstanceSetting:input_type -> memos.api.GetInstanceSettingRequest - 10, // 39: memos.api.InstanceService.BatchGetInstanceSettings:input_type -> memos.api.BatchGetInstanceSettingsRequest - 12, // 40: memos.api.InstanceService.UpdateInstanceSetting:input_type -> memos.api.UpdateInstanceSettingRequest - 13, // 41: memos.api.InstanceService.TestInstanceEmailSetting:input_type -> memos.api.TestInstanceEmailSettingRequest - 15, // 42: memos.api.InstanceService.GetInstanceStats:input_type -> memos.api.GetInstanceStatsRequest - 17, // 43: memos.api.InstanceService.ListUsageLimits:input_type -> memos.api.ListUsageLimitsRequest - 19, // 44: memos.api.InstanceService.GetUsageLimit:input_type -> memos.api.GetUsageLimitRequest - 20, // 45: memos.api.InstanceService.CreateUsageLimit:input_type -> memos.api.CreateUsageLimitRequest - 21, // 46: memos.api.InstanceService.UpdateUsageLimit:input_type -> memos.api.UpdateUsageLimitRequest - 22, // 47: memos.api.InstanceService.DeleteUsageLimit:input_type -> memos.api.DeleteUsageLimitRequest - 6, // 48: memos.api.InstanceService.GetInstanceProfile:output_type -> memos.api.InstanceProfile - 8, // 49: memos.api.InstanceService.GetInstanceSetting:output_type -> memos.api.InstanceSetting - 11, // 50: memos.api.InstanceService.BatchGetInstanceSettings:output_type -> memos.api.BatchGetInstanceSettingsResponse - 8, // 51: memos.api.InstanceService.UpdateInstanceSetting:output_type -> memos.api.InstanceSetting - 45, // 52: memos.api.InstanceService.TestInstanceEmailSetting:output_type -> google.protobuf.Empty - 14, // 53: memos.api.InstanceService.GetInstanceStats:output_type -> memos.api.InstanceStats - 18, // 54: memos.api.InstanceService.ListUsageLimits:output_type -> memos.api.ListUsageLimitsResponse - 16, // 55: memos.api.InstanceService.GetUsageLimit:output_type -> memos.api.UsageLimit - 16, // 56: memos.api.InstanceService.CreateUsageLimit:output_type -> memos.api.UsageLimit - 16, // 57: memos.api.InstanceService.UpdateUsageLimit:output_type -> memos.api.UsageLimit - 45, // 58: memos.api.InstanceService.DeleteUsageLimit:output_type -> google.protobuf.Empty - 48, // [48:59] is the sub-list for method output_type - 37, // [37:48] is the sub-list for method input_type - 37, // [37:37] is the sub-list for extension type_name - 37, // [37:37] is the sub-list for extension extendee - 0, // [0:37] is the sub-list for field type_name + 35, // 1: memos.api.InstanceProfile.admin:type_name -> memos.api.User + 15, // 2: memos.api.InstanceProfile.challenge:type_name -> memos.api.InstanceProfile.Challenge + 16, // 3: memos.api.InstanceSetting.general_setting:type_name -> memos.api.InstanceSetting.GeneralSetting + 17, // 4: memos.api.InstanceSetting.storage_setting:type_name -> memos.api.InstanceSetting.StorageSetting + 19, // 5: memos.api.InstanceSetting.memo_related_setting:type_name -> memos.api.InstanceSetting.MemoRelatedSetting + 20, // 6: memos.api.InstanceSetting.tags_setting:type_name -> memos.api.InstanceSetting.TagsSetting + 22, // 7: memos.api.InstanceSetting.notification_setting:type_name -> memos.api.InstanceSetting.NotificationSetting + 23, // 8: memos.api.InstanceSetting.ai_setting:type_name -> memos.api.InstanceSetting.AISetting + 26, // 9: memos.api.InstanceSetting.access_setting:type_name -> memos.api.InstanceSetting.AccessSetting + 27, // 10: memos.api.InstanceSetting.usage_limits_setting:type_name -> memos.api.InstanceSetting.UsageLimitsSetting + 7, // 11: memos.api.BatchGetInstanceSettingsResponse.instance_settings:type_name -> memos.api.InstanceSetting + 7, // 12: memos.api.UpdateInstanceSettingRequest.instance_setting:type_name -> memos.api.InstanceSetting + 36, // 13: memos.api.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask + 32, // 14: memos.api.TestInstanceEmailSettingRequest.email:type_name -> memos.api.InstanceSetting.NotificationSetting.EmailSetting + 34, // 15: memos.api.InstanceStats.database:type_name -> memos.api.InstanceStats.DatabaseStats + 37, // 16: memos.api.InstanceStats.generated_time:type_name -> google.protobuf.Timestamp + 28, // 17: memos.api.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.InstanceSetting.GeneralSetting.CustomProfile + 18, // 18: memos.api.InstanceSetting.StorageSetting.storages:type_name -> memos.api.InstanceSetting.Storage + 4, // 19: memos.api.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.InstanceSetting.StorageSetting.StorageType + 29, // 20: memos.api.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.InstanceSetting.StorageSetting.S3Config + 2, // 21: memos.api.InstanceSetting.Storage.type:type_name -> memos.api.InstanceSetting.StorageType + 30, // 22: memos.api.InstanceSetting.Storage.s3_config:type_name -> memos.api.InstanceSetting.Storage.S3Config + 31, // 23: memos.api.InstanceSetting.TagsSetting.tags:type_name -> memos.api.InstanceSetting.TagsSetting.TagsEntry + 38, // 24: memos.api.InstanceSetting.TagMetadata.background_color:type_name -> google.type.Color + 32, // 25: memos.api.InstanceSetting.NotificationSetting.email:type_name -> memos.api.InstanceSetting.NotificationSetting.EmailSetting + 24, // 26: memos.api.InstanceSetting.AISetting.providers:type_name -> memos.api.InstanceSetting.AIProviderConfig + 25, // 27: memos.api.InstanceSetting.AISetting.transcription:type_name -> memos.api.InstanceSetting.TranscriptionConfig + 3, // 28: memos.api.InstanceSetting.AIProviderConfig.type:type_name -> memos.api.InstanceSetting.AIProviderType + 0, // 29: memos.api.InstanceSetting.AccessSetting.access_mode:type_name -> memos.api.InstanceAccessMode + 33, // 30: memos.api.InstanceSetting.UsageLimitsSetting.limits:type_name -> memos.api.InstanceSetting.UsageLimitsSetting.UsageLimit + 21, // 31: memos.api.InstanceSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.InstanceSetting.TagMetadata + 6, // 32: memos.api.InstanceService.GetInstanceProfile:input_type -> memos.api.GetInstanceProfileRequest + 8, // 33: memos.api.InstanceService.GetInstanceSetting:input_type -> memos.api.GetInstanceSettingRequest + 9, // 34: memos.api.InstanceService.BatchGetInstanceSettings:input_type -> memos.api.BatchGetInstanceSettingsRequest + 11, // 35: memos.api.InstanceService.UpdateInstanceSetting:input_type -> memos.api.UpdateInstanceSettingRequest + 12, // 36: memos.api.InstanceService.TestInstanceEmailSetting:input_type -> memos.api.TestInstanceEmailSettingRequest + 14, // 37: memos.api.InstanceService.GetInstanceStats:input_type -> memos.api.GetInstanceStatsRequest + 5, // 38: memos.api.InstanceService.GetInstanceProfile:output_type -> memos.api.InstanceProfile + 7, // 39: memos.api.InstanceService.GetInstanceSetting:output_type -> memos.api.InstanceSetting + 10, // 40: memos.api.InstanceService.BatchGetInstanceSettings:output_type -> memos.api.BatchGetInstanceSettingsResponse + 7, // 41: memos.api.InstanceService.UpdateInstanceSetting:output_type -> memos.api.InstanceSetting + 39, // 42: memos.api.InstanceService.TestInstanceEmailSetting:output_type -> google.protobuf.Empty + 13, // 43: memos.api.InstanceService.GetInstanceStats:output_type -> memos.api.InstanceStats + 38, // [38:44] is the sub-list for method output_type + 32, // [32:38] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name } func init() { file_api_instance_service_proto_init() } @@ -3146,17 +2775,19 @@ func file_api_instance_service_proto_init() { (*InstanceSetting_NotificationSetting_)(nil), (*InstanceSetting_AiSetting)(nil), (*InstanceSetting_AccessSetting_)(nil), + (*InstanceSetting_UsageLimitsSetting_)(nil), } - file_api_instance_service_proto_msgTypes[20].OneofWrappers = []any{ + file_api_instance_service_proto_msgTypes[13].OneofWrappers = []any{ (*InstanceSetting_Storage_S3Config_)(nil), } + file_api_instance_service_proto_msgTypes[28].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_instance_service_proto_rawDesc), len(file_api_instance_service_proto_rawDesc)), - NumEnums: 6, - NumMessages: 35, + NumEnums: 5, + NumMessages: 30, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/gen/api/instance_service.pb.gw.go b/proto/gen/api/instance_service.pb.gw.go index 7db2273ba93e9..cdc9000b86669 100644 --- a/proto/gen/api/instance_service.pb.gw.go +++ b/proto/gen/api/instance_service.pb.gw.go @@ -251,241 +251,6 @@ func local_request_InstanceService_GetInstanceStats_0(ctx context.Context, marsh return msg, metadata, err } -var filter_InstanceService_ListUsageLimits_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} - -func request_InstanceService_ListUsageLimits_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq ListUsageLimitsRequest - metadata runtime.ServerMetadata - ) - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InstanceService_ListUsageLimits_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if req.Body != nil { - _, _ = io.Copy(io.Discard, req.Body) - } - msg, err := client.ListUsageLimits(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err -} - -func local_request_InstanceService_ListUsageLimits_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq ListUsageLimitsRequest - metadata runtime.ServerMetadata - ) - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InstanceService_ListUsageLimits_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - msg, err := server.ListUsageLimits(ctx, &protoReq) - return msg, metadata, err -} - -func request_InstanceService_GetUsageLimit_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq GetUsageLimitRequest - metadata runtime.ServerMetadata - err error - ) - val, ok := pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - if req.Body != nil { - _, _ = io.Copy(io.Discard, req.Body) - } - msg, err := client.GetUsageLimit(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err -} - -func local_request_InstanceService_GetUsageLimit_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq GetUsageLimitRequest - metadata runtime.ServerMetadata - err error - ) - val, ok := pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - msg, err := server.GetUsageLimit(ctx, &protoReq) - return msg, metadata, err -} - -var filter_InstanceService_CreateUsageLimit_0 = &utilities.DoubleArray{Encoding: map[string]int{"usage_limit": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} - -func request_InstanceService_CreateUsageLimit_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq CreateUsageLimitRequest - metadata runtime.ServerMetadata - ) - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.UsageLimit); err != nil && !errors.Is(err, io.EOF) { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InstanceService_CreateUsageLimit_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if req.Body != nil { - _, _ = io.Copy(io.Discard, req.Body) - } - msg, err := client.CreateUsageLimit(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err -} - -func local_request_InstanceService_CreateUsageLimit_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq CreateUsageLimitRequest - metadata runtime.ServerMetadata - ) - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.UsageLimit); err != nil && !errors.Is(err, io.EOF) { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InstanceService_CreateUsageLimit_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - msg, err := server.CreateUsageLimit(ctx, &protoReq) - return msg, metadata, err -} - -var filter_InstanceService_UpdateUsageLimit_0 = &utilities.DoubleArray{Encoding: map[string]int{"usage_limit": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} - -func request_InstanceService_UpdateUsageLimit_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq UpdateUsageLimitRequest - metadata runtime.ServerMetadata - err error - ) - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.UsageLimit); err != nil && !errors.Is(err, io.EOF) { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { - if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.UsageLimit); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } else { - protoReq.UpdateMask = fieldMask - } - } - val, ok := pathParams["usage_limit.name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "usage_limit.name") - } - err = runtime.PopulateFieldFromPath(&protoReq, "usage_limit.name", val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "usage_limit.name", err) - } - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InstanceService_UpdateUsageLimit_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if req.Body != nil { - _, _ = io.Copy(io.Discard, req.Body) - } - msg, err := client.UpdateUsageLimit(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err -} - -func local_request_InstanceService_UpdateUsageLimit_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq UpdateUsageLimitRequest - metadata runtime.ServerMetadata - err error - ) - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.UsageLimit); err != nil && !errors.Is(err, io.EOF) { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { - if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.UsageLimit); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } else { - protoReq.UpdateMask = fieldMask - } - } - val, ok := pathParams["usage_limit.name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "usage_limit.name") - } - err = runtime.PopulateFieldFromPath(&protoReq, "usage_limit.name", val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "usage_limit.name", err) - } - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InstanceService_UpdateUsageLimit_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - msg, err := server.UpdateUsageLimit(ctx, &protoReq) - return msg, metadata, err -} - -func request_InstanceService_DeleteUsageLimit_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq DeleteUsageLimitRequest - metadata runtime.ServerMetadata - err error - ) - val, ok := pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - if req.Body != nil { - _, _ = io.Copy(io.Discard, req.Body) - } - msg, err := client.DeleteUsageLimit(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err -} - -func local_request_InstanceService_DeleteUsageLimit_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var ( - protoReq DeleteUsageLimitRequest - metadata runtime.ServerMetadata - err error - ) - val, ok := pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - msg, err := server.DeleteUsageLimit(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. @@ -612,106 +377,6 @@ func RegisterInstanceServiceHandlerServer(ctx context.Context, mux *runtime.Serv } forward_InstanceService_GetInstanceStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle(http.MethodGet, pattern_InstanceService_ListUsageLimits_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.InstanceService/ListUsageLimits", runtime.WithHTTPPathPattern("/api/usageLimits")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_InstanceService_ListUsageLimits_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_ListUsageLimits_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) - mux.Handle(http.MethodGet, pattern_InstanceService_GetUsageLimit_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.InstanceService/GetUsageLimit", runtime.WithHTTPPathPattern("/api/{name=usageLimits/*}")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_InstanceService_GetUsageLimit_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_GetUsageLimit_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) - mux.Handle(http.MethodPost, pattern_InstanceService_CreateUsageLimit_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.InstanceService/CreateUsageLimit", runtime.WithHTTPPathPattern("/api/usageLimits")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_InstanceService_CreateUsageLimit_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_CreateUsageLimit_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) - mux.Handle(http.MethodPatch, pattern_InstanceService_UpdateUsageLimit_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.InstanceService/UpdateUsageLimit", runtime.WithHTTPPathPattern("/api/{usage_limit.name=usageLimits/*}")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_InstanceService_UpdateUsageLimit_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_UpdateUsageLimit_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) - mux.Handle(http.MethodDelete, pattern_InstanceService_DeleteUsageLimit_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.InstanceService/DeleteUsageLimit", runtime.WithHTTPPathPattern("/api/{name=usageLimits/*}")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_InstanceService_DeleteUsageLimit_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_DeleteUsageLimit_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) return nil } @@ -854,91 +519,6 @@ func RegisterInstanceServiceHandlerClient(ctx context.Context, mux *runtime.Serv } forward_InstanceService_GetInstanceStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle(http.MethodGet, pattern_InstanceService_ListUsageLimits_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.InstanceService/ListUsageLimits", runtime.WithHTTPPathPattern("/api/usageLimits")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_InstanceService_ListUsageLimits_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_ListUsageLimits_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) - mux.Handle(http.MethodGet, pattern_InstanceService_GetUsageLimit_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.InstanceService/GetUsageLimit", runtime.WithHTTPPathPattern("/api/{name=usageLimits/*}")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_InstanceService_GetUsageLimit_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_GetUsageLimit_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) - mux.Handle(http.MethodPost, pattern_InstanceService_CreateUsageLimit_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.InstanceService/CreateUsageLimit", runtime.WithHTTPPathPattern("/api/usageLimits")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_InstanceService_CreateUsageLimit_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_CreateUsageLimit_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) - mux.Handle(http.MethodPatch, pattern_InstanceService_UpdateUsageLimit_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.InstanceService/UpdateUsageLimit", runtime.WithHTTPPathPattern("/api/{usage_limit.name=usageLimits/*}")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_InstanceService_UpdateUsageLimit_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_UpdateUsageLimit_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) - mux.Handle(http.MethodDelete, pattern_InstanceService_DeleteUsageLimit_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.InstanceService/DeleteUsageLimit", runtime.WithHTTPPathPattern("/api/{name=usageLimits/*}")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_InstanceService_DeleteUsageLimit_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_DeleteUsageLimit_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - }) return nil } @@ -949,11 +529,6 @@ var ( pattern_InstanceService_UpdateInstanceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 3, 5, 3}, []string{"api", "instance", "settings", "instance_setting.name"}, "")) pattern_InstanceService_TestInstanceEmailSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "instance", "settings", "notification"}, "testEmail")) pattern_InstanceService_GetInstanceStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "instance", "stats"}, "")) - pattern_InstanceService_ListUsageLimits_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "usageLimits"}, "")) - pattern_InstanceService_GetUsageLimit_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 2, 5, 2}, []string{"api", "usageLimits", "name"}, "")) - pattern_InstanceService_CreateUsageLimit_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "usageLimits"}, "")) - pattern_InstanceService_UpdateUsageLimit_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 2, 5, 2}, []string{"api", "usageLimits", "usage_limit.name"}, "")) - pattern_InstanceService_DeleteUsageLimit_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 2, 5, 2}, []string{"api", "usageLimits", "name"}, "")) ) var ( @@ -963,9 +538,4 @@ var ( forward_InstanceService_UpdateInstanceSetting_0 = runtime.ForwardResponseMessage forward_InstanceService_TestInstanceEmailSetting_0 = runtime.ForwardResponseMessage forward_InstanceService_GetInstanceStats_0 = runtime.ForwardResponseMessage - forward_InstanceService_ListUsageLimits_0 = runtime.ForwardResponseMessage - forward_InstanceService_GetUsageLimit_0 = runtime.ForwardResponseMessage - forward_InstanceService_CreateUsageLimit_0 = runtime.ForwardResponseMessage - forward_InstanceService_UpdateUsageLimit_0 = runtime.ForwardResponseMessage - forward_InstanceService_DeleteUsageLimit_0 = runtime.ForwardResponseMessage ) diff --git a/proto/gen/api/instance_service_grpc.pb.go b/proto/gen/api/instance_service_grpc.pb.go index 619b5d527306c..7ed219eada3d2 100644 --- a/proto/gen/api/instance_service_grpc.pb.go +++ b/proto/gen/api/instance_service_grpc.pb.go @@ -26,11 +26,6 @@ const ( InstanceService_UpdateInstanceSetting_FullMethodName = "/memos.api.InstanceService/UpdateInstanceSetting" InstanceService_TestInstanceEmailSetting_FullMethodName = "/memos.api.InstanceService/TestInstanceEmailSetting" InstanceService_GetInstanceStats_FullMethodName = "/memos.api.InstanceService/GetInstanceStats" - InstanceService_ListUsageLimits_FullMethodName = "/memos.api.InstanceService/ListUsageLimits" - InstanceService_GetUsageLimit_FullMethodName = "/memos.api.InstanceService/GetUsageLimit" - InstanceService_CreateUsageLimit_FullMethodName = "/memos.api.InstanceService/CreateUsageLimit" - InstanceService_UpdateUsageLimit_FullMethodName = "/memos.api.InstanceService/UpdateUsageLimit" - InstanceService_DeleteUsageLimit_FullMethodName = "/memos.api.InstanceService/DeleteUsageLimit" ) // InstanceServiceClient is the client API for InstanceService service. @@ -54,22 +49,6 @@ type InstanceServiceClient interface { TestInstanceEmailSetting(ctx context.Context, in *TestInstanceEmailSettingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // GetInstanceStats gets resource usage statistics for the instance. Admin only. Results are cached for 60 seconds. GetInstanceStats(ctx context.Context, in *GetInstanceStatsRequest, opts ...grpc.CallOption) (*InstanceStats, error) - // ListUsageLimits lists the usage limits: the defaults for every user and the - // limits for single users. Admin only. - ListUsageLimits(ctx context.Context, in *ListUsageLimitsRequest, opts ...grpc.CallOption) (*ListUsageLimitsResponse, error) - // GetUsageLimit gets a usage limit. Admin only. - GetUsageLimit(ctx context.Context, in *GetUsageLimitRequest, opts ...grpc.CallOption) (*UsageLimit, error) - // CreateUsageLimit creates a usage limit. Admin only. There is at most one - // limit per metric and user, and one default per metric. Fails with - // FAILED_PRECONDITION when the usage limits are configured by the deployment. - CreateUsageLimit(ctx context.Context, in *CreateUsageLimitRequest, opts ...grpc.CallOption) (*UsageLimit, error) - // UpdateUsageLimit updates the value of a usage limit. Admin only. The metric - // and user cannot change. - UpdateUsageLimit(ctx context.Context, in *UpdateUsageLimitRequest, opts ...grpc.CallOption) (*UsageLimit, error) - // DeleteUsageLimit deletes a usage limit. Admin only. Deleting a user's limit - // returns the user to the default; deleting a default makes the metric - // unlimited for users without their own limit. - DeleteUsageLimit(ctx context.Context, in *DeleteUsageLimitRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } type instanceServiceClient struct { @@ -140,56 +119,6 @@ func (c *instanceServiceClient) GetInstanceStats(ctx context.Context, in *GetIns return out, nil } -func (c *instanceServiceClient) ListUsageLimits(ctx context.Context, in *ListUsageLimitsRequest, opts ...grpc.CallOption) (*ListUsageLimitsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ListUsageLimitsResponse) - err := c.cc.Invoke(ctx, InstanceService_ListUsageLimits_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *instanceServiceClient) GetUsageLimit(ctx context.Context, in *GetUsageLimitRequest, opts ...grpc.CallOption) (*UsageLimit, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(UsageLimit) - err := c.cc.Invoke(ctx, InstanceService_GetUsageLimit_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *instanceServiceClient) CreateUsageLimit(ctx context.Context, in *CreateUsageLimitRequest, opts ...grpc.CallOption) (*UsageLimit, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(UsageLimit) - err := c.cc.Invoke(ctx, InstanceService_CreateUsageLimit_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *instanceServiceClient) UpdateUsageLimit(ctx context.Context, in *UpdateUsageLimitRequest, opts ...grpc.CallOption) (*UsageLimit, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(UsageLimit) - err := c.cc.Invoke(ctx, InstanceService_UpdateUsageLimit_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *instanceServiceClient) DeleteUsageLimit(ctx context.Context, in *DeleteUsageLimitRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, InstanceService_DeleteUsageLimit_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. @@ -211,22 +140,6 @@ type InstanceServiceServer interface { TestInstanceEmailSetting(context.Context, *TestInstanceEmailSettingRequest) (*emptypb.Empty, error) // GetInstanceStats gets resource usage statistics for the instance. Admin only. Results are cached for 60 seconds. GetInstanceStats(context.Context, *GetInstanceStatsRequest) (*InstanceStats, error) - // ListUsageLimits lists the usage limits: the defaults for every user and the - // limits for single users. Admin only. - ListUsageLimits(context.Context, *ListUsageLimitsRequest) (*ListUsageLimitsResponse, error) - // GetUsageLimit gets a usage limit. Admin only. - GetUsageLimit(context.Context, *GetUsageLimitRequest) (*UsageLimit, error) - // CreateUsageLimit creates a usage limit. Admin only. There is at most one - // limit per metric and user, and one default per metric. Fails with - // FAILED_PRECONDITION when the usage limits are configured by the deployment. - CreateUsageLimit(context.Context, *CreateUsageLimitRequest) (*UsageLimit, error) - // UpdateUsageLimit updates the value of a usage limit. Admin only. The metric - // and user cannot change. - UpdateUsageLimit(context.Context, *UpdateUsageLimitRequest) (*UsageLimit, error) - // DeleteUsageLimit deletes a usage limit. Admin only. Deleting a user's limit - // returns the user to the default; deleting a default makes the metric - // unlimited for users without their own limit. - DeleteUsageLimit(context.Context, *DeleteUsageLimitRequest) (*emptypb.Empty, error) mustEmbedUnimplementedInstanceServiceServer() } @@ -255,21 +168,6 @@ func (UnimplementedInstanceServiceServer) TestInstanceEmailSetting(context.Conte func (UnimplementedInstanceServiceServer) GetInstanceStats(context.Context, *GetInstanceStatsRequest) (*InstanceStats, error) { return nil, status.Error(codes.Unimplemented, "method GetInstanceStats not implemented") } -func (UnimplementedInstanceServiceServer) ListUsageLimits(context.Context, *ListUsageLimitsRequest) (*ListUsageLimitsResponse, error) { - return nil, status.Error(codes.Unimplemented, "method ListUsageLimits not implemented") -} -func (UnimplementedInstanceServiceServer) GetUsageLimit(context.Context, *GetUsageLimitRequest) (*UsageLimit, error) { - return nil, status.Error(codes.Unimplemented, "method GetUsageLimit not implemented") -} -func (UnimplementedInstanceServiceServer) CreateUsageLimit(context.Context, *CreateUsageLimitRequest) (*UsageLimit, error) { - return nil, status.Error(codes.Unimplemented, "method CreateUsageLimit not implemented") -} -func (UnimplementedInstanceServiceServer) UpdateUsageLimit(context.Context, *UpdateUsageLimitRequest) (*UsageLimit, error) { - return nil, status.Error(codes.Unimplemented, "method UpdateUsageLimit not implemented") -} -func (UnimplementedInstanceServiceServer) DeleteUsageLimit(context.Context, *DeleteUsageLimitRequest) (*emptypb.Empty, error) { - return nil, status.Error(codes.Unimplemented, "method DeleteUsageLimit not implemented") -} func (UnimplementedInstanceServiceServer) mustEmbedUnimplementedInstanceServiceServer() {} func (UnimplementedInstanceServiceServer) testEmbeddedByValue() {} @@ -399,96 +297,6 @@ func _InstanceService_GetInstanceStats_Handler(srv interface{}, ctx context.Cont return interceptor(ctx, in, info, handler) } -func _InstanceService_ListUsageLimits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ListUsageLimitsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InstanceServiceServer).ListUsageLimits(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: InstanceService_ListUsageLimits_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InstanceServiceServer).ListUsageLimits(ctx, req.(*ListUsageLimitsRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _InstanceService_GetUsageLimit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetUsageLimitRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InstanceServiceServer).GetUsageLimit(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: InstanceService_GetUsageLimit_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InstanceServiceServer).GetUsageLimit(ctx, req.(*GetUsageLimitRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _InstanceService_CreateUsageLimit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CreateUsageLimitRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InstanceServiceServer).CreateUsageLimit(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: InstanceService_CreateUsageLimit_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InstanceServiceServer).CreateUsageLimit(ctx, req.(*CreateUsageLimitRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _InstanceService_UpdateUsageLimit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UpdateUsageLimitRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InstanceServiceServer).UpdateUsageLimit(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: InstanceService_UpdateUsageLimit_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InstanceServiceServer).UpdateUsageLimit(ctx, req.(*UpdateUsageLimitRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _InstanceService_DeleteUsageLimit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DeleteUsageLimitRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InstanceServiceServer).DeleteUsageLimit(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: InstanceService_DeleteUsageLimit_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InstanceServiceServer).DeleteUsageLimit(ctx, req.(*DeleteUsageLimitRequest)) - } - 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) @@ -520,26 +328,6 @@ var InstanceService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetInstanceStats", Handler: _InstanceService_GetInstanceStats_Handler, }, - { - MethodName: "ListUsageLimits", - Handler: _InstanceService_ListUsageLimits_Handler, - }, - { - MethodName: "GetUsageLimit", - Handler: _InstanceService_GetUsageLimit_Handler, - }, - { - MethodName: "CreateUsageLimit", - Handler: _InstanceService_CreateUsageLimit_Handler, - }, - { - MethodName: "UpdateUsageLimit", - Handler: _InstanceService_UpdateUsageLimit_Handler, - }, - { - MethodName: "DeleteUsageLimit", - Handler: _InstanceService_DeleteUsageLimit_Handler, - }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/instance_service.proto", diff --git a/proto/gen/api/user_service.pb.go b/proto/gen/api/user_service.pb.go index b5d51b07ffddc..a7d82b2c9d8ba 100644 --- a/proto/gen/api/user_service.pb.go +++ b/proto/gen/api/user_service.pb.go @@ -1603,7 +1603,7 @@ func (x *GetUserStatsRequest) GetFilter() string { return "" } -// UserUsage is a user's usage and the usage limits in effect for the user. +// UserUsage is a user's usage and the limit set in effect for the user. type UserUsage struct { state protoimpl.MessageState `protogen:"open.v1"` // Identifier. The resource name of the user usage singleton. @@ -1615,16 +1615,14 @@ type UserUsage struct { // The total size in bytes of the user's uploads, including uploads not // attached to a memo. External links do not count. StorageBytes int64 `protobuf:"varint,3,opt,name=storage_bytes,json=storageBytes,proto3" json:"storage_bytes,omitempty"` - // The limit on memo_count. - MemoCountLimit *UserUsage_EffectiveLimit `protobuf:"bytes,4,opt,name=memo_count_limit,json=memoCountLimit,proto3" json:"memo_count_limit,omitempty"` - // The limit on storage_bytes. - StorageBytesLimit *UserUsage_EffectiveLimit `protobuf:"bytes,5,opt,name=storage_bytes_limit,json=storageBytesLimit,proto3" json:"storage_bytes_limit,omitempty"` - // The limit on the size of one uploaded file. - UploadBytesLimit *UserUsage_EffectiveLimit `protobuf:"bytes,6,opt,name=upload_bytes_limit,json=uploadBytesLimit,proto3" json:"upload_bytes_limit,omitempty"` - // The limit on the UTF-8 size in bytes of one memo's content. - MemoContentBytesLimit *UserUsage_EffectiveLimit `protobuf:"bytes,7,opt,name=memo_content_bytes_limit,json=memoContentBytesLimit,proto3" json:"memo_content_bytes_limit,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The limit set in effect: the first USAGE_LIMITS entry that names the + // user, or the default. An unset limit is unlimited. + Limits *UserUsage_Limits `protobuf:"bytes,4,opt,name=limits,proto3" json:"limits,omitempty"` + // The position of the entry in effect in the USAGE_LIMITS setting, starting + // at 0. The default is the last entry. + LimitIndex int32 `protobuf:"varint,5,opt,name=limit_index,json=limitIndex,proto3" json:"limit_index,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UserUsage) Reset() { @@ -1678,32 +1676,18 @@ func (x *UserUsage) GetStorageBytes() int64 { return 0 } -func (x *UserUsage) GetMemoCountLimit() *UserUsage_EffectiveLimit { - if x != nil { - return x.MemoCountLimit - } - return nil -} - -func (x *UserUsage) GetStorageBytesLimit() *UserUsage_EffectiveLimit { +func (x *UserUsage) GetLimits() *UserUsage_Limits { if x != nil { - return x.StorageBytesLimit + return x.Limits } return nil } -func (x *UserUsage) GetUploadBytesLimit() *UserUsage_EffectiveLimit { +func (x *UserUsage) GetLimitIndex() int32 { if x != nil { - return x.UploadBytesLimit + return x.LimitIndex } - return nil -} - -func (x *UserUsage) GetMemoContentBytesLimit() *UserUsage_EffectiveLimit { - if x != nil { - return x.MemoContentBytesLimit - } - return nil + return 0 } // Request message for UserService.GetUserUsage. @@ -4615,34 +4599,35 @@ func (x *UserStats_MemoTypeStats) GetUndoCount() int32 { return 0 } -// EffectiveLimit is the limit in effect for one metric, and where it comes -// from. -type UserUsage_EffectiveLimit struct { +// Limits is the limit for each metric. An unset limit is unlimited, as is 0. +type UserUsage_Limits struct { state protoimpl.MessageState `protogen:"open.v1"` - // The limit. 0 means unlimited. - Value int64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` - // The usage limit the value comes from: the user's own limit, or else the - // default. Empty when no usage limit applies. - // Format: usageLimits/{usage_limit} - UsageLimit string `protobuf:"bytes,2,opt,name=usage_limit,json=usageLimit,proto3" json:"usage_limit,omitempty"` + // The number of memos the user may have created. + MemoCount *int64 `protobuf:"varint,1,opt,name=memo_count,json=memoCount,proto3,oneof" json:"memo_count,omitempty"` + // The UTF-8 size in bytes of one memo's content. + MemoContentBytes *int64 `protobuf:"varint,2,opt,name=memo_content_bytes,json=memoContentBytes,proto3,oneof" json:"memo_content_bytes,omitempty"` + // The total size in bytes of the user's uploads. + StorageBytes *int64 `protobuf:"varint,3,opt,name=storage_bytes,json=storageBytes,proto3,oneof" json:"storage_bytes,omitempty"` + // The size in bytes of one uploaded file. + UploadBytes *int64 `protobuf:"varint,4,opt,name=upload_bytes,json=uploadBytes,proto3,oneof" json:"upload_bytes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *UserUsage_EffectiveLimit) Reset() { - *x = UserUsage_EffectiveLimit{} +func (x *UserUsage_Limits) Reset() { + *x = UserUsage_Limits{} mi := &file_api_user_service_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *UserUsage_EffectiveLimit) String() string { +func (x *UserUsage_Limits) String() string { return protoimpl.X.MessageStringOf(x) } -func (*UserUsage_EffectiveLimit) ProtoMessage() {} +func (*UserUsage_Limits) ProtoMessage() {} -func (x *UserUsage_EffectiveLimit) ProtoReflect() protoreflect.Message { +func (x *UserUsage_Limits) ProtoReflect() protoreflect.Message { mi := &file_api_user_service_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -4654,23 +4639,37 @@ func (x *UserUsage_EffectiveLimit) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use UserUsage_EffectiveLimit.ProtoReflect.Descriptor instead. -func (*UserUsage_EffectiveLimit) Descriptor() ([]byte, []int) { +// Deprecated: Use UserUsage_Limits.ProtoReflect.Descriptor instead. +func (*UserUsage_Limits) Descriptor() ([]byte, []int) { return file_api_user_service_proto_rawDescGZIP(), []int{18, 0} } -func (x *UserUsage_EffectiveLimit) GetValue() int64 { - if x != nil { - return x.Value +func (x *UserUsage_Limits) GetMemoCount() int64 { + if x != nil && x.MemoCount != nil { + return *x.MemoCount } return 0 } -func (x *UserUsage_EffectiveLimit) GetUsageLimit() string { - if x != nil { - return x.UsageLimit +func (x *UserUsage_Limits) GetMemoContentBytes() int64 { + if x != nil && x.MemoContentBytes != nil { + return *x.MemoContentBytes } - return "" + return 0 +} + +func (x *UserUsage_Limits) GetStorageBytes() int64 { + if x != nil && x.StorageBytes != nil { + return *x.StorageBytes + } + return 0 +} + +func (x *UserUsage_Limits) GetUploadBytes() int64 { + if x != nil && x.UploadBytes != nil { + return *x.UploadBytes + } + return 0 } // Icon is the display icon of a memo view. @@ -5122,21 +5121,25 @@ const file_api_user_service_proto_rawDesc = "" + "\x13GetUserStatsRequest\x12*\n" + "\x04name\x18\x01 \x01(\tB\x16\xe0A\x02\xfaA\x10\n" + "\x0ememos.api/UserR\x04name\x12\x1b\n" + - "\x06filter\x18\x02 \x01(\tB\x03\xe0A\x01R\x06filter\"\xe6\x04\n" + + "\x06filter\x18\x02 \x01(\tB\x03\xe0A\x01R\x06filter\"\x80\x04\n" + "\tUserUsage\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x1d\n" + "\n" + "memo_count\x18\x02 \x01(\x03R\tmemoCount\x12#\n" + - "\rstorage_bytes\x18\x03 \x01(\x03R\fstorageBytes\x12M\n" + - "\x10memo_count_limit\x18\x04 \x01(\v2#.memos.api.UserUsage.EffectiveLimitR\x0ememoCountLimit\x12S\n" + - "\x13storage_bytes_limit\x18\x05 \x01(\v2#.memos.api.UserUsage.EffectiveLimitR\x11storageBytesLimit\x12Q\n" + - "\x12upload_bytes_limit\x18\x06 \x01(\v2#.memos.api.UserUsage.EffectiveLimitR\x10uploadBytesLimit\x12\\\n" + - "\x18memo_content_bytes_limit\x18\a \x01(\v2#.memos.api.UserUsage.EffectiveLimitR\x15memoContentBytesLimit\x1ab\n" + - "\x0eEffectiveLimit\x12\x14\n" + - "\x05value\x18\x01 \x01(\x03R\x05value\x12:\n" + - "\vusage_limit\x18\x02 \x01(\tB\x19\xfaA\x16\n" + - "\x14memos.api/UsageLimitR\n" + - "usageLimit:C\xeaA@\n" + + "\rstorage_bytes\x18\x03 \x01(\x03R\fstorageBytes\x123\n" + + "\x06limits\x18\x04 \x01(\v2\x1b.memos.api.UserUsage.LimitsR\x06limits\x12\x1f\n" + + "\vlimit_index\x18\x05 \x01(\x05R\n" + + "limitIndex\x1a\xfa\x01\n" + + "\x06Limits\x12\"\n" + + "\n" + + "memo_count\x18\x01 \x01(\x03H\x00R\tmemoCount\x88\x01\x01\x121\n" + + "\x12memo_content_bytes\x18\x02 \x01(\x03H\x01R\x10memoContentBytes\x88\x01\x01\x12(\n" + + "\rstorage_bytes\x18\x03 \x01(\x03H\x02R\fstorageBytes\x88\x01\x01\x12&\n" + + "\fupload_bytes\x18\x04 \x01(\x03H\x03R\vuploadBytes\x88\x01\x01B\r\n" + + "\v_memo_countB\x15\n" + + "\x13_memo_content_bytesB\x10\n" + + "\x0e_storage_bytesB\x0f\n" + + "\r_upload_bytes:C\xeaA@\n" + "\x13memos.api/UserUsage\x12\x12users/{user}/usage*\n" + "userUsages2\tuserUsage\"F\n" + "\x13GetUserUsageRequest\x12/\n" + @@ -5498,7 +5501,7 @@ var file_api_user_service_proto_goTypes = []any{ nil, // 69: memos.api.UserSetting.TagsSetting.TagsEntry nil, // 70: memos.api.UserStats.TagCountsEntry (*UserStats_MemoTypeStats)(nil), // 71: memos.api.UserStats.MemoTypeStats - (*UserUsage_EffectiveLimit)(nil), // 72: memos.api.UserUsage.EffectiveLimit + (*UserUsage_Limits)(nil), // 72: memos.api.UserUsage.Limits (*MemoView_Icon)(nil), // 73: memos.api.MemoView.Icon (*UserNotification_MemoCommentPayload)(nil), // 74: memos.api.UserNotification.MemoCommentPayload (*UserNotification_MemoMentionPayload)(nil), // 75: memos.api.UserNotification.MemoMentionPayload @@ -5513,147 +5516,144 @@ var file_api_user_service_proto_goTypes = []any{ (*httpbody.HttpBody)(nil), // 84: google.api.HttpBody } var file_api_user_service_proto_depIdxs = []int32{ - 0, // 0: memos.api.User.role:type_name -> memos.api.User.Role - 77, // 1: memos.api.User.state:type_name -> memos.api.State - 78, // 2: memos.api.User.create_time:type_name -> google.protobuf.Timestamp - 78, // 3: memos.api.User.update_time:type_name -> google.protobuf.Timestamp - 6, // 4: memos.api.ListUsersResponse.users:type_name -> memos.api.User - 79, // 5: memos.api.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask - 6, // 6: memos.api.BatchGetUsersResponse.users:type_name -> memos.api.User - 6, // 7: memos.api.CreateUserRequest.user:type_name -> memos.api.User - 6, // 8: memos.api.UpdateUserRequest.user:type_name -> memos.api.User - 79, // 9: memos.api.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask - 65, // 10: memos.api.UserSetting.general_setting:type_name -> memos.api.UserSetting.GeneralSetting - 66, // 11: memos.api.UserSetting.webhooks_setting:type_name -> memos.api.UserSetting.WebhooksSetting - 67, // 12: memos.api.UserSetting.tags_setting:type_name -> memos.api.UserSetting.TagsSetting - 15, // 13: memos.api.ListUserSettingsResponse.user_settings:type_name -> memos.api.UserSetting - 15, // 14: memos.api.UpdateUserSettingRequest.user_setting:type_name -> memos.api.UserSetting - 79, // 15: memos.api.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask - 71, // 16: memos.api.UserStats.memo_type_stats:type_name -> memos.api.UserStats.MemoTypeStats - 70, // 17: memos.api.UserStats.tag_counts:type_name -> memos.api.UserStats.TagCountsEntry - 78, // 18: memos.api.UserStats.memo_created_timestamps:type_name -> google.protobuf.Timestamp - 78, // 19: memos.api.UserStats.memo_updated_timestamps:type_name -> google.protobuf.Timestamp - 77, // 20: memos.api.ListUserStatsRequest.state:type_name -> memos.api.State - 20, // 21: memos.api.ListUserStatsResponse.user_stats:type_name -> memos.api.UserStats - 72, // 22: memos.api.UserUsage.memo_count_limit:type_name -> memos.api.UserUsage.EffectiveLimit - 72, // 23: memos.api.UserUsage.storage_bytes_limit:type_name -> memos.api.UserUsage.EffectiveLimit - 72, // 24: memos.api.UserUsage.upload_bytes_limit:type_name -> memos.api.UserUsage.EffectiveLimit - 72, // 25: memos.api.UserUsage.memo_content_bytes_limit:type_name -> memos.api.UserUsage.EffectiveLimit - 73, // 26: memos.api.MemoView.icon:type_name -> memos.api.MemoView.Icon - 26, // 27: memos.api.ListMemoViewsResponse.memo_views:type_name -> memos.api.MemoView - 26, // 28: memos.api.CreateMemoViewRequest.memo_view:type_name -> memos.api.MemoView - 26, // 29: memos.api.UpdateMemoViewRequest.memo_view:type_name -> memos.api.MemoView - 79, // 30: memos.api.UpdateMemoViewRequest.update_mask:type_name -> google.protobuf.FieldMask - 6, // 31: memos.api.UserNotification.sender_user:type_name -> memos.api.User - 3, // 32: memos.api.UserNotification.type:type_name -> memos.api.UserNotification.Type - 74, // 33: memos.api.UserNotification.memo_comment:type_name -> memos.api.UserNotification.MemoCommentPayload - 75, // 34: memos.api.UserNotification.memo_mention:type_name -> memos.api.UserNotification.MemoMentionPayload - 76, // 35: memos.api.UserNotification.space_invitation:type_name -> memos.api.UserNotification.SpaceInvitationPayload - 2, // 36: memos.api.UserNotification.status:type_name -> memos.api.UserNotification.Status - 78, // 37: memos.api.UserNotification.create_time:type_name -> google.protobuf.Timestamp - 33, // 38: memos.api.ListUserNotificationsResponse.notifications:type_name -> memos.api.UserNotification - 33, // 39: memos.api.UpdateUserNotificationRequest.notification:type_name -> memos.api.UserNotification - 79, // 40: memos.api.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask - 78, // 41: memos.api.UserWebhook.create_time:type_name -> google.protobuf.Timestamp - 78, // 42: memos.api.UserWebhook.update_time:type_name -> google.protobuf.Timestamp - 38, // 43: memos.api.ListUserWebhooksResponse.webhooks:type_name -> memos.api.UserWebhook - 38, // 44: memos.api.CreateUserWebhookRequest.webhook:type_name -> memos.api.UserWebhook - 38, // 45: memos.api.UpdateUserWebhookRequest.webhook:type_name -> memos.api.UserWebhook - 79, // 46: memos.api.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask - 46, // 47: memos.api.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.LinkedIdentity - 78, // 48: memos.api.PersonalAccessToken.create_time:type_name -> google.protobuf.Timestamp - 78, // 49: memos.api.PersonalAccessToken.expire_time:type_name -> google.protobuf.Timestamp - 78, // 50: memos.api.PersonalAccessToken.last_use_time:type_name -> google.protobuf.Timestamp - 52, // 51: memos.api.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.PersonalAccessToken - 52, // 52: memos.api.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.PersonalAccessToken - 61, // 53: memos.api.ImportMemosRequest.spec:type_name -> memos.api.ImportMemosSpec - 5, // 54: memos.api.ImportMemosRequest.conflict_policy:type_name -> memos.api.ImportMemosRequest.ConflictPolicy - 62, // 55: memos.api.ImportMemosResponse.plan:type_name -> memos.api.ImportMemosPlan - 63, // 56: memos.api.ImportMemosResponse.report:type_name -> memos.api.ImportMemosReport - 78, // 57: memos.api.ImportMemosPlan.export_time:type_name -> google.protobuf.Timestamp - 64, // 58: memos.api.ImportMemosPlan.warnings:type_name -> memos.api.ImportMemosIssue - 64, // 59: memos.api.ImportMemosReport.warnings:type_name -> memos.api.ImportMemosIssue - 64, // 60: memos.api.ImportMemosReport.failures:type_name -> memos.api.ImportMemosIssue - 38, // 61: memos.api.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.UserWebhook - 69, // 62: memos.api.UserSetting.TagsSetting.tags:type_name -> memos.api.UserSetting.TagsSetting.TagsEntry - 80, // 63: memos.api.UserSetting.TagMetadata.background_color:type_name -> google.type.Color - 68, // 64: memos.api.UserSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.UserSetting.TagMetadata - 81, // 65: memos.api.UserNotification.SpaceInvitationPayload.space:type_name -> memos.api.Space - 82, // 66: memos.api.UserNotification.SpaceInvitationPayload.role:type_name -> memos.api.SpaceMember.Role - 4, // 67: memos.api.UserNotification.SpaceInvitationPayload.state:type_name -> memos.api.UserNotification.SpaceInvitationPayload.InvitationState - 7, // 68: memos.api.UserService.ListUsers:input_type -> memos.api.ListUsersRequest - 9, // 69: memos.api.UserService.GetUser:input_type -> memos.api.GetUserRequest - 10, // 70: memos.api.UserService.BatchGetUsers:input_type -> memos.api.BatchGetUsersRequest - 12, // 71: memos.api.UserService.CreateUser:input_type -> memos.api.CreateUserRequest - 13, // 72: memos.api.UserService.UpdateUser:input_type -> memos.api.UpdateUserRequest - 14, // 73: memos.api.UserService.DeleteUser:input_type -> memos.api.DeleteUserRequest - 16, // 74: memos.api.UserService.ListUserSettings:input_type -> memos.api.ListUserSettingsRequest - 18, // 75: memos.api.UserService.GetUserSetting:input_type -> memos.api.GetUserSettingRequest - 19, // 76: memos.api.UserService.UpdateUserSetting:input_type -> memos.api.UpdateUserSettingRequest - 21, // 77: memos.api.UserService.ListUserStats:input_type -> memos.api.ListUserStatsRequest - 23, // 78: memos.api.UserService.GetUserStats:input_type -> memos.api.GetUserStatsRequest - 25, // 79: memos.api.UserService.GetUserUsage:input_type -> memos.api.GetUserUsageRequest - 27, // 80: memos.api.UserService.ListMemoViews:input_type -> memos.api.ListMemoViewsRequest - 29, // 81: memos.api.UserService.GetMemoView:input_type -> memos.api.GetMemoViewRequest - 30, // 82: memos.api.UserService.CreateMemoView:input_type -> memos.api.CreateMemoViewRequest - 31, // 83: memos.api.UserService.UpdateMemoView:input_type -> memos.api.UpdateMemoViewRequest - 32, // 84: memos.api.UserService.DeleteMemoView:input_type -> memos.api.DeleteMemoViewRequest - 34, // 85: memos.api.UserService.ListUserNotifications:input_type -> memos.api.ListUserNotificationsRequest - 36, // 86: memos.api.UserService.UpdateUserNotification:input_type -> memos.api.UpdateUserNotificationRequest - 37, // 87: memos.api.UserService.DeleteUserNotification:input_type -> memos.api.DeleteUserNotificationRequest - 39, // 88: memos.api.UserService.ListUserWebhooks:input_type -> memos.api.ListUserWebhooksRequest - 41, // 89: memos.api.UserService.CreateUserWebhook:input_type -> memos.api.CreateUserWebhookRequest - 42, // 90: memos.api.UserService.UpdateUserWebhook:input_type -> memos.api.UpdateUserWebhookRequest - 43, // 91: memos.api.UserService.DeleteUserWebhook:input_type -> memos.api.DeleteUserWebhookRequest - 44, // 92: memos.api.UserService.GetUserWebhookSigningSecret:input_type -> memos.api.GetUserWebhookSigningSecretRequest - 47, // 93: memos.api.UserService.ListLinkedIdentities:input_type -> memos.api.ListLinkedIdentitiesRequest - 49, // 94: memos.api.UserService.GetLinkedIdentity:input_type -> memos.api.GetLinkedIdentityRequest - 50, // 95: memos.api.UserService.CreateLinkedIdentity:input_type -> memos.api.CreateLinkedIdentityRequest - 51, // 96: memos.api.UserService.DeleteLinkedIdentity:input_type -> memos.api.DeleteLinkedIdentityRequest - 53, // 97: memos.api.UserService.ListPersonalAccessTokens:input_type -> memos.api.ListPersonalAccessTokensRequest - 55, // 98: memos.api.UserService.CreatePersonalAccessToken:input_type -> memos.api.CreatePersonalAccessTokenRequest - 57, // 99: memos.api.UserService.DeletePersonalAccessToken:input_type -> memos.api.DeletePersonalAccessTokenRequest - 58, // 100: memos.api.UserService.ExportMemos:input_type -> memos.api.ExportMemosRequest - 59, // 101: memos.api.UserService.ImportMemos:input_type -> memos.api.ImportMemosRequest - 8, // 102: memos.api.UserService.ListUsers:output_type -> memos.api.ListUsersResponse - 6, // 103: memos.api.UserService.GetUser:output_type -> memos.api.User - 11, // 104: memos.api.UserService.BatchGetUsers:output_type -> memos.api.BatchGetUsersResponse - 6, // 105: memos.api.UserService.CreateUser:output_type -> memos.api.User - 6, // 106: memos.api.UserService.UpdateUser:output_type -> memos.api.User - 83, // 107: memos.api.UserService.DeleteUser:output_type -> google.protobuf.Empty - 17, // 108: memos.api.UserService.ListUserSettings:output_type -> memos.api.ListUserSettingsResponse - 15, // 109: memos.api.UserService.GetUserSetting:output_type -> memos.api.UserSetting - 15, // 110: memos.api.UserService.UpdateUserSetting:output_type -> memos.api.UserSetting - 22, // 111: memos.api.UserService.ListUserStats:output_type -> memos.api.ListUserStatsResponse - 20, // 112: memos.api.UserService.GetUserStats:output_type -> memos.api.UserStats - 24, // 113: memos.api.UserService.GetUserUsage:output_type -> memos.api.UserUsage - 28, // 114: memos.api.UserService.ListMemoViews:output_type -> memos.api.ListMemoViewsResponse - 26, // 115: memos.api.UserService.GetMemoView:output_type -> memos.api.MemoView - 26, // 116: memos.api.UserService.CreateMemoView:output_type -> memos.api.MemoView - 26, // 117: memos.api.UserService.UpdateMemoView:output_type -> memos.api.MemoView - 83, // 118: memos.api.UserService.DeleteMemoView:output_type -> google.protobuf.Empty - 35, // 119: memos.api.UserService.ListUserNotifications:output_type -> memos.api.ListUserNotificationsResponse - 33, // 120: memos.api.UserService.UpdateUserNotification:output_type -> memos.api.UserNotification - 83, // 121: memos.api.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty - 40, // 122: memos.api.UserService.ListUserWebhooks:output_type -> memos.api.ListUserWebhooksResponse - 38, // 123: memos.api.UserService.CreateUserWebhook:output_type -> memos.api.UserWebhook - 38, // 124: memos.api.UserService.UpdateUserWebhook:output_type -> memos.api.UserWebhook - 83, // 125: memos.api.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty - 45, // 126: memos.api.UserService.GetUserWebhookSigningSecret:output_type -> memos.api.GetUserWebhookSigningSecretResponse - 48, // 127: memos.api.UserService.ListLinkedIdentities:output_type -> memos.api.ListLinkedIdentitiesResponse - 46, // 128: memos.api.UserService.GetLinkedIdentity:output_type -> memos.api.LinkedIdentity - 46, // 129: memos.api.UserService.CreateLinkedIdentity:output_type -> memos.api.LinkedIdentity - 83, // 130: memos.api.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty - 54, // 131: memos.api.UserService.ListPersonalAccessTokens:output_type -> memos.api.ListPersonalAccessTokensResponse - 56, // 132: memos.api.UserService.CreatePersonalAccessToken:output_type -> memos.api.CreatePersonalAccessTokenResponse - 83, // 133: memos.api.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty - 84, // 134: memos.api.UserService.ExportMemos:output_type -> google.api.HttpBody - 60, // 135: memos.api.UserService.ImportMemos:output_type -> memos.api.ImportMemosResponse - 102, // [102:136] is the sub-list for method output_type - 68, // [68:102] is the sub-list for method input_type - 68, // [68:68] is the sub-list for extension type_name - 68, // [68:68] is the sub-list for extension extendee - 0, // [0:68] is the sub-list for field type_name + 0, // 0: memos.api.User.role:type_name -> memos.api.User.Role + 77, // 1: memos.api.User.state:type_name -> memos.api.State + 78, // 2: memos.api.User.create_time:type_name -> google.protobuf.Timestamp + 78, // 3: memos.api.User.update_time:type_name -> google.protobuf.Timestamp + 6, // 4: memos.api.ListUsersResponse.users:type_name -> memos.api.User + 79, // 5: memos.api.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask + 6, // 6: memos.api.BatchGetUsersResponse.users:type_name -> memos.api.User + 6, // 7: memos.api.CreateUserRequest.user:type_name -> memos.api.User + 6, // 8: memos.api.UpdateUserRequest.user:type_name -> memos.api.User + 79, // 9: memos.api.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask + 65, // 10: memos.api.UserSetting.general_setting:type_name -> memos.api.UserSetting.GeneralSetting + 66, // 11: memos.api.UserSetting.webhooks_setting:type_name -> memos.api.UserSetting.WebhooksSetting + 67, // 12: memos.api.UserSetting.tags_setting:type_name -> memos.api.UserSetting.TagsSetting + 15, // 13: memos.api.ListUserSettingsResponse.user_settings:type_name -> memos.api.UserSetting + 15, // 14: memos.api.UpdateUserSettingRequest.user_setting:type_name -> memos.api.UserSetting + 79, // 15: memos.api.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask + 71, // 16: memos.api.UserStats.memo_type_stats:type_name -> memos.api.UserStats.MemoTypeStats + 70, // 17: memos.api.UserStats.tag_counts:type_name -> memos.api.UserStats.TagCountsEntry + 78, // 18: memos.api.UserStats.memo_created_timestamps:type_name -> google.protobuf.Timestamp + 78, // 19: memos.api.UserStats.memo_updated_timestamps:type_name -> google.protobuf.Timestamp + 77, // 20: memos.api.ListUserStatsRequest.state:type_name -> memos.api.State + 20, // 21: memos.api.ListUserStatsResponse.user_stats:type_name -> memos.api.UserStats + 72, // 22: memos.api.UserUsage.limits:type_name -> memos.api.UserUsage.Limits + 73, // 23: memos.api.MemoView.icon:type_name -> memos.api.MemoView.Icon + 26, // 24: memos.api.ListMemoViewsResponse.memo_views:type_name -> memos.api.MemoView + 26, // 25: memos.api.CreateMemoViewRequest.memo_view:type_name -> memos.api.MemoView + 26, // 26: memos.api.UpdateMemoViewRequest.memo_view:type_name -> memos.api.MemoView + 79, // 27: memos.api.UpdateMemoViewRequest.update_mask:type_name -> google.protobuf.FieldMask + 6, // 28: memos.api.UserNotification.sender_user:type_name -> memos.api.User + 3, // 29: memos.api.UserNotification.type:type_name -> memos.api.UserNotification.Type + 74, // 30: memos.api.UserNotification.memo_comment:type_name -> memos.api.UserNotification.MemoCommentPayload + 75, // 31: memos.api.UserNotification.memo_mention:type_name -> memos.api.UserNotification.MemoMentionPayload + 76, // 32: memos.api.UserNotification.space_invitation:type_name -> memos.api.UserNotification.SpaceInvitationPayload + 2, // 33: memos.api.UserNotification.status:type_name -> memos.api.UserNotification.Status + 78, // 34: memos.api.UserNotification.create_time:type_name -> google.protobuf.Timestamp + 33, // 35: memos.api.ListUserNotificationsResponse.notifications:type_name -> memos.api.UserNotification + 33, // 36: memos.api.UpdateUserNotificationRequest.notification:type_name -> memos.api.UserNotification + 79, // 37: memos.api.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask + 78, // 38: memos.api.UserWebhook.create_time:type_name -> google.protobuf.Timestamp + 78, // 39: memos.api.UserWebhook.update_time:type_name -> google.protobuf.Timestamp + 38, // 40: memos.api.ListUserWebhooksResponse.webhooks:type_name -> memos.api.UserWebhook + 38, // 41: memos.api.CreateUserWebhookRequest.webhook:type_name -> memos.api.UserWebhook + 38, // 42: memos.api.UpdateUserWebhookRequest.webhook:type_name -> memos.api.UserWebhook + 79, // 43: memos.api.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask + 46, // 44: memos.api.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.LinkedIdentity + 78, // 45: memos.api.PersonalAccessToken.create_time:type_name -> google.protobuf.Timestamp + 78, // 46: memos.api.PersonalAccessToken.expire_time:type_name -> google.protobuf.Timestamp + 78, // 47: memos.api.PersonalAccessToken.last_use_time:type_name -> google.protobuf.Timestamp + 52, // 48: memos.api.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.PersonalAccessToken + 52, // 49: memos.api.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.PersonalAccessToken + 61, // 50: memos.api.ImportMemosRequest.spec:type_name -> memos.api.ImportMemosSpec + 5, // 51: memos.api.ImportMemosRequest.conflict_policy:type_name -> memos.api.ImportMemosRequest.ConflictPolicy + 62, // 52: memos.api.ImportMemosResponse.plan:type_name -> memos.api.ImportMemosPlan + 63, // 53: memos.api.ImportMemosResponse.report:type_name -> memos.api.ImportMemosReport + 78, // 54: memos.api.ImportMemosPlan.export_time:type_name -> google.protobuf.Timestamp + 64, // 55: memos.api.ImportMemosPlan.warnings:type_name -> memos.api.ImportMemosIssue + 64, // 56: memos.api.ImportMemosReport.warnings:type_name -> memos.api.ImportMemosIssue + 64, // 57: memos.api.ImportMemosReport.failures:type_name -> memos.api.ImportMemosIssue + 38, // 58: memos.api.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.UserWebhook + 69, // 59: memos.api.UserSetting.TagsSetting.tags:type_name -> memos.api.UserSetting.TagsSetting.TagsEntry + 80, // 60: memos.api.UserSetting.TagMetadata.background_color:type_name -> google.type.Color + 68, // 61: memos.api.UserSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.UserSetting.TagMetadata + 81, // 62: memos.api.UserNotification.SpaceInvitationPayload.space:type_name -> memos.api.Space + 82, // 63: memos.api.UserNotification.SpaceInvitationPayload.role:type_name -> memos.api.SpaceMember.Role + 4, // 64: memos.api.UserNotification.SpaceInvitationPayload.state:type_name -> memos.api.UserNotification.SpaceInvitationPayload.InvitationState + 7, // 65: memos.api.UserService.ListUsers:input_type -> memos.api.ListUsersRequest + 9, // 66: memos.api.UserService.GetUser:input_type -> memos.api.GetUserRequest + 10, // 67: memos.api.UserService.BatchGetUsers:input_type -> memos.api.BatchGetUsersRequest + 12, // 68: memos.api.UserService.CreateUser:input_type -> memos.api.CreateUserRequest + 13, // 69: memos.api.UserService.UpdateUser:input_type -> memos.api.UpdateUserRequest + 14, // 70: memos.api.UserService.DeleteUser:input_type -> memos.api.DeleteUserRequest + 16, // 71: memos.api.UserService.ListUserSettings:input_type -> memos.api.ListUserSettingsRequest + 18, // 72: memos.api.UserService.GetUserSetting:input_type -> memos.api.GetUserSettingRequest + 19, // 73: memos.api.UserService.UpdateUserSetting:input_type -> memos.api.UpdateUserSettingRequest + 21, // 74: memos.api.UserService.ListUserStats:input_type -> memos.api.ListUserStatsRequest + 23, // 75: memos.api.UserService.GetUserStats:input_type -> memos.api.GetUserStatsRequest + 25, // 76: memos.api.UserService.GetUserUsage:input_type -> memos.api.GetUserUsageRequest + 27, // 77: memos.api.UserService.ListMemoViews:input_type -> memos.api.ListMemoViewsRequest + 29, // 78: memos.api.UserService.GetMemoView:input_type -> memos.api.GetMemoViewRequest + 30, // 79: memos.api.UserService.CreateMemoView:input_type -> memos.api.CreateMemoViewRequest + 31, // 80: memos.api.UserService.UpdateMemoView:input_type -> memos.api.UpdateMemoViewRequest + 32, // 81: memos.api.UserService.DeleteMemoView:input_type -> memos.api.DeleteMemoViewRequest + 34, // 82: memos.api.UserService.ListUserNotifications:input_type -> memos.api.ListUserNotificationsRequest + 36, // 83: memos.api.UserService.UpdateUserNotification:input_type -> memos.api.UpdateUserNotificationRequest + 37, // 84: memos.api.UserService.DeleteUserNotification:input_type -> memos.api.DeleteUserNotificationRequest + 39, // 85: memos.api.UserService.ListUserWebhooks:input_type -> memos.api.ListUserWebhooksRequest + 41, // 86: memos.api.UserService.CreateUserWebhook:input_type -> memos.api.CreateUserWebhookRequest + 42, // 87: memos.api.UserService.UpdateUserWebhook:input_type -> memos.api.UpdateUserWebhookRequest + 43, // 88: memos.api.UserService.DeleteUserWebhook:input_type -> memos.api.DeleteUserWebhookRequest + 44, // 89: memos.api.UserService.GetUserWebhookSigningSecret:input_type -> memos.api.GetUserWebhookSigningSecretRequest + 47, // 90: memos.api.UserService.ListLinkedIdentities:input_type -> memos.api.ListLinkedIdentitiesRequest + 49, // 91: memos.api.UserService.GetLinkedIdentity:input_type -> memos.api.GetLinkedIdentityRequest + 50, // 92: memos.api.UserService.CreateLinkedIdentity:input_type -> memos.api.CreateLinkedIdentityRequest + 51, // 93: memos.api.UserService.DeleteLinkedIdentity:input_type -> memos.api.DeleteLinkedIdentityRequest + 53, // 94: memos.api.UserService.ListPersonalAccessTokens:input_type -> memos.api.ListPersonalAccessTokensRequest + 55, // 95: memos.api.UserService.CreatePersonalAccessToken:input_type -> memos.api.CreatePersonalAccessTokenRequest + 57, // 96: memos.api.UserService.DeletePersonalAccessToken:input_type -> memos.api.DeletePersonalAccessTokenRequest + 58, // 97: memos.api.UserService.ExportMemos:input_type -> memos.api.ExportMemosRequest + 59, // 98: memos.api.UserService.ImportMemos:input_type -> memos.api.ImportMemosRequest + 8, // 99: memos.api.UserService.ListUsers:output_type -> memos.api.ListUsersResponse + 6, // 100: memos.api.UserService.GetUser:output_type -> memos.api.User + 11, // 101: memos.api.UserService.BatchGetUsers:output_type -> memos.api.BatchGetUsersResponse + 6, // 102: memos.api.UserService.CreateUser:output_type -> memos.api.User + 6, // 103: memos.api.UserService.UpdateUser:output_type -> memos.api.User + 83, // 104: memos.api.UserService.DeleteUser:output_type -> google.protobuf.Empty + 17, // 105: memos.api.UserService.ListUserSettings:output_type -> memos.api.ListUserSettingsResponse + 15, // 106: memos.api.UserService.GetUserSetting:output_type -> memos.api.UserSetting + 15, // 107: memos.api.UserService.UpdateUserSetting:output_type -> memos.api.UserSetting + 22, // 108: memos.api.UserService.ListUserStats:output_type -> memos.api.ListUserStatsResponse + 20, // 109: memos.api.UserService.GetUserStats:output_type -> memos.api.UserStats + 24, // 110: memos.api.UserService.GetUserUsage:output_type -> memos.api.UserUsage + 28, // 111: memos.api.UserService.ListMemoViews:output_type -> memos.api.ListMemoViewsResponse + 26, // 112: memos.api.UserService.GetMemoView:output_type -> memos.api.MemoView + 26, // 113: memos.api.UserService.CreateMemoView:output_type -> memos.api.MemoView + 26, // 114: memos.api.UserService.UpdateMemoView:output_type -> memos.api.MemoView + 83, // 115: memos.api.UserService.DeleteMemoView:output_type -> google.protobuf.Empty + 35, // 116: memos.api.UserService.ListUserNotifications:output_type -> memos.api.ListUserNotificationsResponse + 33, // 117: memos.api.UserService.UpdateUserNotification:output_type -> memos.api.UserNotification + 83, // 118: memos.api.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty + 40, // 119: memos.api.UserService.ListUserWebhooks:output_type -> memos.api.ListUserWebhooksResponse + 38, // 120: memos.api.UserService.CreateUserWebhook:output_type -> memos.api.UserWebhook + 38, // 121: memos.api.UserService.UpdateUserWebhook:output_type -> memos.api.UserWebhook + 83, // 122: memos.api.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty + 45, // 123: memos.api.UserService.GetUserWebhookSigningSecret:output_type -> memos.api.GetUserWebhookSigningSecretResponse + 48, // 124: memos.api.UserService.ListLinkedIdentities:output_type -> memos.api.ListLinkedIdentitiesResponse + 46, // 125: memos.api.UserService.GetLinkedIdentity:output_type -> memos.api.LinkedIdentity + 46, // 126: memos.api.UserService.CreateLinkedIdentity:output_type -> memos.api.LinkedIdentity + 83, // 127: memos.api.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty + 54, // 128: memos.api.UserService.ListPersonalAccessTokens:output_type -> memos.api.ListPersonalAccessTokensResponse + 56, // 129: memos.api.UserService.CreatePersonalAccessToken:output_type -> memos.api.CreatePersonalAccessTokenResponse + 83, // 130: memos.api.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty + 84, // 131: memos.api.UserService.ExportMemos:output_type -> google.api.HttpBody + 60, // 132: memos.api.UserService.ImportMemos:output_type -> memos.api.ImportMemosResponse + 99, // [99:133] is the sub-list for method output_type + 65, // [65:99] is the sub-list for method input_type + 65, // [65:65] is the sub-list for extension type_name + 65, // [65:65] is the sub-list for extension extendee + 0, // [0:65] is the sub-list for field type_name } func init() { file_api_user_service_proto_init() } @@ -5682,6 +5682,7 @@ func file_api_user_service_proto_init() { (*ImportMemosResponse_Plan)(nil), (*ImportMemosResponse_Report)(nil), } + file_api_user_service_proto_msgTypes[66].OneofWrappers = []any{} file_api_user_service_proto_msgTypes[67].OneofWrappers = []any{ (*MemoView_Icon_Emoji)(nil), (*MemoView_Icon_Lucide)(nil), diff --git a/proto/gen/openapi.yaml b/proto/gen/openapi.yaml index 4d8e8f057de53..cf3f5d257d74d 100644 --- a/proto/gen/openapi.yaml +++ b/proto/gen/openapi.yaml @@ -1927,180 +1927,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - /api/usageLimits: - get: - tags: - - InstanceService - description: |- - ListUsageLimits lists the usage limits: the defaults for every user and the - limits for single users. Admin only. - operationId: InstanceService_ListUsageLimits - parameters: - - name: pageSize - in: query - description: |- - Optional. The maximum number of usage limits to return. If unspecified, at - most 50 are returned. The maximum value is 1000; values above 1000 are - coerced to 1000. - schema: - type: integer - format: int32 - - name: pageToken - in: query - description: |- - Optional. A page token from a previous call. Provide it to retrieve the - next page. - schema: - type: string - - name: filter - in: query - description: |- - Optional. A CEL filter on `user` and `metric`, combined with `&&`. - `user == ""` selects the defaults for every user. - Example: "user == 'users/steven' && metric == 'STORAGE_BYTES'" - schema: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ListUsageLimitsResponse' - default: - description: Default error response - content: - application/json: - schema: - $ref: '#/components/schemas/Status' - post: - tags: - - InstanceService - description: |- - CreateUsageLimit creates a usage limit. Admin only. There is at most one - limit per metric and user, and one default per metric. Fails with - FAILED_PRECONDITION when the usage limits are configured by the deployment. - operationId: InstanceService_CreateUsageLimit - parameters: - - name: usageLimitId - in: query - description: |- - Optional. The ID to use for the usage limit, which becomes the final - segment of its name. Lowercase letters, digits and hyphens, at most 32 - characters, starting with a letter. Generated when empty. - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UsageLimit' - required: true - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/UsageLimit' - default: - description: Default error response - content: - application/json: - schema: - $ref: '#/components/schemas/Status' - /api/usageLimits/{usageLimit}: - get: - tags: - - InstanceService - description: GetUsageLimit gets a usage limit. Admin only. - operationId: InstanceService_GetUsageLimit - parameters: - - name: usageLimit - in: path - description: The usageLimit id. - required: true - schema: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/UsageLimit' - default: - description: Default error response - content: - application/json: - schema: - $ref: '#/components/schemas/Status' - delete: - tags: - - InstanceService - description: |- - DeleteUsageLimit deletes a usage limit. Admin only. Deleting a user's limit - returns the user to the default; deleting a default makes the metric - unlimited for users without their own limit. - operationId: InstanceService_DeleteUsageLimit - parameters: - - name: usageLimit - in: path - description: The usageLimit id. - required: true - schema: - type: string - responses: - "200": - description: OK - content: {} - default: - description: Default error response - content: - application/json: - schema: - $ref: '#/components/schemas/Status' - patch: - tags: - - InstanceService - description: |- - UpdateUsageLimit updates the value of a usage limit. Admin only. The metric - and user cannot change. - operationId: InstanceService_UpdateUsageLimit - parameters: - - name: usageLimit - in: path - description: The usageLimit id. - required: true - schema: - type: string - - name: updateMask - in: query - description: |- - Optional. The fields to update. Only `value` is supported, which is also - the default. - schema: - type: string - format: field-mask - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UsageLimit' - required: true - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/UsageLimit' - default: - description: Default error response - content: - application/json: - schema: - $ref: '#/components/schemas/Status' /api/users: get: tags: @@ -4093,6 +3919,10 @@ components: allOf: - $ref: '#/components/schemas/InstanceSetting_AccessSetting' description: The value of the ACCESS setting. + usageLimitsSetting: + allOf: + - $ref: '#/components/schemas/InstanceSetting_UsageLimitsSetting' + description: The value of the USAGE_LIMITS setting. description: InstanceSetting is one group of instance-wide settings, selected by its key. InstanceSetting_AIProviderConfig: type: object @@ -4309,6 +4139,21 @@ components: type: string description: A default spelling and vocabulary hint passed to the provider. Hidden from non-admins. description: TranscriptionConfig is the speech-to-text feature configuration. + InstanceSetting_UsageLimitsSetting: + type: object + properties: + limits: + type: array + items: + $ref: '#/components/schemas/UsageLimitsSetting_UsageLimit' + description: |- + The entries, in the order they are checked. At most 100, naming at most + 1,000 users in all. + description: |- + UsageLimitsSetting is an ordered list of limit sets. The first entry that + names a user is that user's limit set, whole. The last entry has all_users + set and is the default. The setting is read and replaced whole. Admins + are limited like everyone else. InstanceStats: type: object properties: @@ -4526,18 +4371,6 @@ components: type: string description: A token to retrieve the next page. Empty when there are no more pages. description: Response message for SpaceService.ListSpaces. - ListUsageLimitsResponse: - type: object - properties: - usageLimits: - type: array - items: - $ref: '#/components/schemas/UsageLimit' - description: The usage limits, defaults first, then by user and metric. - nextPageToken: - type: string - description: A token to retrieve the next page. Empty when there are no more pages. - description: Response message for InstanceService.ListUsageLimits. ListUserNotificationsResponse: type: object properties: @@ -5541,48 +5374,40 @@ components: - $ref: '#/components/schemas/Reaction' description: Required. The reaction to upsert. Only reaction_type is used. description: Request message for MemoService.UpsertMemoReaction. - UsageLimit: - required: - - metric - - value + UsageLimitsSetting_UsageLimit: type: object properties: - name: - type: string + users: + type: array + items: + type: string description: |- - Identifier. The resource name of the usage limit. - Format: usageLimits/{usage_limit} - metric: - enum: - - METRIC_UNSPECIFIED - - MEMO_COUNT - - MEMO_CONTENT_BYTES - - STORAGE_BYTES - - UPLOAD_BYTES - type: string - description: Required. Immutable. What is limited. - format: enum - user: + The users the entry applies to. Not repeated within an entry. + Format: users/{user} + allUsers: + type: boolean + description: |- + Whether the entry applies to every user. Exactly one entry has it, and + it is the last entry. An entry has users or all_users, never both. + memoCount: type: string description: |- - Optional. Immutable. The user the limit applies to. Empty means every user. - Format: users/{user} - value: + The number of memos the user may have created, comments and archived + memos included. + memoContentBytes: type: string - description: Required. The limit. Must not be negative. 0 means unlimited. - createTime: - readOnly: true + description: The UTF-8 size in bytes of one memo's content. + storageBytes: type: string - description: Output only. The creation time. - format: date-time - updateTime: - readOnly: true + description: |- + The total size in bytes of the user's uploads. External links do not + count. + uploadBytes: type: string - description: Output only. The last update time. - format: date-time + description: The size in bytes of one uploaded file. description: |- - UsageLimit sets one limit for every user, or for one user. When both exist - for the same metric, the limit for the user applies to that user. + UsageLimit is one entry: who it applies to and the limit for each + metric. An unset limit is unlimited, as is 0. User: required: - username @@ -5945,38 +5770,35 @@ components: description: |- The total size in bytes of the user's uploads, including uploads not attached to a memo. External links do not count. - memoCountLimit: - allOf: - - $ref: '#/components/schemas/UserUsage_EffectiveLimit' - description: The limit on memo_count. - storageBytesLimit: - allOf: - - $ref: '#/components/schemas/UserUsage_EffectiveLimit' - description: The limit on storage_bytes. - uploadBytesLimit: + limits: allOf: - - $ref: '#/components/schemas/UserUsage_EffectiveLimit' - description: The limit on the size of one uploaded file. - memoContentBytesLimit: - allOf: - - $ref: '#/components/schemas/UserUsage_EffectiveLimit' - description: The limit on the UTF-8 size in bytes of one memo's content. - description: UserUsage is a user's usage and the usage limits in effect for the user. - UserUsage_EffectiveLimit: + - $ref: '#/components/schemas/UserUsage_Limits' + description: |- + The limit set in effect: the first USAGE_LIMITS entry that names the + user, or the default. An unset limit is unlimited. + limitIndex: + type: integer + description: |- + The position of the entry in effect in the USAGE_LIMITS setting, starting + at 0. The default is the last entry. + format: int32 + description: UserUsage is a user's usage and the limit set in effect for the user. + UserUsage_Limits: type: object properties: - value: + memoCount: type: string - description: The limit. 0 means unlimited. - usageLimit: + description: The number of memos the user may have created. + memoContentBytes: type: string - description: |- - The usage limit the value comes from: the user's own limit, or else the - default. Empty when no usage limit applies. - Format: usageLimits/{usage_limit} - description: |- - EffectiveLimit is the limit in effect for one metric, and where it comes - from. + description: The UTF-8 size in bytes of one memo's content. + storageBytes: + type: string + description: The total size in bytes of the user's uploads. + uploadBytes: + type: string + description: The size in bytes of one uploaded file. + description: Limits is the limit for each metric. An unset limit is unlimited, as is 0. UserWebhook: type: object properties: diff --git a/proto/gen/store/instance_setting.pb.go b/proto/gen/store/instance_setting.pb.go index c77fb94afa77a..f9cde89b98fec 100644 --- a/proto/gen/store/instance_setting.pb.go +++ b/proto/gen/store/instance_setting.pb.go @@ -10,8 +10,6 @@ import ( color "google.golang.org/genproto/googleapis/type/color" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - emptypb "google.golang.org/protobuf/types/known/emptypb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -44,8 +42,8 @@ const ( InstanceSettingKey_AI InstanceSettingKey = 7 // ACCESS is the key for instance access policy settings. InstanceSettingKey_ACCESS InstanceSettingKey = 8 - // USAGE_LIMITS is the key for per-user usage limits. Store only; exposed - // through the usageLimits API, never through the instance settings API. + // USAGE_LIMITS is the key for the ordered usage limits list, read and + // replaced whole through the instance settings API. InstanceSettingKey_USAGE_LIMITS InstanceSettingKey = 9 ) @@ -313,66 +311,6 @@ func (InstanceStorageSetting_StorageType) EnumDescriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{5, 0} } -type InstanceUsageLimitsSetting_Metric int32 - -const ( - InstanceUsageLimitsSetting_METRIC_UNSPECIFIED InstanceUsageLimitsSetting_Metric = 0 - // MEMO_COUNT limits the number of memos a user created, comments included. - InstanceUsageLimitsSetting_MEMO_COUNT InstanceUsageLimitsSetting_Metric = 1 - // MEMO_CONTENT_BYTES limits the UTF-8 size of one memo's content. - InstanceUsageLimitsSetting_MEMO_CONTENT_BYTES InstanceUsageLimitsSetting_Metric = 2 - // STORAGE_BYTES limits the total size of a user's uploads. External links - // do not count. - InstanceUsageLimitsSetting_STORAGE_BYTES InstanceUsageLimitsSetting_Metric = 3 - // UPLOAD_BYTES limits the size of one uploaded file. - InstanceUsageLimitsSetting_UPLOAD_BYTES InstanceUsageLimitsSetting_Metric = 4 -) - -// Enum value maps for InstanceUsageLimitsSetting_Metric. -var ( - InstanceUsageLimitsSetting_Metric_name = map[int32]string{ - 0: "METRIC_UNSPECIFIED", - 1: "MEMO_COUNT", - 2: "MEMO_CONTENT_BYTES", - 3: "STORAGE_BYTES", - 4: "UPLOAD_BYTES", - } - InstanceUsageLimitsSetting_Metric_value = map[string]int32{ - "METRIC_UNSPECIFIED": 0, - "MEMO_COUNT": 1, - "MEMO_CONTENT_BYTES": 2, - "STORAGE_BYTES": 3, - "UPLOAD_BYTES": 4, - } -) - -func (x InstanceUsageLimitsSetting_Metric) Enum() *InstanceUsageLimitsSetting_Metric { - p := new(InstanceUsageLimitsSetting_Metric) - *p = x - return p -} - -func (x InstanceUsageLimitsSetting_Metric) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (InstanceUsageLimitsSetting_Metric) Descriptor() protoreflect.EnumDescriptor { - return file_store_instance_setting_proto_enumTypes[5].Descriptor() -} - -func (InstanceUsageLimitsSetting_Metric) Type() protoreflect.EnumType { - return &file_store_instance_setting_proto_enumTypes[5] -} - -func (x InstanceUsageLimitsSetting_Metric) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use InstanceUsageLimitsSetting_Metric.Descriptor instead. -func (InstanceUsageLimitsSetting_Metric) EnumDescriptor() ([]byte, []int) { - return file_store_instance_setting_proto_rawDescGZIP(), []int{15, 0} -} - type InstanceSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Key InstanceSettingKey `protobuf:"varint,1,opt,name=key,proto3,enum=memos.store.InstanceSettingKey" json:"key,omitempty"` @@ -1537,9 +1475,9 @@ func (x *InstanceAccessSetting) GetAccessMode() InstanceAccessMode { return InstanceAccessMode_INSTANCE_ACCESS_MODE_UNSPECIFIED } -// InstanceUsageLimitsSetting holds every usage limit of the instance. A limit -// applies to one user, or independently to every user. For each metric, a -// user's own limit replaces the default. +// InstanceUsageLimitsSetting is an ordered list of limit sets. The first +// entry that names a user is that user's limit set, whole. The last entry +// has all_users set and is the default. An unset value is unlimited, as is 0. type InstanceUsageLimitsSetting struct { state protoimpl.MessageState `protogen:"open.v1"` Limits []*InstanceUsageLimitsSetting_UsageLimit `protobuf:"bytes,1,rep,name=limits,proto3" json:"limits,omitempty"` @@ -1702,20 +1640,21 @@ func (x *InstanceNotificationSetting_EmailSetting) GetUseSsl() bool { type InstanceUsageLimitsSetting_UsageLimit struct { state protoimpl.MessageState `protogen:"open.v1"` - // uid is the {usage_limit} segment of the usageLimits/{usage_limit} name. - Uid string `protobuf:"bytes,1,opt,name=uid,proto3" json:"uid,omitempty"` - Metric InstanceUsageLimitsSetting_Metric `protobuf:"varint,2,opt,name=metric,proto3,enum=memos.store.InstanceUsageLimitsSetting_Metric" json:"metric,omitempty"` - // Exactly one target must be set. - // - // Types that are valid to be assigned to Target: - // - // *InstanceUsageLimitsSetting_UsageLimit_AllUsers - // *InstanceUsageLimitsSetting_UsageLimit_UserId - Target isInstanceUsageLimitsSetting_UsageLimit_Target `protobuf_oneof:"target"` - // value is the limit. 0 means unlimited. - Value int64 `protobuf:"varint,5,opt,name=value,proto3" json:"value,omitempty"` - CreateTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` - UpdateTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` + // The users this entry applies to. Positive IDs, not repeated. + UserIds []int32 `protobuf:"varint,1,rep,packed,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"` + // Field 2 is kept for labels: users holding any listed label would match. + // all_users makes the entry the default. Exactly one entry has it, and it + // is the last entry. An entry has user_ids or all_users, never both. + AllUsers bool `protobuf:"varint,3,opt,name=all_users,json=allUsers,proto3" json:"all_users,omitempty"` + // memo_count limits the number of memos a user created, comments included. + MemoCount *int64 `protobuf:"varint,4,opt,name=memo_count,json=memoCount,proto3,oneof" json:"memo_count,omitempty"` + // memo_content_bytes limits the UTF-8 size of one memo's content. + MemoContentBytes *int64 `protobuf:"varint,5,opt,name=memo_content_bytes,json=memoContentBytes,proto3,oneof" json:"memo_content_bytes,omitempty"` + // storage_bytes limits the total size of a user's uploads. External links + // do not count. + StorageBytes *int64 `protobuf:"varint,6,opt,name=storage_bytes,json=storageBytes,proto3,oneof" json:"storage_bytes,omitempty"` + // upload_bytes limits the size of one uploaded file. + UploadBytes *int64 `protobuf:"varint,7,opt,name=upload_bytes,json=uploadBytes,proto3,oneof" json:"upload_bytes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1750,91 +1689,53 @@ func (*InstanceUsageLimitsSetting_UsageLimit) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{15, 0} } -func (x *InstanceUsageLimitsSetting_UsageLimit) GetUid() string { - if x != nil { - return x.Uid - } - return "" -} - -func (x *InstanceUsageLimitsSetting_UsageLimit) GetMetric() InstanceUsageLimitsSetting_Metric { - if x != nil { - return x.Metric - } - return InstanceUsageLimitsSetting_METRIC_UNSPECIFIED -} - -func (x *InstanceUsageLimitsSetting_UsageLimit) GetTarget() isInstanceUsageLimitsSetting_UsageLimit_Target { +func (x *InstanceUsageLimitsSetting_UsageLimit) GetUserIds() []int32 { if x != nil { - return x.Target + return x.UserIds } return nil } -func (x *InstanceUsageLimitsSetting_UsageLimit) GetAllUsers() *emptypb.Empty { +func (x *InstanceUsageLimitsSetting_UsageLimit) GetAllUsers() bool { if x != nil { - if x, ok := x.Target.(*InstanceUsageLimitsSetting_UsageLimit_AllUsers); ok { - return x.AllUsers - } + return x.AllUsers } - return nil + return false } -func (x *InstanceUsageLimitsSetting_UsageLimit) GetUserId() int32 { - if x != nil { - if x, ok := x.Target.(*InstanceUsageLimitsSetting_UsageLimit_UserId); ok { - return x.UserId - } +func (x *InstanceUsageLimitsSetting_UsageLimit) GetMemoCount() int64 { + if x != nil && x.MemoCount != nil { + return *x.MemoCount } return 0 } -func (x *InstanceUsageLimitsSetting_UsageLimit) GetValue() int64 { - if x != nil { - return x.Value +func (x *InstanceUsageLimitsSetting_UsageLimit) GetMemoContentBytes() int64 { + if x != nil && x.MemoContentBytes != nil { + return *x.MemoContentBytes } return 0 } -func (x *InstanceUsageLimitsSetting_UsageLimit) GetCreateTime() *timestamppb.Timestamp { - if x != nil { - return x.CreateTime +func (x *InstanceUsageLimitsSetting_UsageLimit) GetStorageBytes() int64 { + if x != nil && x.StorageBytes != nil { + return *x.StorageBytes } - return nil + return 0 } -func (x *InstanceUsageLimitsSetting_UsageLimit) GetUpdateTime() *timestamppb.Timestamp { - if x != nil { - return x.UpdateTime +func (x *InstanceUsageLimitsSetting_UsageLimit) GetUploadBytes() int64 { + if x != nil && x.UploadBytes != nil { + return *x.UploadBytes } - return nil -} - -type isInstanceUsageLimitsSetting_UsageLimit_Target interface { - isInstanceUsageLimitsSetting_UsageLimit_Target() -} - -type InstanceUsageLimitsSetting_UsageLimit_AllUsers struct { - // The default limit applied independently to every user. - AllUsers *emptypb.Empty `protobuf:"bytes,3,opt,name=all_users,json=allUsers,proto3,oneof"` -} - -type InstanceUsageLimitsSetting_UsageLimit_UserId struct { - // The user this limit applies to. Must be positive. - UserId int32 `protobuf:"varint,4,opt,name=user_id,json=userId,proto3,oneof"` -} - -func (*InstanceUsageLimitsSetting_UsageLimit_AllUsers) isInstanceUsageLimitsSetting_UsageLimit_Target() { -} - -func (*InstanceUsageLimitsSetting_UsageLimit_UserId) isInstanceUsageLimitsSetting_UsageLimit_Target() { + return 0 } var File_store_instance_setting_proto protoreflect.FileDescriptor const file_store_instance_setting_proto_rawDesc = "" + "\n" + - "\x1cstore/instance_setting.proto\x12\vmemos.store\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17google/type/color.proto\"\xa5\x06\n" + + "\x1cstore/instance_setting.proto\x12\vmemos.store\x1a\x17google/type/color.proto\"\xa5\x06\n" + "\x0fInstanceSetting\x121\n" + "\x03key\x18\x01 \x01(\x0e2\x1f.memos.store.InstanceSettingKeyR\x03key\x12H\n" + "\rbasic_setting\x18\x02 \x01(\v2!.memos.store.InstanceBasicSettingH\x00R\fbasicSetting\x12N\n" + @@ -1934,28 +1835,22 @@ const file_store_instance_setting_proto_rawDesc = "" + "\x06prompt\x18\x04 \x01(\tR\x06prompt\"Y\n" + "\x15InstanceAccessSetting\x12@\n" + "\vaccess_mode\x18\x01 \x01(\x0e2\x1f.memos.store.InstanceAccessModeR\n" + - "accessMode\"\xac\x04\n" + + "accessMode\"\xa1\x03\n" + "\x1aInstanceUsageLimitsSetting\x12J\n" + - "\x06limits\x18\x01 \x03(\v22.memos.store.InstanceUsageLimitsSetting.UsageLimitR\x06limits\x1a\xd2\x02\n" + + "\x06limits\x18\x01 \x03(\v22.memos.store.InstanceUsageLimitsSetting.UsageLimitR\x06limits\x1a\xb6\x02\n" + "\n" + - "UsageLimit\x12\x10\n" + - "\x03uid\x18\x01 \x01(\tR\x03uid\x12F\n" + - "\x06metric\x18\x02 \x01(\x0e2..memos.store.InstanceUsageLimitsSetting.MetricR\x06metric\x125\n" + - "\tall_users\x18\x03 \x01(\v2\x16.google.protobuf.EmptyH\x00R\ballUsers\x12\x19\n" + - "\auser_id\x18\x04 \x01(\x05H\x00R\x06userId\x12\x14\n" + - "\x05value\x18\x05 \x01(\x03R\x05value\x12;\n" + - "\vcreate_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "createTime\x12;\n" + - "\vupdate_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "updateTimeB\b\n" + - "\x06target\"m\n" + - "\x06Metric\x12\x16\n" + - "\x12METRIC_UNSPECIFIED\x10\x00\x12\x0e\n" + + "UsageLimit\x12\x19\n" + + "\buser_ids\x18\x01 \x03(\x05R\auserIds\x12\x1b\n" + + "\tall_users\x18\x03 \x01(\bR\ballUsers\x12\"\n" + "\n" + - "MEMO_COUNT\x10\x01\x12\x16\n" + - "\x12MEMO_CONTENT_BYTES\x10\x02\x12\x11\n" + - "\rSTORAGE_BYTES\x10\x03\x12\x10\n" + - "\fUPLOAD_BYTES\x10\x04*\xb3\x01\n" + + "memo_count\x18\x04 \x01(\x03H\x00R\tmemoCount\x88\x01\x01\x121\n" + + "\x12memo_content_bytes\x18\x05 \x01(\x03H\x01R\x10memoContentBytes\x88\x01\x01\x12(\n" + + "\rstorage_bytes\x18\x06 \x01(\x03H\x02R\fstorageBytes\x88\x01\x01\x12&\n" + + "\fupload_bytes\x18\a \x01(\x03H\x03R\vuploadBytes\x88\x01\x01B\r\n" + + "\v_memo_countB\x15\n" + + "\x13_memo_content_bytesB\x10\n" + + "\x0e_storage_bytesB\x0f\n" + + "\r_upload_bytes*\xb3\x01\n" + "\x12InstanceSettingKey\x12$\n" + " INSTANCE_SETTING_KEY_UNSPECIFIED\x10\x00\x12\t\n" + "\x05BASIC\x10\x01\x12\v\n" + @@ -1997,7 +1892,7 @@ func file_store_instance_setting_proto_rawDescGZIP() []byte { return file_store_instance_setting_proto_rawDescData } -var file_store_instance_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 6) +var file_store_instance_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_store_instance_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_store_instance_setting_proto_goTypes = []any{ (InstanceSettingKey)(0), // 0: memos.store.InstanceSettingKey @@ -2005,65 +1900,58 @@ var file_store_instance_setting_proto_goTypes = []any{ (AIProviderType)(0), // 2: memos.store.AIProviderType (InstanceAccessMode)(0), // 3: memos.store.InstanceAccessMode (InstanceStorageSetting_StorageType)(0), // 4: memos.store.InstanceStorageSetting.StorageType - (InstanceUsageLimitsSetting_Metric)(0), // 5: memos.store.InstanceUsageLimitsSetting.Metric - (*InstanceSetting)(nil), // 6: memos.store.InstanceSetting - (*InstanceBasicSetting)(nil), // 7: memos.store.InstanceBasicSetting - (*InstanceGeneralSetting)(nil), // 8: memos.store.InstanceGeneralSetting - (*InstanceCustomProfile)(nil), // 9: memos.store.InstanceCustomProfile - (*Storage)(nil), // 10: memos.store.Storage - (*InstanceStorageSetting)(nil), // 11: memos.store.InstanceStorageSetting - (*StorageS3Config)(nil), // 12: memos.store.StorageS3Config - (*InstanceMemoRelatedSetting)(nil), // 13: memos.store.InstanceMemoRelatedSetting - (*InstanceTagMetadata)(nil), // 14: memos.store.InstanceTagMetadata - (*InstanceTagsSetting)(nil), // 15: memos.store.InstanceTagsSetting - (*InstanceNotificationSetting)(nil), // 16: memos.store.InstanceNotificationSetting - (*InstanceAISetting)(nil), // 17: memos.store.InstanceAISetting - (*AIProviderConfig)(nil), // 18: memos.store.AIProviderConfig - (*TranscriptionConfig)(nil), // 19: memos.store.TranscriptionConfig - (*InstanceAccessSetting)(nil), // 20: memos.store.InstanceAccessSetting - (*InstanceUsageLimitsSetting)(nil), // 21: memos.store.InstanceUsageLimitsSetting - nil, // 22: memos.store.InstanceTagsSetting.TagsEntry - (*InstanceNotificationSetting_EmailSetting)(nil), // 23: memos.store.InstanceNotificationSetting.EmailSetting - (*InstanceUsageLimitsSetting_UsageLimit)(nil), // 24: memos.store.InstanceUsageLimitsSetting.UsageLimit - (*color.Color)(nil), // 25: google.type.Color - (*emptypb.Empty)(nil), // 26: google.protobuf.Empty - (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp + (*InstanceSetting)(nil), // 5: memos.store.InstanceSetting + (*InstanceBasicSetting)(nil), // 6: memos.store.InstanceBasicSetting + (*InstanceGeneralSetting)(nil), // 7: memos.store.InstanceGeneralSetting + (*InstanceCustomProfile)(nil), // 8: memos.store.InstanceCustomProfile + (*Storage)(nil), // 9: memos.store.Storage + (*InstanceStorageSetting)(nil), // 10: memos.store.InstanceStorageSetting + (*StorageS3Config)(nil), // 11: memos.store.StorageS3Config + (*InstanceMemoRelatedSetting)(nil), // 12: memos.store.InstanceMemoRelatedSetting + (*InstanceTagMetadata)(nil), // 13: memos.store.InstanceTagMetadata + (*InstanceTagsSetting)(nil), // 14: memos.store.InstanceTagsSetting + (*InstanceNotificationSetting)(nil), // 15: memos.store.InstanceNotificationSetting + (*InstanceAISetting)(nil), // 16: memos.store.InstanceAISetting + (*AIProviderConfig)(nil), // 17: memos.store.AIProviderConfig + (*TranscriptionConfig)(nil), // 18: memos.store.TranscriptionConfig + (*InstanceAccessSetting)(nil), // 19: memos.store.InstanceAccessSetting + (*InstanceUsageLimitsSetting)(nil), // 20: memos.store.InstanceUsageLimitsSetting + nil, // 21: memos.store.InstanceTagsSetting.TagsEntry + (*InstanceNotificationSetting_EmailSetting)(nil), // 22: memos.store.InstanceNotificationSetting.EmailSetting + (*InstanceUsageLimitsSetting_UsageLimit)(nil), // 23: memos.store.InstanceUsageLimitsSetting.UsageLimit + (*color.Color)(nil), // 24: google.type.Color } var file_store_instance_setting_proto_depIdxs = []int32{ 0, // 0: memos.store.InstanceSetting.key:type_name -> memos.store.InstanceSettingKey - 7, // 1: memos.store.InstanceSetting.basic_setting:type_name -> memos.store.InstanceBasicSetting - 8, // 2: memos.store.InstanceSetting.general_setting:type_name -> memos.store.InstanceGeneralSetting - 11, // 3: memos.store.InstanceSetting.storage_setting:type_name -> memos.store.InstanceStorageSetting - 13, // 4: memos.store.InstanceSetting.memo_related_setting:type_name -> memos.store.InstanceMemoRelatedSetting - 15, // 5: memos.store.InstanceSetting.tags_setting:type_name -> memos.store.InstanceTagsSetting - 16, // 6: memos.store.InstanceSetting.notification_setting:type_name -> memos.store.InstanceNotificationSetting - 17, // 7: memos.store.InstanceSetting.ai_setting:type_name -> memos.store.InstanceAISetting - 20, // 8: memos.store.InstanceSetting.access_setting:type_name -> memos.store.InstanceAccessSetting - 21, // 9: memos.store.InstanceSetting.usage_limits_setting:type_name -> memos.store.InstanceUsageLimitsSetting - 9, // 10: memos.store.InstanceGeneralSetting.custom_profile:type_name -> memos.store.InstanceCustomProfile + 6, // 1: memos.store.InstanceSetting.basic_setting:type_name -> memos.store.InstanceBasicSetting + 7, // 2: memos.store.InstanceSetting.general_setting:type_name -> memos.store.InstanceGeneralSetting + 10, // 3: memos.store.InstanceSetting.storage_setting:type_name -> memos.store.InstanceStorageSetting + 12, // 4: memos.store.InstanceSetting.memo_related_setting:type_name -> memos.store.InstanceMemoRelatedSetting + 14, // 5: memos.store.InstanceSetting.tags_setting:type_name -> memos.store.InstanceTagsSetting + 15, // 6: memos.store.InstanceSetting.notification_setting:type_name -> memos.store.InstanceNotificationSetting + 16, // 7: memos.store.InstanceSetting.ai_setting:type_name -> memos.store.InstanceAISetting + 19, // 8: memos.store.InstanceSetting.access_setting:type_name -> memos.store.InstanceAccessSetting + 20, // 9: memos.store.InstanceSetting.usage_limits_setting:type_name -> memos.store.InstanceUsageLimitsSetting + 8, // 10: memos.store.InstanceGeneralSetting.custom_profile:type_name -> memos.store.InstanceCustomProfile 1, // 11: memos.store.Storage.type:type_name -> memos.store.StorageType - 12, // 12: memos.store.Storage.s3_config:type_name -> memos.store.StorageS3Config + 11, // 12: memos.store.Storage.s3_config:type_name -> memos.store.StorageS3Config 4, // 13: memos.store.InstanceStorageSetting.storage_type:type_name -> memos.store.InstanceStorageSetting.StorageType - 12, // 14: memos.store.InstanceStorageSetting.s3_config:type_name -> memos.store.StorageS3Config - 10, // 15: memos.store.InstanceStorageSetting.storages:type_name -> memos.store.Storage - 25, // 16: memos.store.InstanceTagMetadata.background_color:type_name -> google.type.Color - 22, // 17: memos.store.InstanceTagsSetting.tags:type_name -> memos.store.InstanceTagsSetting.TagsEntry - 23, // 18: memos.store.InstanceNotificationSetting.email:type_name -> memos.store.InstanceNotificationSetting.EmailSetting - 18, // 19: memos.store.InstanceAISetting.providers:type_name -> memos.store.AIProviderConfig - 19, // 20: memos.store.InstanceAISetting.transcription:type_name -> memos.store.TranscriptionConfig + 11, // 14: memos.store.InstanceStorageSetting.s3_config:type_name -> memos.store.StorageS3Config + 9, // 15: memos.store.InstanceStorageSetting.storages:type_name -> memos.store.Storage + 24, // 16: memos.store.InstanceTagMetadata.background_color:type_name -> google.type.Color + 21, // 17: memos.store.InstanceTagsSetting.tags:type_name -> memos.store.InstanceTagsSetting.TagsEntry + 22, // 18: memos.store.InstanceNotificationSetting.email:type_name -> memos.store.InstanceNotificationSetting.EmailSetting + 17, // 19: memos.store.InstanceAISetting.providers:type_name -> memos.store.AIProviderConfig + 18, // 20: memos.store.InstanceAISetting.transcription:type_name -> memos.store.TranscriptionConfig 2, // 21: memos.store.AIProviderConfig.type:type_name -> memos.store.AIProviderType 3, // 22: memos.store.InstanceAccessSetting.access_mode:type_name -> memos.store.InstanceAccessMode - 24, // 23: memos.store.InstanceUsageLimitsSetting.limits:type_name -> memos.store.InstanceUsageLimitsSetting.UsageLimit - 14, // 24: memos.store.InstanceTagsSetting.TagsEntry.value:type_name -> memos.store.InstanceTagMetadata - 5, // 25: memos.store.InstanceUsageLimitsSetting.UsageLimit.metric:type_name -> memos.store.InstanceUsageLimitsSetting.Metric - 26, // 26: memos.store.InstanceUsageLimitsSetting.UsageLimit.all_users:type_name -> google.protobuf.Empty - 27, // 27: memos.store.InstanceUsageLimitsSetting.UsageLimit.create_time:type_name -> google.protobuf.Timestamp - 27, // 28: memos.store.InstanceUsageLimitsSetting.UsageLimit.update_time:type_name -> google.protobuf.Timestamp - 29, // [29:29] is the sub-list for method output_type - 29, // [29:29] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 23, // 23: memos.store.InstanceUsageLimitsSetting.limits:type_name -> memos.store.InstanceUsageLimitsSetting.UsageLimit + 13, // 24: memos.store.InstanceTagsSetting.TagsEntry.value:type_name -> memos.store.InstanceTagMetadata + 25, // [25:25] is the sub-list for method output_type + 25, // [25:25] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_store_instance_setting_proto_init() } @@ -2085,16 +1973,13 @@ func file_store_instance_setting_proto_init() { file_store_instance_setting_proto_msgTypes[4].OneofWrappers = []any{ (*Storage_S3Config)(nil), } - file_store_instance_setting_proto_msgTypes[18].OneofWrappers = []any{ - (*InstanceUsageLimitsSetting_UsageLimit_AllUsers)(nil), - (*InstanceUsageLimitsSetting_UsageLimit_UserId)(nil), - } + file_store_instance_setting_proto_msgTypes[18].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_instance_setting_proto_rawDesc), len(file_store_instance_setting_proto_rawDesc)), - NumEnums: 6, + NumEnums: 5, NumMessages: 19, NumExtensions: 0, NumServices: 0, diff --git a/proto/store/instance_setting.proto b/proto/store/instance_setting.proto index 0a35711e826ef..7b9b718fc3507 100644 --- a/proto/store/instance_setting.proto +++ b/proto/store/instance_setting.proto @@ -2,8 +2,6 @@ syntax = "proto3"; package memos.store; -import "google/protobuf/empty.proto"; -import "google/protobuf/timestamp.proto"; import "google/type/color.proto"; option go_package = "gen/store"; @@ -26,8 +24,8 @@ enum InstanceSettingKey { AI = 7; // ACCESS is the key for instance access policy settings. ACCESS = 8; - // USAGE_LIMITS is the key for per-user usage limits. Store only; exposed - // through the usageLimits API, never through the instance settings API. + // USAGE_LIMITS is the key for the ordered usage limits list, read and + // replaced whole through the instance settings API. USAGE_LIMITS = 9; } @@ -246,39 +244,27 @@ message InstanceAccessSetting { InstanceAccessMode access_mode = 1; } -// InstanceUsageLimitsSetting holds every usage limit of the instance. A limit -// applies to one user, or independently to every user. For each metric, a -// user's own limit replaces the default. +// InstanceUsageLimitsSetting is an ordered list of limit sets. The first +// entry that names a user is that user's limit set, whole. The last entry +// has all_users set and is the default. An unset value is unlimited, as is 0. message InstanceUsageLimitsSetting { repeated UsageLimit limits = 1; message UsageLimit { - // uid is the {usage_limit} segment of the usageLimits/{usage_limit} name. - string uid = 1; - Metric metric = 2; - // Exactly one target must be set. - oneof target { - // The default limit applied independently to every user. - google.protobuf.Empty all_users = 3; - // The user this limit applies to. Must be positive. - int32 user_id = 4; - } - // value is the limit. 0 means unlimited. - int64 value = 5; - google.protobuf.Timestamp create_time = 6; - google.protobuf.Timestamp update_time = 7; - } - - enum Metric { - METRIC_UNSPECIFIED = 0; - // MEMO_COUNT limits the number of memos a user created, comments included. - MEMO_COUNT = 1; - // MEMO_CONTENT_BYTES limits the UTF-8 size of one memo's content. - MEMO_CONTENT_BYTES = 2; - // STORAGE_BYTES limits the total size of a user's uploads. External links + // The users this entry applies to. Positive IDs, not repeated. + repeated int32 user_ids = 1; + // Field 2 is kept for labels: users holding any listed label would match. + // all_users makes the entry the default. Exactly one entry has it, and it + // is the last entry. An entry has user_ids or all_users, never both. + bool all_users = 3; + // memo_count limits the number of memos a user created, comments included. + optional int64 memo_count = 4; + // memo_content_bytes limits the UTF-8 size of one memo's content. + optional int64 memo_content_bytes = 5; + // storage_bytes limits the total size of a user's uploads. External links // do not count. - STORAGE_BYTES = 3; - // UPLOAD_BYTES limits the size of one uploaded file. - UPLOAD_BYTES = 4; + optional int64 storage_bytes = 6; + // upload_bytes limits the size of one uploaded file. + optional int64 upload_bytes = 7; } } diff --git a/server/api/attachment_upload_test.go b/server/api/attachment_upload_test.go index ea807993bad7c..804ab4c413350 100644 --- a/server/api/attachment_upload_test.go +++ b/server/api/attachment_upload_test.go @@ -18,6 +18,7 @@ import ( "github.com/usememos/memos/internal/testutil" apipb "github.com/usememos/memos/proto/gen/api" + storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) @@ -159,8 +160,7 @@ func TestUploadAttachmentValidation(t *testing.T) { } _, err = svc.UploadAttachment(ctx, &apipb.UploadAttachmentRequest{Upload: uploadSpec("file", 3), WriteOffset: 1, Data: []byte("abc")}) require.Equal(t, codes.OutOfRange, status.Code(err)) - _, err = svc.Store.UpdateUsageLimit(ctx, "default-upload", 1<<30) - require.NoError(t, err) + setDefaultUploadLimitForTest(ctx, t, svc.Store, 1<<30) _, err = svc.UploadAttachment(ctx, &apipb.UploadAttachmentRequest{Upload: uploadSpec("file", 1<<60)}) require.Equal(t, codes.ResourceExhausted, status.Code(err)) require.Empty(t, svc.attachmentUploads.entries, "rejected spec calls must not register uploads") @@ -248,8 +248,7 @@ func TestUploadAttachmentRechecksMemoAndSize(t *testing.T) { // A per-file limit set after the upload started applies at finish. id := startTestUpload(ctx, t, svc, 2*MebiByte) - _, err = svc.Store.UpdateUsageLimit(ctx, "default-upload", MebiByte) - require.NoError(t, err) + setDefaultUploadLimitForTest(ctx, t, svc.Store, MebiByte) _, err = svc.UploadAttachment(ctx, &apipb.UploadAttachmentRequest{Upload: uploadID(id), Data: make([]byte, 2*MebiByte), FinishWrite: true}) require.Equal(t, codes.ResourceExhausted, status.Code(err)) rows, err := svc.Store.ListAttachments(ctx, &store.FindAttachment{}) @@ -298,12 +297,12 @@ func TestUploadAttachmentExpiryAndLimits(t *testing.T) { func TestUploadAttachmentAboveLegacyRequestLimit(t *testing.T) { svc, ctx := newUploadTestService(t) - _, err := svc.Store.UpdateUsageLimit(ctx, "default-upload", 0) - require.NoError(t, err) + setDefaultUploadLimitForTest(ctx, t, svc.Store, 0) const size = 300 * MebiByte id := startTestUpload(ctx, t, svc, size) chunk := bytes.Repeat([]byte{0x5a}, uploadChunkSize) var response *apipb.UploadAttachmentResponse + var err error for offset := int64(0); offset < size; offset += int64(len(chunk)) { response, err = svc.UploadAttachment(ctx, &apipb.UploadAttachmentRequest{ Upload: uploadID(id), WriteOffset: offset, Data: chunk, FinishWrite: offset+int64(len(chunk)) == size, @@ -364,3 +363,16 @@ func TestUploadAttachmentMediaProcessing(t *testing.T) { }) } } + +// setDefaultUploadLimitForTest stores one all-users entry with the given +// per-file limit. +func setDefaultUploadLimitForTest(ctx context.Context, t *testing.T, s *store.Store, uploadBytes int64) { + t.Helper() + _, err := s.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ + Key: storepb.InstanceSettingKey_USAGE_LIMITS, + Value: &storepb.InstanceSetting_UsageLimitsSetting{UsageLimitsSetting: &storepb.InstanceUsageLimitsSetting{ + Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{{AllUsers: true, UploadBytes: &uploadBytes}}, + }}, + }) + require.NoError(t, err) +} diff --git a/server/api/connect_services.go b/server/api/connect_services.go index 66b7599aeae44..33b157cfad5f0 100644 --- a/server/api/connect_services.go +++ b/server/api/connect_services.go @@ -64,46 +64,6 @@ func (s *ConnectServiceHandler) GetInstanceStats(ctx context.Context, req *conne return connect.NewResponse(resp), nil } -func (s *ConnectServiceHandler) ListUsageLimits(ctx context.Context, req *connect.Request[apipb.ListUsageLimitsRequest]) (*connect.Response[apipb.ListUsageLimitsResponse], error) { - resp, err := s.APIService.ListUsageLimits(ctx, req.Msg) - if err != nil { - return nil, convertGRPCError(err) - } - return connect.NewResponse(resp), nil -} - -func (s *ConnectServiceHandler) GetUsageLimit(ctx context.Context, req *connect.Request[apipb.GetUsageLimitRequest]) (*connect.Response[apipb.UsageLimit], error) { - resp, err := s.APIService.GetUsageLimit(ctx, req.Msg) - if err != nil { - return nil, convertGRPCError(err) - } - return connect.NewResponse(resp), nil -} - -func (s *ConnectServiceHandler) CreateUsageLimit(ctx context.Context, req *connect.Request[apipb.CreateUsageLimitRequest]) (*connect.Response[apipb.UsageLimit], error) { - resp, err := s.APIService.CreateUsageLimit(ctx, req.Msg) - if err != nil { - return nil, convertGRPCError(err) - } - return connect.NewResponse(resp), nil -} - -func (s *ConnectServiceHandler) UpdateUsageLimit(ctx context.Context, req *connect.Request[apipb.UpdateUsageLimitRequest]) (*connect.Response[apipb.UsageLimit], error) { - resp, err := s.APIService.UpdateUsageLimit(ctx, req.Msg) - if err != nil { - return nil, convertGRPCError(err) - } - return connect.NewResponse(resp), nil -} - -func (s *ConnectServiceHandler) DeleteUsageLimit(ctx context.Context, req *connect.Request[apipb.DeleteUsageLimitRequest]) (*connect.Response[emptypb.Empty], error) { - resp, err := s.APIService.DeleteUsageLimit(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/api/instance_service.go b/server/api/instance_service.go index 86f6abb8a67a4..c571eb6b3174a 100644 --- a/server/api/instance_service.go +++ b/server/api/instance_service.go @@ -140,6 +140,10 @@ func (s *APIService) getInstanceSettingByName(ctx context.Context, name string, var setting *storepb.InstanceAccessSetting setting, err = s.Store.GetInstanceAccessSetting(ctx) instanceSetting = &storepb.InstanceSetting{Key: instanceSettingKey, Value: &storepb.InstanceSetting_AccessSetting{AccessSetting: setting}} + case storepb.InstanceSettingKey_USAGE_LIMITS: + var setting *storepb.InstanceUsageLimitsSetting + setting, err = s.Store.GetInstanceUsageLimitsSetting(ctx) + instanceSetting = &storepb.InstanceSetting{Key: instanceSettingKey, Value: &storepb.InstanceSetting_UsageLimitsSetting{UsageLimitsSetting: setting}} default: return nil, status.Errorf(codes.InvalidArgument, "unsupported instance setting key: %v", instanceSettingKey) } @@ -147,9 +151,11 @@ func (s *APIService) getInstanceSettingByName(ctx context.Context, name string, return nil, status.Errorf(codes.Internal, "failed to get instance setting: %v", err) } - // Storage and notification settings contain credentials; restrict to admins only. + // Storage and notification settings contain credentials, and usage limits + // name users; restrict them to admins. if instanceSetting.Key == storepb.InstanceSettingKey_STORAGE || - instanceSetting.Key == storepb.InstanceSettingKey_NOTIFICATION { + instanceSetting.Key == storepb.InstanceSettingKey_NOTIFICATION || + instanceSetting.Key == storepb.InstanceSettingKey_USAGE_LIMITS { user, err := caller.currentUser(ctx, s) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) @@ -173,7 +179,10 @@ func (s *APIService) getInstanceSettingByName(ctx context.Context, name string, isAdminCaller = user.Role == store.RoleAdmin } - result := convertInstanceSettingFromStore(instanceSetting) + result, err := s.convertInstanceSettingResponse(ctx, instanceSetting) + if err != nil { + return nil, err + } if instanceSetting.Key == storepb.InstanceSettingKey_AI && !isAdminCaller { // Non-admin callers only need transcription.provider_id to gate the // editor's Transcribe button. Model / language / prompt are @@ -207,10 +216,6 @@ func (s *APIService) UpdateInstanceSetting(ctx context.Context, request *apipb.U return nil, status.Errorf(codes.InvalidArgument, "invalid instance setting name: %v", err) } settingKey := storepb.InstanceSettingKey(storepb.InstanceSettingKey_value[settingKeyString]) - if settingKey == storepb.InstanceSettingKey_USAGE_LIMITS { - // Usage limits are store only; the usageLimits RPCs are their only API. - return nil, status.Errorf(codes.InvalidArgument, "unsupported instance setting key: %v", settingKey) - } if s.Store.IsInstanceSettingDeploymentConfigured(settingKey) { return nil, status.Errorf(codes.FailedPrecondition, "instance setting %q is configured by the deployment", settingKeyString) } @@ -223,6 +228,13 @@ func (s *APIService) UpdateInstanceSetting(ctx context.Context, request *apipb.U } updateSetting := convertInstanceSettingToStore(request.InstanceSetting) + if updateSetting.Key == storepb.InstanceSettingKey_USAGE_LIMITS { + usageLimits, err := s.convertUsageLimitsSettingToStore(ctx, request.InstanceSetting.GetUsageLimitsSetting()) + if err != nil { + return nil, err + } + updateSetting.Value = &storepb.InstanceSetting_UsageLimitsSetting{UsageLimitsSetting: usageLimits} + } // Preserve write-only credential fields when the caller sends an empty value. // An empty string means "no change", not "clear the credential". @@ -269,10 +281,13 @@ func (s *APIService) UpdateInstanceSetting(ctx context.Context, request *apipb.U if errors.Is(err, store.ErrUnsafeAuthenticationConfiguration) { return nil, status.Error(codes.FailedPrecondition, err.Error()) } + if errors.Is(err, store.ErrInvalidUsageLimits) { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } return nil, status.Errorf(codes.Internal, "failed to upsert instance setting: %v", err) } - return convertInstanceSettingFromStore(instanceSetting), nil + return s.convertInstanceSettingResponse(ctx, instanceSetting) } // challengeProfile describes the configured challenge so the web app can diff --git a/server/api/instance_service_usage_limits.go b/server/api/instance_service_usage_limits.go new file mode 100644 index 0000000000000..59efa8a90893d --- /dev/null +++ b/server/api/instance_service_usage_limits.go @@ -0,0 +1,110 @@ +package api + +import ( + "context" + "maps" + "slices" + + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + apipb "github.com/usememos/memos/proto/gen/api" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +// convertInstanceSettingResponse converts a stored setting for the API. Usage +// limits name users, so they need the store to turn IDs into user names. +func (s *APIService) convertInstanceSettingResponse(ctx context.Context, setting *storepb.InstanceSetting) (*apipb.InstanceSetting, error) { + result := convertInstanceSettingFromStore(setting) + if setting.Key == storepb.InstanceSettingKey_USAGE_LIMITS { + usageLimits, err := s.convertUsageLimitsSettingFromStore(ctx, setting.GetUsageLimitsSetting()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert usage limits: %v", err) + } + result.Value = &apipb.InstanceSetting_UsageLimitsSetting_{UsageLimitsSetting: usageLimits} + } + return result, nil +} + +// convertUsageLimitsSettingFromStore converts the stored usage limits for the +// API, naming users by username. Users that no longer exist are left out, +// because an entry naming only missing users never matches anyone. +func (s *APIService) convertUsageLimitsSettingFromStore(ctx context.Context, setting *storepb.InstanceUsageLimitsSetting) (*apipb.InstanceSetting_UsageLimitsSetting, error) { + userIDs := []int32{} + for _, limit := range setting.GetLimits() { + userIDs = append(userIDs, limit.GetUserIds()...) + } + usernames, err := s.listUsernamesByID(ctx, userIDs) + if err != nil { + return nil, errors.Wrap(err, "failed to list users") + } + result := &apipb.InstanceSetting_UsageLimitsSetting{} + for _, limit := range setting.GetLimits() { + entry := &apipb.InstanceSetting_UsageLimitsSetting_UsageLimit{ + AllUsers: limit.GetAllUsers(), + MemoCount: limit.MemoCount, + MemoContentBytes: limit.MemoContentBytes, + StorageBytes: limit.StorageBytes, + UploadBytes: limit.UploadBytes, + } + for _, userID := range limit.GetUserIds() { + if username, ok := usernames[userID]; ok { + entry.Users = append(entry.Users, BuildUserName(username)) + } + } + result.Limits = append(result.Limits, entry) + } + return result, nil +} + +// convertUsageLimitsSettingToStore converts usage limits from the API for the +// store, resolving usernames to IDs. It returns an InvalidArgument status for +// a malformed user name and a NotFound status for a user that does not exist. +// The store validates the list when it is written. +func (s *APIService) convertUsageLimitsSettingToStore(ctx context.Context, setting *apipb.InstanceSetting_UsageLimitsSetting) (*storepb.InstanceUsageLimitsSetting, error) { + if setting == nil { + return nil, status.Errorf(codes.InvalidArgument, "usage limits setting is required") + } + // Request user name to username, parsed once. + usernameOf := map[string]string{} + for _, limit := range setting.GetLimits() { + for _, name := range limit.GetUsers() { + username, err := parseUsernameFromName(name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name %q: %v", name, err) + } + usernameOf[name] = username + } + } + userIDs := map[string]int32{} + if len(usernameOf) > 0 { + users, err := s.Store.ListUsers(ctx, &store.FindUser{UsernameList: slices.Collect(maps.Values(usernameOf))}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list users: %v", err) + } + for _, user := range users { + userIDs[user.Username] = user.ID + } + } + result := &storepb.InstanceUsageLimitsSetting{} + for _, limit := range setting.GetLimits() { + entry := &storepb.InstanceUsageLimitsSetting_UsageLimit{ + AllUsers: limit.GetAllUsers(), + MemoCount: limit.MemoCount, + MemoContentBytes: limit.MemoContentBytes, + StorageBytes: limit.StorageBytes, + UploadBytes: limit.UploadBytes, + } + for _, name := range limit.GetUsers() { + userID, ok := userIDs[usernameOf[name]] + if !ok { + return nil, status.Errorf(codes.NotFound, "user %q not found", name) + } + entry.UserIds = append(entry.UserIds, userID) + } + result.Limits = append(result.Limits, entry) + } + return result, nil +} diff --git a/server/api/instance_service_validation.go b/server/api/instance_service_validation.go index 8c7937e88dc3a..1e93e3201fb4b 100644 --- a/server/api/instance_service_validation.go +++ b/server/api/instance_service_validation.go @@ -29,6 +29,11 @@ func validateInstanceSetting(setting *apipb.InstanceSetting) error { return validateInstanceTagsSetting(setting.GetTagsSetting()) case storepb.InstanceSettingKey_ACCESS.String(): return validateInstanceAccessSetting(setting.GetAccessSetting()) + case storepb.InstanceSettingKey_USAGE_LIMITS.String(): + if setting.GetUsageLimitsSetting() == nil { + return errors.New("usage limits setting is required") + } + return nil default: return nil } diff --git a/server/api/memo_service.go b/server/api/memo_service.go index 09cd364b12b66..0f80470ab0801 100644 --- a/server/api/memo_service.go +++ b/server/api/memo_service.go @@ -46,7 +46,7 @@ func (s *APIService) CreateMemo(ctx context.Context, request *apipb.CreateMemoRe if err != nil { return nil, err } - if err := s.checkMemoQuota(ctx, user); err != nil { + if err := s.checkMemoQuota(ctx, user.ID); err != nil { return nil, err } diff --git a/server/api/memo_service_comments.go b/server/api/memo_service_comments.go index eff40b421a377..4248e4be372ab 100644 --- a/server/api/memo_service_comments.go +++ b/server/api/memo_service_comments.go @@ -62,7 +62,7 @@ func (s *APIService) CreateMemoComment(ctx context.Context, request *apipb.Creat return nil, err } // A comment is a memo row, so it counts against the memo limit too. - if err := s.checkMemoQuota(ctx, user); err != nil { + if err := s.checkMemoQuota(ctx, user.ID); err != nil { return nil, err } if err := s.createMemoWithMutation(ctx, user, prepared.memo, &relatedMemo.ID, prepared.attachments, prepared.requiredAttachmentIDs, prepared.referenceRelations); err != nil { diff --git a/server/api/ratelimit.go b/server/api/ratelimit.go index bec84986c406a..d45dd33638ede 100644 --- a/server/api/ratelimit.go +++ b/server/api/ratelimit.go @@ -74,8 +74,8 @@ func newRateLimitError(scope ratelimit.Scope, decision ratelimit.Decision) error // newQuotaExceededError builds the refusal for an action that would exceed a // usage limit: RESOURCE_EXHAUSTED with an ErrorInfo carrying the metric, the -// limit, the usage and the usage limit that applied, and a QuotaFailure naming -// the user. It carries no RetryInfo, because waiting does not help. +// limit and the usage, and a QuotaFailure naming the user. It carries no +// RetryInfo, because waiting does not help. func newQuotaExceededError(username string, refusal *usagelimit.Refusal) error { st := status.New(codes.ResourceExhausted, quotaExceededMessage) detailed, err := st.WithDetails( @@ -83,10 +83,9 @@ func newQuotaExceededError(username string, refusal *usagelimit.Refusal) error { Reason: reasonQuotaExceeded, Domain: errorInfoDomain, Metadata: map[string]string{ - "metric": refusal.Metric.String(), - "limit": strconv.FormatInt(refusal.Limit, 10), - "usage": strconv.FormatInt(refusal.Usage, 10), - "usage_limit": UsageLimitNamePrefix + refusal.Source, + "metric": string(refusal.Metric), + "limit": strconv.FormatInt(refusal.Limit, 10), + "usage": strconv.FormatInt(refusal.Usage, 10), }, }, &errdetails.QuotaFailure{ diff --git a/server/api/resource_name.go b/server/api/resource_name.go index aa369a97de3e0..21dce301e4956 100644 --- a/server/api/resource_name.go +++ b/server/api/resource_name.go @@ -27,7 +27,6 @@ const ( SpaceNamePrefix = "spaces/" SpaceMemberNamePrefix = "members/" SpaceInvitationNamePrefix = "invitations/" - UsageLimitNamePrefix = "usageLimits/" ) // GetNameParentTokens returns the tokens from a resource name. @@ -172,16 +171,6 @@ func ExtractIdentityProviderUIDFromName(name string) (string, error) { return tokens[0], nil } -// ExtractUsageLimitIDFromName returns the usage limit ID from a resource name. -// e.g., "usageLimits/default-memos" -> "default-memos". -func ExtractUsageLimitIDFromName(name string) (string, error) { - tokens, err := GetNameParentTokens(name, UsageLimitNamePrefix) - if err != nil { - return "", err - } - return tokens[0], nil -} - // ValidateAndGenerateUID validates a user-provided UID or generates a new one. // If provided is empty, a new shortuuid is generated. // If provided is non-empty, it is validated against identifier.UIDMatcher. diff --git a/server/api/test/memo_export_test.go b/server/api/test/memo_export_test.go index d62e89f43a0d8..f3e06a00f0a4c 100644 --- a/server/api/test/memo_export_test.go +++ b/server/api/test/memo_export_test.go @@ -611,8 +611,7 @@ func TestImportMemosFailureKeepsPreexistingAttachment(t *testing.T) { defer ts.Cleanup() t.Cleanup(ts.Service.CloseUploads) ctx := context.Background() - _, err := ts.Store.UpdateUsageLimit(ctx, "default-upload", 1<<20) - require.NoError(t, err) + setDefaultUploadLimit(ctx, t, ts.Store, 1<<20) user, err := ts.CreateRegularUser(ctx, "keeper") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, user.ID) diff --git a/server/api/test/usage_limit_service_test.go b/server/api/test/usage_limit_service_test.go index d86c1c82884bb..02e3ef5cedff2 100644 --- a/server/api/test/usage_limit_service_test.go +++ b/server/api/test/usage_limit_service_test.go @@ -19,14 +19,36 @@ import ( "github.com/usememos/memos/store" ) -func createUsageLimit(ctx context.Context, t *testing.T, ts *TestService, id string, metric apipb.UsageLimit_Metric, user string, value int64) *apipb.UsageLimit { +type usageLimitEntry = apipb.InstanceSetting_UsageLimitsSetting_UsageLimit + +const usageLimitsSettingName = "instance/settings/USAGE_LIMITS" + +func usageLimitsSetting(limits ...*usageLimitEntry) *apipb.InstanceSetting { + return &apipb.InstanceSetting{ + Name: usageLimitsSettingName, + Value: &apipb.InstanceSetting_UsageLimitsSetting_{UsageLimitsSetting: &apipb.InstanceSetting_UsageLimitsSetting{Limits: limits}}, + } +} + +// setUsageLimits replaces the usage limits through the API as ctx's user. +func setUsageLimits(ctx context.Context, t *testing.T, ts *TestService, limits ...*usageLimitEntry) *apipb.InstanceSetting_UsageLimitsSetting { + t.Helper() + setting, err := ts.Service.UpdateInstanceSetting(ctx, &apipb.UpdateInstanceSettingRequest{InstanceSetting: usageLimitsSetting(limits...)}) + require.NoError(t, err) + return setting.GetUsageLimitsSetting() +} + +// setDefaultUploadLimit stores one all-users entry with the given per-file +// limit directly in the store, for tests of other services. +func setDefaultUploadLimit(ctx context.Context, t *testing.T, s *store.Store, uploadBytes int64) { t.Helper() - limit, err := ts.Service.CreateUsageLimit(ctx, &apipb.CreateUsageLimitRequest{ - UsageLimitId: id, - UsageLimit: &apipb.UsageLimit{Metric: metric, User: user, Value: value}, + _, err := s.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ + Key: storepb.InstanceSettingKey_USAGE_LIMITS, + Value: &storepb.InstanceSetting_UsageLimitsSetting{UsageLimitsSetting: &storepb.InstanceUsageLimitsSetting{ + Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{{AllUsers: true, UploadBytes: &uploadBytes}}, + }}, }) require.NoError(t, err) - return limit } // requireQuotaExceeded checks the usage limit error contract and returns the @@ -60,7 +82,7 @@ func requireQuotaExceeded(t *testing.T, err error, metric, subject string) map[s return info.Metadata } -func TestUsageLimitServicePermissions(t *testing.T) { +func TestUsageLimitsSettingPermissions(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() @@ -69,7 +91,6 @@ func TestUsageLimitServicePermissions(t *testing.T) { user, err := ts.CreateRegularUser(ctx, "limits-user") require.NoError(t, err) adminCtx := ts.CreateUserContext(ctx, admin.ID) - limit := createUsageLimit(adminCtx, t, ts, "default-memos", apipb.UsageLimit_MEMO_COUNT, "", 10) for _, caller := range []struct { ctx context.Context @@ -78,29 +99,23 @@ func TestUsageLimitServicePermissions(t *testing.T) { {ctx: ts.CreateUserContext(ctx, user.ID), code: codes.PermissionDenied}, {ctx: ctx, code: codes.Unauthenticated}, } { - _, err := ts.Service.ListUsageLimits(caller.ctx, &apipb.ListUsageLimitsRequest{}) - require.Equal(t, caller.code, status.Code(err)) - _, err = ts.Service.GetUsageLimit(caller.ctx, &apipb.GetUsageLimitRequest{Name: limit.Name}) - require.Equal(t, caller.code, status.Code(err)) - _, err = ts.Service.CreateUsageLimit(caller.ctx, &apipb.CreateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Metric: apipb.UsageLimit_STORAGE_BYTES, Value: 1}}) - require.Equal(t, caller.code, status.Code(err)) - _, err = ts.Service.UpdateUsageLimit(caller.ctx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: limit.Name, Value: 1}}) - require.Equal(t, caller.code, status.Code(err)) - _, err = ts.Service.DeleteUsageLimit(caller.ctx, &apipb.DeleteUsageLimitRequest{Name: limit.Name}) + _, err := ts.Service.GetInstanceSetting(caller.ctx, &apipb.GetInstanceSettingRequest{Name: usageLimitsSettingName}) + require.Equal(t, caller.code, status.Code(err), "the entries name users, so only admins read them") + _, err = ts.Service.UpdateInstanceSetting(caller.ctx, &apipb.UpdateInstanceSettingRequest{InstanceSetting: usageLimitsSetting(&usageLimitEntry{AllUsers: true})}) require.Equal(t, caller.code, status.Code(err)) } - // The row never leaks through the instance settings API. - _, err = ts.Service.GetInstanceSetting(adminCtx, &apipb.GetInstanceSettingRequest{Name: "instance/settings/USAGE_LIMITS"}) - require.Equal(t, codes.InvalidArgument, status.Code(err)) - _, err = ts.Service.UpdateInstanceSetting(adminCtx, &apipb.UpdateInstanceSettingRequest{InstanceSetting: &apipb.InstanceSetting{Name: "instance/settings/USAGE_LIMITS"}}) - require.Equal(t, codes.InvalidArgument, status.Code(err)) - got, err := ts.Service.GetUsageLimit(adminCtx, &apipb.GetUsageLimitRequest{Name: limit.Name}) + setting, err := ts.Service.GetInstanceSetting(adminCtx, &apipb.GetInstanceSettingRequest{Name: usageLimitsSettingName}) require.NoError(t, err) - require.Equal(t, int64(10), got.Value, "a refused settings update leaves the limits unchanged") + limits := setting.GetUsageLimitsSetting().Limits + require.Len(t, limits, 1, "the built-in default is the only entry") + require.True(t, limits[0].AllUsers) + require.Equal(t, store.DefaultUploadBytes, limits[0].GetUploadBytes()) + require.Equal(t, store.DefaultMemoContentBytes, limits[0].GetMemoContentBytes()) + require.Nil(t, limits[0].MemoCount) } -func TestUsageLimitServiceCRUD(t *testing.T) { +func TestUsageLimitsSettingUpdate(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() @@ -108,124 +123,77 @@ func TestUsageLimitServiceCRUD(t *testing.T) { require.NoError(t, err) alice, err := ts.CreateRegularUser(ctx, "crud-alice") require.NoError(t, err) + bob, err := ts.CreateRegularUser(ctx, "crud-bob") + require.NoError(t, err) adminCtx := ts.CreateUserContext(ctx, admin.ID) - aliceName := api.BuildUserName(alice.Username) - - defaults, err := ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{}) - require.NoError(t, err) - require.Len(t, defaults.UsageLimits, 2) - require.Equal(t, "usageLimits/default-memo-content", defaults.UsageLimits[0].Name) - require.Equal(t, apipb.UsageLimit_MEMO_CONTENT_BYTES, defaults.UsageLimits[0].Metric) - require.Equal(t, store.DefaultMemoContentBytes, defaults.UsageLimits[0].Value) - require.Empty(t, defaults.UsageLimits[0].User) - require.Equal(t, "usageLimits/default-upload", defaults.UsageLimits[1].Name) - require.Equal(t, apipb.UsageLimit_UPLOAD_BYTES, defaults.UsageLimits[1].Metric) - require.Equal(t, store.DefaultUploadBytes, defaults.UsageLimits[1].Value) - require.Empty(t, defaults.UsageLimits[1].User) - - defaultStorage := createUsageLimit(adminCtx, t, ts, "default-storage", apipb.UsageLimit_STORAGE_BYTES, "", 1<<30) - require.Equal(t, "usageLimits/default-storage", defaultStorage.Name) - require.Empty(t, defaultStorage.User) - require.NotNil(t, defaultStorage.CreateTime) - aliceStorage := createUsageLimit(adminCtx, t, ts, "", apipb.UsageLimit_STORAGE_BYTES, aliceName, 5<<30) - require.Regexp(t, `^usageLimits/limit-[a-z2-7]{12}$`, aliceStorage.Name, "an ID is generated when none is given") - require.Equal(t, aliceName, aliceStorage.User) - createUsageLimit(adminCtx, t, ts, "default-memos", apipb.UsageLimit_MEMO_COUNT, "", 1000) - - for name, request := range map[string]*apipb.CreateUsageLimitRequest{ - "negative value": {UsageLimit: &apipb.UsageLimit{Metric: apipb.UsageLimit_MEMO_COUNT, User: aliceName, Value: -1}}, - "no metric": {UsageLimit: &apipb.UsageLimit{User: aliceName, Value: 1}}, - "bad user name": {UsageLimit: &apipb.UsageLimit{Metric: apipb.UsageLimit_MEMO_COUNT, User: "people/alice", Value: 1}}, - "bad ID": {UsageLimitId: "Alice_Memos", UsageLimit: &apipb.UsageLimit{Metric: apipb.UsageLimit_MEMO_COUNT, Value: 1}}, - "long ID": {UsageLimitId: "a-very-long-usage-limit-identifier-x", UsageLimit: &apipb.UsageLimit{Metric: apipb.UsageLimit_MEMO_COUNT, Value: 1}}, - "missing limit": {}, + aliceName, bobName := api.BuildUserName(alice.Username), api.BuildUserName(bob.Username) + + saved := setUsageLimits(adminCtx, t, ts, + &usageLimitEntry{Users: []string{aliceName, bobName}, StorageBytes: new(int64(5 << 30))}, + &usageLimitEntry{Users: []string{aliceName}, MemoCount: new(int64(0))}, + &usageLimitEntry{AllUsers: true, StorageBytes: new(int64(1 << 30)), MemoCount: new(int64(1000))}, + ) + require.Len(t, saved.Limits, 3) + require.Equal(t, []string{aliceName, bobName}, saved.Limits[0].Users) + require.Equal(t, int64(5<<30), saved.Limits[0].GetStorageBytes()) + require.Nil(t, saved.Limits[0].MemoCount, "unset stays unset") + require.Zero(t, saved.Limits[1].GetMemoCount()) + require.NotNil(t, saved.Limits[1].MemoCount, "an explicit zero stays explicit") + got, err := ts.Service.GetInstanceSetting(adminCtx, &apipb.GetInstanceSettingRequest{Name: usageLimitsSettingName}) + require.NoError(t, err) + require.Equal(t, saved.String(), got.GetUsageLimitsSetting().String(), "the update response is what a read returns") + + // Resolution follows the order: Alice's first entry is the pair, which has + // no memo limit, so her later memo entry is never reached. + usage, err := ts.Service.GetUserUsage(adminCtx, &apipb.GetUserUsageRequest{Name: aliceName + "/usage"}) + require.NoError(t, err) + require.Equal(t, int32(0), usage.LimitIndex) + require.Equal(t, int64(5<<30), usage.Limits.GetStorageBytes()) + require.Nil(t, usage.Limits.MemoCount) + usage, err = ts.Service.GetUserUsage(adminCtx, &apipb.GetUserUsageRequest{Name: api.BuildUserName(admin.Username) + "/usage"}) + require.NoError(t, err) + require.Equal(t, int32(2), usage.LimitIndex, "admins fall through to the default like everyone else") + require.Equal(t, int64(1000), usage.Limits.GetMemoCount()) + + for name, request := range map[string]*apipb.InstanceSetting{ + "missing setting": {Name: usageLimitsSettingName}, + "empty list": usageLimitsSetting(), + "no all-users entry": usageLimitsSetting(&usageLimitEntry{Users: []string{aliceName}}), + "all users not last": usageLimitsSetting(&usageLimitEntry{AllUsers: true}, &usageLimitEntry{Users: []string{aliceName}}), + "users and all users": usageLimitsSetting(&usageLimitEntry{AllUsers: true, Users: []string{aliceName}}), + "entry without users": usageLimitsSetting(&usageLimitEntry{}, &usageLimitEntry{AllUsers: true}), + "repeated user": usageLimitsSetting(&usageLimitEntry{Users: []string{aliceName, aliceName}}, &usageLimitEntry{AllUsers: true}), + "negative value": usageLimitsSetting(&usageLimitEntry{AllUsers: true, MemoCount: new(int64(-1))}), + "bad user name": usageLimitsSetting(&usageLimitEntry{Users: []string{"people/alice"}}, &usageLimitEntry{AllUsers: true}), } { - _, err := ts.Service.CreateUsageLimit(adminCtx, request) + _, err := ts.Service.UpdateInstanceSetting(adminCtx, &apipb.UpdateInstanceSettingRequest{InstanceSetting: request}) require.Equal(t, codes.InvalidArgument, status.Code(err), name) } - _, err = ts.Service.CreateUsageLimit(adminCtx, &apipb.CreateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Metric: apipb.UsageLimit_MEMO_COUNT, User: "users/nobody", Value: 1}}) - require.Equal(t, codes.NotFound, status.Code(err)) - _, err = ts.Service.CreateUsageLimit(adminCtx, &apipb.CreateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Metric: apipb.UsageLimit_STORAGE_BYTES, User: aliceName, Value: 1}}) - require.Equal(t, codes.AlreadyExists, status.Code(err), "one limit per metric and user") - _, err = ts.Service.CreateUsageLimit(adminCtx, &apipb.CreateUsageLimitRequest{UsageLimitId: "default-storage", UsageLimit: &apipb.UsageLimit{Metric: apipb.UsageLimit_UPLOAD_BYTES, Value: 1}}) - require.Equal(t, codes.AlreadyExists, status.Code(err), "IDs are unique") - - // Update: only value changes; metric and user are immutable. - updated, err := ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{ - UsageLimit: &apipb.UsageLimit{Name: aliceStorage.Name, Value: 6 << 30, Metric: apipb.UsageLimit_STORAGE_BYTES, User: aliceName}, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"value"}}, + _, err = ts.Service.UpdateInstanceSetting(adminCtx, &apipb.UpdateInstanceSettingRequest{ + InstanceSetting: usageLimitsSetting(&usageLimitEntry{Users: []string{"users/nobody"}}, &usageLimitEntry{AllUsers: true}), }) - require.NoError(t, err) - require.Equal(t, int64(6<<30), updated.Value) - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: aliceStorage.Name, Value: 1, Metric: apipb.UsageLimit_MEMO_COUNT}}) - require.Equal(t, codes.InvalidArgument, status.Code(err)) - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: defaultStorage.Name, Value: 1, User: aliceName}}) - require.Equal(t, codes.InvalidArgument, status.Code(err)) - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{ - UsageLimit: &apipb.UsageLimit{Name: defaultStorage.Name, Value: 1}, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"metric"}}, - }) - require.Equal(t, codes.InvalidArgument, status.Code(err)) - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: defaultStorage.Name, Value: -1}}) - require.Equal(t, codes.InvalidArgument, status.Code(err)) - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: "usageLimits/missing", Value: 1}}) require.Equal(t, codes.NotFound, status.Code(err)) - - // List, filter and page. Defaults come first. - list, err := ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{}) - require.NoError(t, err) - require.Len(t, list.UsageLimits, 5) - require.Empty(t, list.UsageLimits[0].User) - require.Empty(t, list.UsageLimits[1].User) - require.Empty(t, list.UsageLimits[2].User) - require.Empty(t, list.UsageLimits[3].User) - require.Equal(t, aliceName, list.UsageLimits[4].User) - filtered, err := ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{Filter: `user == "` + aliceName + `"`}) - require.NoError(t, err) - require.Len(t, filtered.UsageLimits, 1) - filtered, err = ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{Filter: `user == "" && metric == "STORAGE_BYTES"`}) - require.NoError(t, err) - require.Len(t, filtered.UsageLimits, 1) - require.Equal(t, defaultStorage.Name, filtered.UsageLimits[0].Name) - _, err = ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{Filter: `metric == "PLANS"`}) - require.Equal(t, codes.InvalidArgument, status.Code(err)) - _, err = ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{Filter: `user != ""`}) - require.Equal(t, codes.InvalidArgument, status.Code(err)) - page, err := ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{PageSize: 2}) + got, err = ts.Service.GetInstanceSetting(adminCtx, &apipb.GetInstanceSettingRequest{Name: usageLimitsSettingName}) require.NoError(t, err) - require.Len(t, page.UsageLimits, 2) - require.NotEmpty(t, page.NextPageToken) - page, err = ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{PageToken: page.NextPageToken}) - require.NoError(t, err) - require.Len(t, page.UsageLimits, 2) - require.NotEmpty(t, page.NextPageToken) - page, err = ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{PageToken: page.NextPageToken}) - require.NoError(t, err) - require.Len(t, page.UsageLimits, 1) - require.Empty(t, page.NextPageToken) + require.Equal(t, saved.String(), got.GetUsageLimitsSetting().String(), "a refused update leaves the limits unchanged") - // Renaming a user keeps their limits; deleting the user removes them. + // Renaming a user keeps their entries; deleting the user removes them. renamed := "crud-alice-renamed" _, err = ts.Store.UpdateUser(ctx, &store.UpdateUser{ID: alice.ID, Username: &renamed}) require.NoError(t, err) - got, err := ts.Service.GetUsageLimit(adminCtx, &apipb.GetUsageLimitRequest{Name: aliceStorage.Name}) + got, err = ts.Service.GetInstanceSetting(adminCtx, &apipb.GetInstanceSettingRequest{Name: usageLimitsSettingName}) require.NoError(t, err) - require.Equal(t, api.BuildUserName(renamed), got.User) + require.Equal(t, []string{api.BuildUserName(renamed), bobName}, got.GetUsageLimitsSetting().Limits[0].Users) _, err = ts.Store.DeleteUser(ctx, &store.DeleteUser{ID: alice.ID}) require.NoError(t, err) - _, err = ts.Service.GetUsageLimit(adminCtx, &apipb.GetUsageLimitRequest{Name: aliceStorage.Name}) - require.Equal(t, codes.NotFound, status.Code(err)) - - _, err = ts.Service.DeleteUsageLimit(adminCtx, &apipb.DeleteUsageLimitRequest{Name: defaultStorage.Name}) - require.NoError(t, err) - _, err = ts.Service.DeleteUsageLimit(adminCtx, &apipb.DeleteUsageLimitRequest{Name: defaultStorage.Name}) - require.Equal(t, codes.NotFound, status.Code(err)) - list, err = ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{}) + got, err = ts.Service.GetInstanceSetting(adminCtx, &apipb.GetInstanceSettingRequest{Name: usageLimitsSettingName}) require.NoError(t, err) - require.Len(t, list.UsageLimits, 3) + require.Len(t, got.GetUsageLimitsSetting().Limits, 2) + require.Equal(t, []string{bobName}, got.GetUsageLimitsSetting().Limits[0].Users) + require.True(t, got.GetUsageLimitsSetting().Limits[1].AllUsers) } -func TestUsageLimitServiceDeploymentFile(t *testing.T) { +func TestUsageLimitsDeploymentFile(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() @@ -235,17 +203,14 @@ func TestUsageLimitServiceDeploymentFile(t *testing.T) { dir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(dir, "memos-instance-setting-usage-limits.json"), - []byte(`{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"uid":"default-memos","metric":"MEMO_COUNT","allUsers":{},"value":"1"}]}}`), 0o600)) + []byte(`{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"allUsers":true,"memoCount":"1"}]}}`), 0o600)) require.NoError(t, ts.Store.LoadDeploymentConfigurationDir(ctx, dir)) - list, err := ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{}) + got, err := ts.Service.GetInstanceSetting(adminCtx, &apipb.GetInstanceSettingRequest{Name: usageLimitsSettingName}) require.NoError(t, err) - require.Len(t, list.UsageLimits, 1) - _, err = ts.Service.CreateUsageLimit(adminCtx, &apipb.CreateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Metric: apipb.UsageLimit_STORAGE_BYTES, Value: 1}}) - require.Equal(t, codes.FailedPrecondition, status.Code(err)) - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: "usageLimits/default-memos", Value: 5}}) - require.Equal(t, codes.FailedPrecondition, status.Code(err)) - _, err = ts.Service.DeleteUsageLimit(adminCtx, &apipb.DeleteUsageLimitRequest{Name: "usageLimits/default-memos"}) + require.Len(t, got.GetUsageLimitsSetting().Limits, 1) + require.Equal(t, int64(1), got.GetUsageLimitsSetting().Limits[0].GetMemoCount()) + _, err = ts.Service.UpdateInstanceSetting(adminCtx, &apipb.UpdateInstanceSettingRequest{InstanceSetting: usageLimitsSetting(&usageLimitEntry{AllUsers: true})}) require.Equal(t, codes.FailedPrecondition, status.Code(err)) // The file-provided limit is enforced. @@ -265,34 +230,34 @@ func TestMemoLimitEnforcement(t *testing.T) { require.NoError(t, err) adminCtx := ts.CreateUserContext(ctx, admin.ID) userCtx := ts.CreateUserContext(ctx, user.ID) + userName := api.BuildUserName(user.Username) createMemo := func(ctx context.Context, content string) (*apipb.Memo, error) { return ts.Service.CreateMemo(ctx, &apipb.CreateMemoRequest{Memo: &apipb.Memo{Content: content, Visibility: apipb.Visibility_PUBLIC}}) } - // With no limits, nothing changes. + // With the built-in default, memos are not counted. first, err := createMemo(userCtx, "first") require.NoError(t, err) - usage, err := ts.Service.GetUserUsage(userCtx, &apipb.GetUserUsageRequest{Name: api.BuildUserName(user.Username) + "/usage"}) + usage, err := ts.Service.GetUserUsage(userCtx, &apipb.GetUserUsageRequest{Name: userName + "/usage"}) require.NoError(t, err) require.Equal(t, int64(1), usage.MemoCount) - require.Zero(t, usage.MemoCountLimit.Value) - require.Empty(t, usage.MemoCountLimit.UsageLimit) - require.Equal(t, store.DefaultUploadBytes, usage.UploadBytesLimit.Value) - require.Equal(t, "usageLimits/default-upload", usage.UploadBytesLimit.UsageLimit) + require.Nil(t, usage.Limits.MemoCount) + require.Equal(t, store.DefaultUploadBytes, usage.Limits.GetUploadBytes()) + require.Equal(t, int32(0), usage.LimitIndex) - createUsageLimit(adminCtx, t, ts, "default-memos", apipb.UsageLimit_MEMO_COUNT, "", 2) + setUsageLimits(adminCtx, t, ts, &usageLimitEntry{AllUsers: true, MemoCount: new(int64(2))}) second, err := createMemo(userCtx, "second") require.NoError(t, err) _, err = createMemo(userCtx, "third") - metadata := requireQuotaExceeded(t, err, "MEMO_COUNT", api.BuildUserName(user.Username)) - require.Equal(t, map[string]string{"metric": "MEMO_COUNT", "limit": "2", "usage": "2", "usage_limit": "usageLimits/default-memos"}, metadata) + metadata := requireQuotaExceeded(t, err, "MEMO_COUNT", userName) + require.Equal(t, map[string]string{"metric": "MEMO_COUNT", "limit": "2", "usage": "2"}, metadata) count, err := ts.Store.CountUserMemos(ctx, user.ID) require.NoError(t, err) require.Equal(t, int64(2), count, "a refused create writes no memo") // A comment is a memo row: it is refused at the limit and writes nothing. _, err = ts.Service.CreateMemoComment(userCtx, &apipb.CreateMemoCommentRequest{Parent: first.Name, Comment: &apipb.Memo{Content: "a comment", Visibility: apipb.Visibility_PUBLIC}}) - requireQuotaExceeded(t, err, "MEMO_COUNT", api.BuildUserName(user.Username)) + requireQuotaExceeded(t, err, "MEMO_COUNT", userName) count, err = ts.Store.CountUserMemos(ctx, user.ID) require.NoError(t, err) require.Equal(t, int64(2), count) @@ -309,8 +274,12 @@ func TestMemoLimitEnforcement(t *testing.T) { _, err = createMemo(userCtx, "fits again") require.NoError(t, err) - // A user's own limit replaces the default, and admins are limited too. - createUsageLimit(adminCtx, t, ts, "user-memos", apipb.UsageLimit_MEMO_COUNT, api.BuildUserName(user.Username), 0) + // An entry naming the user comes first and is the whole limit set, so an + // explicit zero makes the user unlimited. Admins are limited too. + setUsageLimits(adminCtx, t, ts, + &usageLimitEntry{Users: []string{userName}, MemoCount: new(int64(0))}, + &usageLimitEntry{AllUsers: true, MemoCount: new(int64(2))}, + ) _, err = createMemo(userCtx, "unlimited now") require.NoError(t, err) _, err = createMemo(adminCtx, "admin one") @@ -320,18 +289,14 @@ func TestMemoLimitEnforcement(t *testing.T) { _, err = createMemo(adminCtx, "admin three") requireQuotaExceeded(t, err, "MEMO_COUNT", api.BuildUserName(admin.Username)) - usage, err = ts.Service.GetUserUsage(userCtx, &apipb.GetUserUsageRequest{Name: api.BuildUserName(user.Username) + "/usage"}) + usage, err = ts.Service.GetUserUsage(userCtx, &apipb.GetUserUsageRequest{Name: userName + "/usage"}) require.NoError(t, err) - require.Equal(t, "usageLimits/user-memos", usage.MemoCountLimit.UsageLimit) - require.Zero(t, usage.MemoCountLimit.Value) + require.Equal(t, int32(0), usage.LimitIndex) + require.Zero(t, usage.Limits.GetMemoCount()) + require.NotNil(t, usage.Limits.MemoCount) - // Deleting every limit restores the previous behavior. - limits, err := ts.Service.ListUsageLimits(adminCtx, &apipb.ListUsageLimitsRequest{}) - require.NoError(t, err) - for _, limit := range limits.UsageLimits { - _, err := ts.Service.DeleteUsageLimit(adminCtx, &apipb.DeleteUsageLimitRequest{Name: limit.Name}) - require.NoError(t, err) - } + // A default without a memo limit restores the previous behavior. + setUsageLimits(adminCtx, t, ts, &usageLimitEntry{AllUsers: true}) _, err = createMemo(adminCtx, "admin three") require.NoError(t, err) } @@ -351,8 +316,7 @@ func TestMemoContentLimitEnforcement(t *testing.T) { usage, err := ts.Service.GetUserUsage(userCtx, &apipb.GetUserUsageRequest{Name: subject + "/usage"}) require.NoError(t, err) - require.Equal(t, store.DefaultMemoContentBytes, usage.MemoContentBytesLimit.Value) - require.Equal(t, "usageLimits/default-memo-content", usage.MemoContentBytesLimit.UsageLimit) + require.Equal(t, store.DefaultMemoContentBytes, usage.Limits.GetMemoContentBytes()) create := func(content string) (*apipb.Memo, error) { return ts.Service.CreateMemo(userCtx, &apipb.CreateMemoRequest{Memo: &apipb.Memo{Content: content, Visibility: apipb.Visibility_PRIVATE}}) } @@ -360,8 +324,7 @@ func TestMemoContentLimitEnforcement(t *testing.T) { require.NoError(t, err) _, err = create(strings.Repeat("a", 8193)) metadata := requireQuotaExceeded(t, err, "MEMO_CONTENT_BYTES", subject) - require.Equal(t, "8193", metadata["usage"]) - require.Equal(t, "usageLimits/default-memo-content", metadata["usage_limit"]) + require.Equal(t, map[string]string{"metric": "MEMO_CONTENT_BYTES", "limit": "8192", "usage": "8193"}, metadata) _, err = ts.Service.UpdateMemo(userCtx, &apipb.UpdateMemoRequest{ Memo: &apipb.Memo{Name: memo.Name, Content: strings.Repeat("a", 8193)}, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"content"}}, @@ -373,7 +336,10 @@ func TestMemoContentLimitEnforcement(t *testing.T) { _, err = ts.Service.CreateMemoComment(userCtx, &apipb.CreateMemoCommentRequest{Parent: memo.Name, Comment: &apipb.Memo{Content: strings.Repeat("a", 8193), Visibility: apipb.Visibility_PRIVATE}}) requireQuotaExceeded(t, err, "MEMO_CONTENT_BYTES", subject) - own := createUsageLimit(adminCtx, t, ts, "user-content", apipb.UsageLimit_MEMO_CONTENT_BYTES, subject, 6) + setUsageLimits(adminCtx, t, ts, + &usageLimitEntry{Users: []string{subject}, MemoContentBytes: new(int64(6))}, + &usageLimitEntry{AllUsers: true, MemoContentBytes: new(int64(8192))}, + ) unicodeMemo, err := create("中文") require.NoError(t, err) _, err = create("中文字") @@ -385,7 +351,6 @@ func TestMemoContentLimitEnforcement(t *testing.T) { }) metadata = requireQuotaExceeded(t, err, "MEMO_CONTENT_BYTES", subject) require.Equal(t, "6", metadata["limit"]) - require.Equal(t, "usageLimits/user-content", metadata["usage_limit"]) archive := buildImportArchive(t, user.Username, nil) importArchive := func() *apipb.ImportMemosReport { @@ -399,8 +364,10 @@ func TestMemoContentLimitEnforcement(t *testing.T) { report := importArchive() require.Equal(t, int32(1), report.Failed) require.Zero(t, report.Created) - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: own.Name, Value: 0}}) - require.NoError(t, err) + setUsageLimits(adminCtx, t, ts, + &usageLimitEntry{Users: []string{subject}, MemoContentBytes: new(int64(0))}, + &usageLimitEntry{AllUsers: true, MemoContentBytes: new(int64(8192))}, + ) _, err = create(strings.Repeat("a", 65536)) require.NoError(t, err, "explicit zero disables the content limit") report = importArchive() @@ -439,9 +406,7 @@ func TestUploadLimitEnforcement(t *testing.T) { return err } - createUsageLimit(adminCtx, t, ts, "default-storage", apipb.UsageLimit_STORAGE_BYTES, "", 100) - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: "usageLimits/default-upload", Value: 60}}) - require.NoError(t, err) + setUsageLimits(adminCtx, t, ts, &usageLimitEntry{AllUsers: true, StorageBytes: new(int64(100)), UploadBytes: new(int64(60))}) require.NoError(t, create(60)) metadata := requireQuotaExceeded(t, create(61), "UPLOAD_BYTES", subject) @@ -460,12 +425,11 @@ func TestUploadLimitEnforcement(t *testing.T) { usage, err := ts.Service.GetUserUsage(userCtx, &apipb.GetUserUsageRequest{Name: subject + "/usage"}) require.NoError(t, err) require.Equal(t, int64(100), usage.StorageBytes) - require.Equal(t, "usageLimits/default-storage", usage.StorageBytesLimit.UsageLimit) - require.Equal(t, int64(60), usage.UploadBytesLimit.Value) + require.Equal(t, int64(100), usage.Limits.GetStorageBytes()) + require.Equal(t, int64(60), usage.Limits.GetUploadBytes()) // A chunked upload is refused at start before any session or file exists. - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: "usageLimits/default-storage", Value: 200}}) - require.NoError(t, err) + setUsageLimits(adminCtx, t, ts, &usageLimitEntry{AllUsers: true, StorageBytes: new(int64(200)), UploadBytes: new(int64(60))}) _, err = ts.Service.UploadAttachment(userCtx, &apipb.UploadAttachmentRequest{Upload: &apipb.UploadAttachmentRequest_Spec{Spec: &apipb.UploadAttachmentSpec{ Attachment: &apipb.Attachment{Filename: "big.bin", Type: "application/octet-stream"}, TotalSize: 120, }}}) @@ -490,19 +454,18 @@ func TestUploadLimitEnforcement(t *testing.T) { }) require.Equal(t, codes.NotFound, status.Code(err), "a refused upload is revoked") - // A user's own per-file limit replaces the default in either direction. - _, err = ts.Service.UpdateUsageLimit(adminCtx, &apipb.UpdateUsageLimitRequest{UsageLimit: &apipb.UsageLimit{Name: "usageLimits/default-storage", Value: 0}}) - require.NoError(t, err) - createUsageLimit(adminCtx, t, ts, "user-upload", apipb.UsageLimit_UPLOAD_BYTES, subject, 1<<20) + // An entry naming the user is the whole limit set: a bigger per-file limit + // and no storage limit, whatever the default says. + setUsageLimits(adminCtx, t, ts, + &usageLimitEntry{Users: []string{subject}, UploadBytes: new(int64(1 << 20))}, + &usageLimitEntry{AllUsers: true, StorageBytes: new(int64(200)), UploadBytes: new(int64(60))}, + ) require.NoError(t, create(1<<20)) metadata = requireQuotaExceeded(t, create(1<<20+1), "UPLOAD_BYTES", subject) - require.Equal(t, "usageLimits/user-upload", metadata["usage_limit"]) + require.Equal(t, "1048576", metadata["limit"]) // Without any per-file limit, a file of any size is accepted. - _, err = ts.Service.DeleteUsageLimit(adminCtx, &apipb.DeleteUsageLimitRequest{Name: "usageLimits/user-upload"}) - require.NoError(t, err) - _, err = ts.Service.DeleteUsageLimit(adminCtx, &apipb.DeleteUsageLimitRequest{Name: "usageLimits/default-upload"}) - require.NoError(t, err) + setUsageLimits(adminCtx, t, ts, &usageLimitEntry{AllUsers: true}) require.NoError(t, create(33<<20)) } diff --git a/server/api/usage_limit.go b/server/api/usage_limit.go new file mode 100644 index 0000000000000..5276248dd7b89 --- /dev/null +++ b/server/api/usage_limit.go @@ -0,0 +1,51 @@ +package api + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/usememos/memos/core/usagelimit" + "github.com/usememos/memos/store" +) + +// checkMemoQuota refuses a new memo when the user has reached the memo limit. +func (s *APIService) checkMemoQuota(ctx context.Context, userID int32) error { + return s.quotaError(ctx, userID, usagelimit.CheckMemoCreate(ctx, s.Store, userID)) +} + +// checkMemoContentQuota refuses content larger than the memo owner's limit. +func (s *APIService) checkMemoContentQuota(ctx context.Context, userID int32, content string) error { + return s.quotaError(ctx, userID, usagelimit.CheckMemoContentSize(ctx, s.Store, userID, int64(len(content)))) +} + +// checkUploadQuota refuses an upload of size bytes that would break the +// per-file or storage limit of the user. +func (s *APIService) checkUploadQuota(ctx context.Context, userID int32, size int64) error { + return s.quotaError(ctx, userID, usagelimit.CheckUpload(ctx, s.Store, userID, size)) +} + +// checkFileSizeQuota refuses a file of size bytes that is larger than the +// per-file limit of the user. It runs no count or sum query. +func (s *APIService) checkFileSizeQuota(ctx context.Context, userID int32, size int64) error { + return s.quotaError(ctx, userID, usagelimit.CheckFileSize(ctx, s.Store, userID, size)) +} + +// quotaError maps a usage limit check result to a service error, naming the +// user in a refusal. A failed check refuses the request: failing open would +// let a database problem switch the limits off. +func (s *APIService) quotaError(ctx context.Context, userID int32, err error) error { + if err == nil { + return nil + } + refusal := usagelimit.AsRefusal(err) + if refusal == nil { + return status.Errorf(codes.Internal, "failed to check usage limits: %v", err) + } + username := "" + if user, userErr := s.Store.GetUser(ctx, &store.FindUser{ID: &userID}); userErr == nil && user != nil { + username = user.Username + } + return newQuotaExceededError(username, refusal) +} diff --git a/server/api/usage_limit_service.go b/server/api/usage_limit_service.go deleted file mode 100644 index 1df2bb6879410..0000000000000 --- a/server/api/usage_limit_service.go +++ /dev/null @@ -1,433 +0,0 @@ -package api - -import ( - "context" - "crypto/rand" - "slices" - "strings" - - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/ast" - "github.com/pkg/errors" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/emptypb" - - "github.com/usememos/memos/core/usagelimit" - apipb "github.com/usememos/memos/proto/gen/api" - storepb "github.com/usememos/memos/proto/gen/store" - "github.com/usememos/memos/store" -) - -// ListUsageLimits lists the usage limits. Admin only. -func (s *APIService) ListUsageLimits(ctx context.Context, request *apipb.ListUsageLimitsRequest) (*apipb.ListUsageLimitsResponse, error) { - if _, err := s.requireUsageLimitAdmin(ctx); err != nil { - return nil, err - } - filter, err := parseUsageLimitFilter(request.Filter) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) - } - if filter.username != nil && *filter.username != "" { - user, err := s.Store.GetUser(ctx, &store.FindUser{Username: filter.username}) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) - } - if user == nil { - return &apipb.ListUsageLimitsResponse{}, nil - } - filter.userID = &user.ID - } else if filter.username != nil { - everyUser := int32(0) - filter.userID = &everyUser - } - - var limit, offset int - if request.PageToken != "" { - var pageToken apipb.PageToken - if err := unmarshalPageToken(request.PageToken, &pageToken); err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err) - } - limit = normalizePageSize(pageToken.Limit) - offset = max(int(pageToken.Offset), 0) - } else { - limit = normalizePageSize(request.PageSize) - } - - limits, usernames, err := s.listVisibleUsageLimits(ctx) - if err != nil { - return nil, err - } - limits = slices.DeleteFunc(limits, func(limit *storepb.InstanceUsageLimitsSetting_UsageLimit) bool { - return (filter.userID != nil && limit.GetUserId() != *filter.userID) || (filter.metric != nil && limit.Metric != *filter.metric) - }) - // Defaults first, then by user and metric, so pages are stable. - slices.SortStableFunc(limits, func(a, b *storepb.InstanceUsageLimitsSetting_UsageLimit) int { - if a.GetUserId() != b.GetUserId() { - if a.GetUserId() == 0 || b.GetUserId() == 0 { - return int(min(a.GetUserId(), 1)) - int(min(b.GetUserId(), 1)) - } - return strings.Compare(usernames[a.GetUserId()], usernames[b.GetUserId()]) - } - return int(a.Metric) - int(b.Metric) - }) - - response := &apipb.ListUsageLimitsResponse{} - if offset < len(limits) { - end := min(offset+limit, len(limits)) - for _, item := range limits[offset:end] { - response.UsageLimits = append(response.UsageLimits, convertUsageLimitFromStore(item, usernames[item.GetUserId()])) - } - if end < len(limits) { - response.NextPageToken, err = getPageToken(limit, end) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get next page token: %v", err) - } - } - } - return response, nil -} - -// GetUsageLimit gets a usage limit. Admin only. -func (s *APIService) GetUsageLimit(ctx context.Context, request *apipb.GetUsageLimitRequest) (*apipb.UsageLimit, error) { - if _, err := s.requireUsageLimitAdmin(ctx); err != nil { - return nil, err - } - uid, err := ExtractUsageLimitIDFromName(request.Name) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid usage limit name: %v", err) - } - limits, usernames, err := s.listVisibleUsageLimits(ctx) - if err != nil { - return nil, err - } - for _, limit := range limits { - if limit.Uid == uid { - return convertUsageLimitFromStore(limit, usernames[limit.GetUserId()]), nil - } - } - return nil, status.Errorf(codes.NotFound, "usage limit not found") -} - -// CreateUsageLimit creates a usage limit. Admin only. -func (s *APIService) CreateUsageLimit(ctx context.Context, request *apipb.CreateUsageLimitRequest) (*apipb.UsageLimit, error) { - if _, err := s.requireUsageLimitAdmin(ctx); err != nil { - return nil, err - } - if request.UsageLimit == nil { - return nil, status.Errorf(codes.InvalidArgument, "usage_limit is required") - } - uid, err := validateAndGenerateUsageLimitID(request.UsageLimitId) - if err != nil { - return nil, err - } - metric := storepb.InstanceUsageLimitsSetting_Metric(request.UsageLimit.Metric) - if !store.IsValidUsageLimitMetric(metric) { - return nil, status.Errorf(codes.InvalidArgument, "metric must be MEMO_COUNT, STORAGE_BYTES, UPLOAD_BYTES, or MEMO_CONTENT_BYTES") - } - if request.UsageLimit.Value < 0 { - return nil, status.Errorf(codes.InvalidArgument, "value must not be negative") - } - var user *store.User - if request.UsageLimit.User != "" { - user, err = s.resolveUsageLimitUser(ctx, request.UsageLimit.User) - if err != nil { - return nil, err - } - } - create := &storepb.InstanceUsageLimitsSetting_UsageLimit{ - Uid: uid, - Metric: metric, - Value: request.UsageLimit.Value, - Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_AllUsers{AllUsers: &emptypb.Empty{}}, - } - username := "" - if user != nil { - create.Target = &storepb.InstanceUsageLimitsSetting_UsageLimit_UserId{UserId: user.ID} - username = user.Username - } - created, err := s.Store.CreateUsageLimit(ctx, create) - if err != nil { - return nil, usageLimitStoreError(err) - } - return convertUsageLimitFromStore(created, username), nil -} - -// UpdateUsageLimit updates the value of a usage limit. Admin only. -func (s *APIService) UpdateUsageLimit(ctx context.Context, request *apipb.UpdateUsageLimitRequest) (*apipb.UsageLimit, error) { - if _, err := s.requireUsageLimitAdmin(ctx); err != nil { - return nil, err - } - if request.UsageLimit == nil { - return nil, status.Errorf(codes.InvalidArgument, "usage_limit is required") - } - uid, err := ExtractUsageLimitIDFromName(request.UsageLimit.Name) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid usage limit name: %v", err) - } - for _, path := range request.UpdateMask.GetPaths() { - if path != "value" { - return nil, status.Errorf(codes.InvalidArgument, "only value can be updated; %q is immutable or unknown", path) - } - } - if request.UsageLimit.Value < 0 { - return nil, status.Errorf(codes.InvalidArgument, "value must not be negative") - } - limits, usernames, err := s.listVisibleUsageLimits(ctx) - if err != nil { - return nil, err - } - index := slices.IndexFunc(limits, func(limit *storepb.InstanceUsageLimitsSetting_UsageLimit) bool { return limit.Uid == uid }) - if index < 0 { - return nil, status.Errorf(codes.NotFound, "usage limit not found") - } - // metric and user are immutable. A request may echo them back unchanged. - current := limits[index] - if request.UsageLimit.Metric != apipb.UsageLimit_METRIC_UNSPECIFIED && storepb.InstanceUsageLimitsSetting_Metric(request.UsageLimit.Metric) != current.Metric { - return nil, status.Errorf(codes.InvalidArgument, "metric is immutable") - } - if request.UsageLimit.User != "" && request.UsageLimit.User != usageLimitUserName(usernames[current.GetUserId()]) { - return nil, status.Errorf(codes.InvalidArgument, "user is immutable") - } - updated, err := s.Store.UpdateUsageLimit(ctx, uid, request.UsageLimit.Value) - if err != nil { - return nil, usageLimitStoreError(err) - } - return convertUsageLimitFromStore(updated, usernames[updated.GetUserId()]), nil -} - -// DeleteUsageLimit deletes a usage limit. Admin only. -func (s *APIService) DeleteUsageLimit(ctx context.Context, request *apipb.DeleteUsageLimitRequest) (*emptypb.Empty, error) { - if _, err := s.requireUsageLimitAdmin(ctx); err != nil { - return nil, err - } - uid, err := ExtractUsageLimitIDFromName(request.Name) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid usage limit name: %v", err) - } - if err := s.Store.DeleteUsageLimit(ctx, uid); err != nil { - return nil, usageLimitStoreError(err) - } - return &emptypb.Empty{}, nil -} - -func (s *APIService) requireUsageLimitAdmin(ctx context.Context) (*store.User, 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") - } - return user, nil -} - -// listVisibleUsageLimits returns the stored usage limits, without those of -// users that no longer exist, and the usernames of the users they name. -func (s *APIService) listVisibleUsageLimits(ctx context.Context) ([]*storepb.InstanceUsageLimitsSetting_UsageLimit, map[int32]string, error) { - setting, err := s.Store.GetInstanceUsageLimitsSetting(ctx) - if err != nil { - return nil, nil, status.Errorf(codes.Internal, "failed to get usage limits: %v", err) - } - userIDs := []int32{} - for _, limit := range setting.GetLimits() { - if limit.GetUserId() != 0 && !slices.Contains(userIDs, limit.GetUserId()) { - userIDs = append(userIDs, limit.GetUserId()) - } - } - usernames := map[int32]string{} - if len(userIDs) > 0 { - users, err := s.Store.ListUsers(ctx, &store.FindUser{IDList: userIDs}) - if err != nil { - return nil, nil, status.Errorf(codes.Internal, "failed to list users: %v", err) - } - for _, user := range users { - usernames[user.ID] = user.Username - } - } - limits := make([]*storepb.InstanceUsageLimitsSetting_UsageLimit, 0, len(setting.GetLimits())) - for _, limit := range setting.GetLimits() { - if _, ok := usernames[limit.GetUserId()]; limit.GetUserId() != 0 && !ok { - continue - } - limits = append(limits, limit) - } - return limits, usernames, nil -} - -func (s *APIService) resolveUsageLimitUser(ctx context.Context, name string) (*store.User, error) { - username, err := parseUsernameFromName(name) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) - } - user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username}) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) - } - if user == nil { - return nil, status.Errorf(codes.NotFound, "user not found") - } - return user, nil -} - -func usageLimitStoreError(err error) error { - switch { - case errors.Is(err, store.ErrUsageLimitNotFound): - return status.Errorf(codes.NotFound, "usage limit not found") - case errors.Is(err, store.ErrUsageLimitAlreadyExists): - return status.Error(codes.AlreadyExists, err.Error()) - case errors.Is(err, store.ErrUsageLimitsDeploymentConfigured): - return status.Errorf(codes.FailedPrecondition, "usage limits are configured by the deployment") - case errors.Is(err, store.ErrUsageLimitWriteConflict): - return status.Errorf(codes.Aborted, "usage limits changed concurrently; retry") - case errors.Is(err, store.ErrInvalidUsageLimit): - return status.Error(codes.InvalidArgument, err.Error()) - default: - return status.Errorf(codes.Internal, "failed to write usage limits: %v", err) - } -} - -// validateAndGenerateUsageLimitID validates a client-chosen usage limit ID, -// or generates one. -func validateAndGenerateUsageLimitID(provided string) (string, error) { - if provided == "" { - return "limit-" + strings.ToLower(rand.Text()[:12]), nil - } - if len(provided) > 32 || !store.UsageLimitUIDMatcher.MatchString(provided) { - return "", status.Errorf(codes.InvalidArgument, "usage_limit_id must be lowercase kebab case, start with a letter, and have at most 32 characters") - } - return provided, nil -} - -// checkMemoQuota refuses a new memo when the user has reached the memo limit. -func (s *APIService) checkMemoQuota(ctx context.Context, user *store.User) error { - return usageLimitCheckError(user.Username, usagelimit.CheckMemoCreate(ctx, s.Store, user.ID)) -} - -// checkMemoContentQuota refuses content larger than the memo owner's limit. -func (s *APIService) checkMemoContentQuota(ctx context.Context, userID int32, content string) error { - return s.userQuotaError(ctx, userID, usagelimit.CheckMemoContentSize(ctx, s.Store, userID, int64(len(content)))) -} - -// checkUploadQuota refuses an upload of size bytes that would break the -// per-file or storage limit of the user. -func (s *APIService) checkUploadQuota(ctx context.Context, userID int32, size int64) error { - return s.userQuotaError(ctx, userID, usagelimit.CheckUpload(ctx, s.Store, userID, size)) -} - -// checkFileSizeQuota refuses a file of size bytes that is larger than the -// per-file limit of the user. It runs no count or sum query. -func (s *APIService) checkFileSizeQuota(ctx context.Context, userID int32, size int64) error { - return s.userQuotaError(ctx, userID, usagelimit.CheckFileSize(ctx, s.Store, userID, size)) -} - -// userQuotaError maps a usage limit check result to a service error, naming the -// user in a refusal. -func (s *APIService) userQuotaError(ctx context.Context, userID int32, err error) error { - if err == nil { - return nil - } - username := "" - if usagelimit.AsRefusal(err) != nil { - user, userErr := s.Store.GetUser(ctx, &store.FindUser{ID: &userID}) - if userErr == nil && user != nil { - username = user.Username - } - } - return usageLimitCheckError(username, err) -} - -// usageLimitCheckError maps a usage limit check result to a service error. -// A failed check refuses the request: failing open would let a database -// problem switch the limits off. -func usageLimitCheckError(username string, err error) error { - if err == nil { - return nil - } - if refusal := usagelimit.AsRefusal(err); refusal != nil { - return newQuotaExceededError(username, refusal) - } - return status.Errorf(codes.Internal, "failed to check usage limits: %v", err) -} - -// usageLimitFilter is a parsed ListUsageLimits filter. A nil field matches -// every value. -type usageLimitFilter struct { - // username is "" for the defaults for every user. - username *string - userID *int32 - metric *storepb.InstanceUsageLimitsSetting_Metric -} - -// parseUsageLimitFilter parses `user == "users/{user}"`, `user == ""`, and -// `metric == "STORAGE_BYTES"` comparisons joined by `&&`. -func parseUsageLimitFilter(filter string) (*usageLimitFilter, error) { - result := &usageLimitFilter{} - filter = strings.TrimSpace(filter) - if filter == "" { - return result, nil - } - env, err := cel.NewEnv(cel.Variable("user", cel.StringType), cel.Variable("metric", cel.StringType)) - if err != nil { - return nil, errors.Wrap(err, "failed to create CEL environment") - } - compiled, issues := env.Compile(filter) - if issues != nil && issues.Err() != nil { - return nil, errors.Wrapf(issues.Err(), "invalid filter expression: %s", filter) - } - if err := result.add(compiled.NativeRep().Expr()); err != nil { - return nil, err - } - return result, nil -} - -func (f *usageLimitFilter) add(expr ast.Expr) error { - if expr.Kind() != ast.CallKind { - return errors.New("filter must compare user or metric with a string") - } - call := expr.AsCall() - args := call.Args() - switch call.FunctionName() { - case "_&&_": - for _, arg := range args { - if err := f.add(arg); err != nil { - return err - } - } - return nil - case "_==_": - default: - return errors.Errorf("unsupported operator %s; use == and &&", call.FunctionName()) - } - if len(args) != 2 || args[0].Kind() != ast.IdentKind || args[1].Kind() != ast.LiteralKind { - return errors.New("filter must compare user or metric with a string") - } - value, ok := args[1].AsLiteral().Value().(string) - if !ok { - return errors.New("filter must compare user or metric with a string") - } - switch args[0].AsIdent() { - case "user": - username := "" - if value != "" { - parsed, err := parseUsernameFromName(value) - if err != nil { - return errors.Wrap(err, "invalid user") - } - username = parsed - } - f.username = &username - case "metric": - metric := storepb.InstanceUsageLimitsSetting_Metric(storepb.InstanceUsageLimitsSetting_Metric_value[value]) - if !store.IsValidUsageLimitMetric(metric) { - return errors.Errorf("unknown metric %q", value) - } - f.metric = &metric - default: - return errors.Errorf("unknown field %q", args[0].AsIdent()) - } - return nil -} diff --git a/server/api/usage_limit_service_converters.go b/server/api/usage_limit_service_converters.go deleted file mode 100644 index 37e02a6d6429e..0000000000000 --- a/server/api/usage_limit_service_converters.go +++ /dev/null @@ -1,37 +0,0 @@ -package api - -import ( - "github.com/usememos/memos/core/usagelimit" - apipb "github.com/usememos/memos/proto/gen/api" - storepb "github.com/usememos/memos/proto/gen/store" -) - -// convertUsageLimitFromStore converts a stored usage limit. username is the -// name of the user the limit applies to, or empty for a default. -func convertUsageLimitFromStore(limit *storepb.InstanceUsageLimitsSetting_UsageLimit, username string) *apipb.UsageLimit { - return &apipb.UsageLimit{ - Name: UsageLimitNamePrefix + limit.Uid, - Metric: apipb.UsageLimit_Metric(limit.Metric), - User: usageLimitUserName(username), - Value: limit.Value, - CreateTime: limit.CreateTime, - UpdateTime: limit.UpdateTime, - } -} - -// usageLimitUserName returns the user field of a usage limit: the user's -// resource name, or empty for a default. -func usageLimitUserName(username string) string { - if username == "" { - return "" - } - return BuildUserName(username) -} - -func convertEffectiveLimitToProto(limit usagelimit.Limit) *apipb.UserUsage_EffectiveLimit { - effective := &apipb.UserUsage_EffectiveLimit{Value: limit.Value} - if limit.Source != "" { - effective.UsageLimit = UsageLimitNamePrefix + limit.Source - } - return effective -} diff --git a/server/api/user_service_usage.go b/server/api/user_service_usage.go index eedeeffbae70e..ee6b8a87a9ff4 100644 --- a/server/api/user_service_usage.go +++ b/server/api/user_service_usage.go @@ -12,7 +12,7 @@ import ( "github.com/usememos/memos/store" ) -// GetUserUsage returns a user's usage and the usage limits in effect for the +// GetUserUsage returns a user's usage and the limit set in effect for the // user. Only the user or an admin may call it. func (s *APIService) GetUserUsage(ctx context.Context, request *apipb.GetUserUsageRequest) (*apipb.UserUsage, error) { parent, ok := strings.CutSuffix(request.Name, "/usage") @@ -48,13 +48,18 @@ func (s *APIService) GetUserUsage(ctx context.Context, request *apipb.GetUserUsa if err != nil { return nil, status.Errorf(codes.Internal, "failed to get usage: %v", err) } + limits := &apipb.UserUsage_Limits{} + if entry := usage.Limits.Entry; entry != nil { + limits.MemoCount = entry.MemoCount + limits.MemoContentBytes = entry.MemoContentBytes + limits.StorageBytes = entry.StorageBytes + limits.UploadBytes = entry.UploadBytes + } return &apipb.UserUsage{ - Name: BuildUserName(user.Username) + "/usage", - MemoCount: usage.MemoCount, - StorageBytes: usage.StorageBytes, - MemoCountLimit: convertEffectiveLimitToProto(usage.Limits.MemoCount), - StorageBytesLimit: convertEffectiveLimitToProto(usage.Limits.StorageBytes), - UploadBytesLimit: convertEffectiveLimitToProto(usage.Limits.UploadBytes), - MemoContentBytesLimit: convertEffectiveLimitToProto(usage.Limits.MemoContentBytes), + Name: BuildUserName(user.Username) + "/usage", + MemoCount: usage.MemoCount, + StorageBytes: usage.StorageBytes, + Limits: limits, + LimitIndex: int32(usage.Limits.Index), }, nil } diff --git a/server/test/usage_limit_test.go b/server/test/usage_limit_test.go index dc06a4df93168..843e86282c1b4 100644 --- a/server/test/usage_limit_test.go +++ b/server/test/usage_limit_test.go @@ -13,9 +13,9 @@ import ( // TestUsageLimitRefusalOnBothTransports proves the quota refusal contract on // the wire: HTTP 429 with an ErrorInfo of reason QUOTA_EXCEEDED and a // QuotaFailure, and no Retry-After or RateLimit fields, because waiting does -// not lift a usage limit. It also proves that the usageLimits and user usage -// routes reach their handlers. Admins are limited like everyone else, so the -// first user is enough. +// not lift a usage limit. It also proves that the usage limits setting and +// the user usage routes reach their handlers. Admins are limited like +// everyone else, so the first user is enough. func TestUsageLimitRefusalOnBothTransports(t *testing.T) { ctx := context.Background() inst := bootInstance(ctx, t, instanceOptions{instanceURL: "http://127.0.0.1"}) @@ -38,30 +38,33 @@ func TestUsageLimitRefusalOnBothTransports(t *testing.T) { return resp } - status, body := inst.do(t, http.MethodPost, "/api/usageLimits?usageLimitId=default-memos", token, map[string]any{ - "metric": "MEMO_COUNT", - "value": "1", + status, body := inst.do(t, http.MethodPatch, "/api/instance/settings/USAGE_LIMITS", token, map[string]any{ + "usageLimitsSetting": map[string]any{ + "limits": []map[string]any{{"allUsers": true, "memoCount": "1", "memoContentBytes": "8192", "uploadBytes": "33554432"}}, + }, }) - require.Equal(t, http.StatusOK, status, "creating a usage limit should succeed: %s", body) + require.Equal(t, http.StatusOK, status, "replacing the usage limits should succeed: %s", body) - status, body = inst.do(t, http.MethodGet, "/api/usageLimits", token, nil) + status, body = inst.do(t, http.MethodGet, "/api/instance/settings/USAGE_LIMITS", token, nil) require.Equal(t, http.StatusOK, status, "%s", body) - var listed struct { - UsageLimits []struct { - Name string `json:"name"` - Metric string `json:"metric"` - Value string `json:"value"` - } `json:"usageLimits"` + var setting struct { + Name string `json:"name"` + UsageLimitsSetting struct { + Limits []struct { + AllUsers bool `json:"allUsers"` + MemoCount string `json:"memoCount"` + MemoContentBytes string `json:"memoContentBytes"` + UploadBytes string `json:"uploadBytes"` + } `json:"limits"` + } `json:"usageLimitsSetting"` } - require.NoError(t, json.Unmarshal(body, &listed)) - require.Len(t, listed.UsageLimits, 3) - require.Equal(t, "usageLimits/default-memos", listed.UsageLimits[0].Name) - require.Equal(t, "usageLimits/default-memo-content", listed.UsageLimits[1].Name) - require.Equal(t, "MEMO_CONTENT_BYTES", listed.UsageLimits[1].Metric) - require.Equal(t, "8192", listed.UsageLimits[1].Value) - require.Equal(t, "usageLimits/default-upload", listed.UsageLimits[2].Name) - require.Equal(t, "UPLOAD_BYTES", listed.UsageLimits[2].Metric) - require.Equal(t, "33554432", listed.UsageLimits[2].Value) + require.NoError(t, json.Unmarshal(body, &setting)) + require.Equal(t, "instance/settings/USAGE_LIMITS", setting.Name) + require.Len(t, setting.UsageLimitsSetting.Limits, 1) + require.True(t, setting.UsageLimitsSetting.Limits[0].AllUsers) + require.Equal(t, "1", setting.UsageLimitsSetting.Limits[0].MemoCount) + require.Equal(t, "8192", setting.UsageLimitsSetting.Limits[0].MemoContentBytes) + require.Equal(t, "33554432", setting.UsageLimitsSetting.Limits[0].UploadBytes) inst.createMemo(t, token, "first", "the only memo the limit allows") @@ -69,18 +72,18 @@ func TestUsageLimitRefusalOnBothTransports(t *testing.T) { status, body = inst.do(t, http.MethodGet, "/api/"+userName+"/usage", token, nil) require.Equal(t, http.StatusOK, status, "%s", body) var usage struct { - Name string `json:"name"` - MemoCount string `json:"memoCount"` - MemoCountLimit struct { - Value string `json:"value"` - UsageLimit string `json:"usageLimit"` - } `json:"memoCountLimit"` + Name string `json:"name"` + MemoCount string `json:"memoCount"` + Limits struct { + MemoCount string `json:"memoCount"` + } `json:"limits"` + LimitIndex int `json:"limitIndex"` } require.NoError(t, json.Unmarshal(body, &usage)) require.Equal(t, userName+"/usage", usage.Name) require.Equal(t, "1", usage.MemoCount) - require.Equal(t, "1", usage.MemoCountLimit.Value) - require.Equal(t, "usageLimits/default-memos", usage.MemoCountLimit.UsageLimit) + require.Equal(t, "1", usage.Limits.MemoCount) + require.Equal(t, 0, usage.LimitIndex) requireNoWaitHeaders := func(resp *http.Response) { t.Helper() @@ -117,7 +120,7 @@ func TestUsageLimitRefusalOnBothTransports(t *testing.T) { case "type.googleapis.com/google.rpc.ErrorInfo": require.Equal(t, "QUOTA_EXCEEDED", detail.Reason) require.Equal(t, "memos.api", detail.Domain) - require.Equal(t, map[string]string{"metric": "MEMO_COUNT", "limit": "1", "usage": "1", "usage_limit": "usageLimits/default-memos"}, detail.Metadata) + require.Equal(t, map[string]string{"metric": "MEMO_COUNT", "limit": "1", "usage": "1"}, detail.Metadata) case "type.googleapis.com/google.rpc.QuotaFailure": require.Len(t, detail.Violations, 1) require.Equal(t, userName, detail.Violations[0].Subject) @@ -146,9 +149,4 @@ func TestUsageLimitRefusalOnBothTransports(t *testing.T) { connectTypes = append(connectTypes, detail.Type) } require.ElementsMatch(t, []string{"google.rpc.ErrorInfo", "google.rpc.QuotaFailure"}, connectTypes) - - // Deleting the limit lifts the refusal at once: checks skip the cache. - status, body = inst.do(t, http.MethodDelete, "/api/usageLimits/default-memos", token, nil) - require.Equal(t, http.StatusOK, status, "%s", body) - inst.createMemo(t, token, "second", "allowed again") } diff --git a/store/deployment_config_test.go b/store/deployment_config_test.go index a88eec7e9b374..91362d202c558 100644 --- a/store/deployment_config_test.go +++ b/store/deployment_config_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/emptypb" "github.com/usememos/memos/internal/profile" storepb "github.com/usememos/memos/proto/gen/store" @@ -293,18 +292,13 @@ func TestLoadDeploymentUsageLimitsPreservesStoreTargets(t *testing.T) { require.NoError(t, err) dir := t.TempDir() path := filepath.Join(dir, "memos-instance-setting-usage-limits.json") + uploadBytes, unlimited := int64(32<<20), int64(0) input := &storepb.InstanceSetting{ Key: storepb.InstanceSettingKey_USAGE_LIMITS, Value: &storepb.InstanceSetting_UsageLimitsSetting{UsageLimitsSetting: &storepb.InstanceUsageLimitsSetting{ Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{ - { - Uid: "default-upload", Metric: storepb.InstanceUsageLimitsSetting_UPLOAD_BYTES, Value: 32 << 20, - Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_AllUsers{AllUsers: &emptypb.Empty{}}, - }, - { - Uid: "alice-upload", Metric: storepb.InstanceUsageLimitsSetting_UPLOAD_BYTES, Value: 0, - Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_UserId{UserId: alice.ID}, - }, + {UserIds: []int32{alice.ID}, UploadBytes: &unlimited}, + {AllUsers: true, UploadBytes: &uploadBytes}, }, }}, } @@ -312,7 +306,7 @@ func TestLoadDeploymentUsageLimitsPreservesStoreTargets(t *testing.T) { require.NoError(t, stores.LoadDeploymentConfigurationDir(ctx, dir)) setting, err := stores.GetInstanceUsageLimitsSetting(ctx) require.NoError(t, err) - require.True(t, proto.Equal(input.GetUsageLimitsSetting(), setting), "configuration uses the stored schema without converting targets") + require.True(t, proto.Equal(input.GetUsageLimitsSetting(), setting), "configuration uses the stored schema without converting users") _, err = stores.UpdateUser(ctx, &store.UpdateUser{ID: alice.ID, Username: new("alice-renamed")}) require.NoError(t, err) @@ -324,21 +318,23 @@ func TestLoadDeploymentUsageLimitsPreservesStoreTargets(t *testing.T) { func TestLoadDeploymentUsageLimitsRejectsInvalidTargets(t *testing.T) { for name, limits := range map[string]string{ - "missing target": `[{"uid":"bad-user","metric":"UPLOAD_BYTES","value":"1"}]`, - "zero user ID": `[{"uid":"bad-user","metric":"UPLOAD_BYTES","userId":0,"value":"1"}]`, - "negative user ID": `[{"uid":"bad-user","metric":"UPLOAD_BYTES","userId":-1,"value":"1"}]`, - "null all users": `[{"uid":"bad-user","metric":"UPLOAD_BYTES","allUsers":null,"value":"1"}]`, - "both targets": `[{"uid":"bad-user","metric":"UPLOAD_BYTES","allUsers":{},"userId":1,"value":"1"}]`, - "user resource name": `[{"uid":"bad-user","metric":"UPLOAD_BYTES","user":"users/alice","value":"1"}]`, - "duplicate user": `[{"uid":"first","metric":"UPLOAD_BYTES","userId":1,"value":"1"},{"uid":"second","metric":"UPLOAD_BYTES","userId":1,"value":"0"}]`, - "duplicate all users": `[{"uid":"first","metric":"UPLOAD_BYTES","allUsers":{},"value":"1"},{"uid":"second","metric":"UPLOAD_BYTES","allUsers":{},"value":"0"}]`, + "no all-users entry": `[{"userIds":[1],"uploadBytes":"1"}]`, + "all users not last": `[{"allUsers":true,"uploadBytes":"1"},{"userIds":[1],"uploadBytes":"1"}]`, + "zero user ID": `[{"userIds":[0],"uploadBytes":"1"},{"allUsers":true}]`, + "negative user ID": `[{"userIds":[-1],"uploadBytes":"1"},{"allUsers":true}]`, + "entry without users": `[{"uploadBytes":"1"},{"allUsers":true}]`, + "both targets": `[{"allUsers":true,"userIds":[1],"uploadBytes":"1"}]`, + "user resource name": `[{"users":["users/alice"],"uploadBytes":"1"},{"allUsers":true}]`, + "repeated user": `[{"userIds":[1,1],"uploadBytes":"1"},{"allUsers":true}]`, + "negative value": `[{"allUsers":true,"uploadBytes":"-1"}]`, + "old per-metric": `[{"uid":"a","metric":"UPLOAD_BYTES","allUsers":{},"value":"1"}]`, } { t.Run(name, func(t *testing.T) { ctx := context.Background() stores := newDeploymentConfigurationTestStore(t) dir := t.TempDir() path := filepath.Join(dir, "memos-instance-setting-usage-limits.json") - valid := `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"uid":"default-upload","metric":"UPLOAD_BYTES","allUsers":{},"value":"33554432"}]}}` + valid := `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"allUsers":true,"uploadBytes":"33554432"}]}}` require.NoError(t, os.WriteFile(path, []byte(valid), 0600)) require.NoError(t, stores.LoadDeploymentConfigurationDir(ctx, dir)) previous, err := stores.GetInstanceUsageLimitsSetting(ctx) diff --git a/store/instance_setting.go b/store/instance_setting.go index 9cd09cb1d36b3..23d9008f81f80 100644 --- a/store/instance_setting.go +++ b/store/instance_setting.go @@ -50,6 +50,11 @@ func (s *Store) UpsertInstanceSetting(ctx context.Context, upsert *storepb.Insta valueBytes, err = protojson.Marshal(upsert.GetAiSetting()) } else if upsert.Key == storepb.InstanceSettingKey_ACCESS { valueBytes, err = protojson.Marshal(upsert.GetAccessSetting()) + } else if upsert.Key == storepb.InstanceSettingKey_USAGE_LIMITS { + if err := ValidateInstanceUsageLimitsSetting(upsert.GetUsageLimitsSetting()); err != nil { + return nil, err + } + valueBytes, err = protojson.Marshal(upsert.GetUsageLimitsSetting()) } else { return nil, errors.Errorf("unsupported instance setting key: %v", upsert.Key) } diff --git a/store/test/usage_limit_test.go b/store/test/usage_limit_test.go index 5b20090b4abd8..253c5d1dd4ad6 100644 --- a/store/test/usage_limit_test.go +++ b/store/test/usage_limit_test.go @@ -2,41 +2,44 @@ package test import ( "context" - "errors" "fmt" "os" "path/filepath" - "sync" "testing" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/proto" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" - storedb "github.com/usememos/memos/store/db" ) -func usageLimit(uid string, metric storepb.InstanceUsageLimitsSetting_Metric, userID int32, value int64) *storepb.InstanceUsageLimitsSetting_UsageLimit { - limit := &storepb.InstanceUsageLimitsSetting_UsageLimit{ - Uid: uid, Metric: metric, Value: value, - Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_AllUsers{AllUsers: &emptypb.Empty{}}, - } - if userID != 0 { - limit.Target = &storepb.InstanceUsageLimitsSetting_UsageLimit_UserId{UserId: userID} - } - return limit +type usageLimitEntry = storepb.InstanceUsageLimitsSetting_UsageLimit + +func usageLimitsSetting(limits ...*usageLimitEntry) *storepb.InstanceUsageLimitsSetting { + return &storepb.InstanceUsageLimitsSetting{Limits: limits} } -func storedUsageLimitUIDs(ctx context.Context, t *testing.T, ts *store.Store) []string { +// writeUsageLimits stores limits through the generic instance setting write, +// which is the only write path the API has. +func writeUsageLimits(ctx context.Context, t *testing.T, ts *store.Store, limits ...*usageLimitEntry) { + t.Helper() + _, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ + Key: storepb.InstanceSettingKey_USAGE_LIMITS, + Value: &storepb.InstanceSetting_UsageLimitsSetting{UsageLimitsSetting: usageLimitsSetting(limits...)}, + }) + require.NoError(t, err) +} + +func storedUserIDs(ctx context.Context, t *testing.T, ts *store.Store) [][]int32 { t.Helper() setting, err := ts.GetInstanceUsageLimitsSetting(ctx) require.NoError(t, err) - uids := []string{} + ids := [][]int32{} for _, limit := range setting.Limits { - uids = append(uids, limit.Uid) + ids = append(ids, limit.UserIds) } - return uids + return ids } func TestCountUserMemos(t *testing.T) { @@ -114,157 +117,114 @@ func TestUpdateInstanceSettingIfUnchanged(t *testing.T) { require.Equal(t, "second", settings[0].Value) } -func TestUsageLimitLifecycle(t *testing.T) { +func TestUsageLimitsDefaultSetting(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) defer ts.Close() - alice, err := ts.CreateUser(ctx, &store.User{Username: "limit-alice", Role: store.RoleUser, PasswordHash: "hash"}) - require.NoError(t, err) setting, err := ts.GetInstanceUsageLimitsSetting(ctx) require.NoError(t, err) - require.Len(t, setting.Limits, 2) - require.Equal(t, "default-upload", setting.Limits[0].Uid) - require.Equal(t, store.DefaultUploadBytes, setting.Limits[0].Value) - - created, err := ts.CreateUsageLimit(ctx, usageLimit("default-memos", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, 0, 1000)) - require.NoError(t, err) - require.NotNil(t, created.CreateTime) - require.Equal(t, created.CreateTime.AsTime(), created.UpdateTime.AsTime()) - _, err = ts.CreateUsageLimit(ctx, usageLimit("alice-storage", storepb.InstanceUsageLimitsSetting_STORAGE_BYTES, alice.ID, 5<<30)) - require.NoError(t, err) - require.Equal(t, []string{"default-upload", "default-memo-content", "default-memos", "alice-storage"}, storedUsageLimitUIDs(ctx, t, ts)) - - _, err = ts.CreateUsageLimit(ctx, usageLimit("default-memos", storepb.InstanceUsageLimitsSetting_STORAGE_BYTES, 0, 1)) - require.ErrorIs(t, err, store.ErrUsageLimitAlreadyExists, "IDs are unique") - _, err = ts.CreateUsageLimit(ctx, usageLimit("other-memos", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, 0, 1)) - require.ErrorIs(t, err, store.ErrUsageLimitAlreadyExists, "one default per metric") - _, err = ts.CreateUsageLimit(ctx, usageLimit("alice-storage-2", storepb.InstanceUsageLimitsSetting_STORAGE_BYTES, alice.ID, 1)) - require.ErrorIs(t, err, store.ErrUsageLimitAlreadyExists, "one limit per metric and user") - _, err = ts.CreateUsageLimit(ctx, usageLimit("negative", storepb.InstanceUsageLimitsSetting_UPLOAD_BYTES, alice.ID, -1)) - require.ErrorIs(t, err, store.ErrInvalidUsageLimit) - _, err = ts.CreateUsageLimit(ctx, usageLimit("Bad_ID", storepb.InstanceUsageLimitsSetting_UPLOAD_BYTES, alice.ID, 1)) - require.ErrorIs(t, err, store.ErrInvalidUsageLimit) - _, err = ts.CreateUsageLimit(ctx, usageLimit("no-metric", storepb.InstanceUsageLimitsSetting_METRIC_UNSPECIFIED, 0, 1)) - require.ErrorIs(t, err, store.ErrInvalidUsageLimit) - - updated, err := ts.UpdateUsageLimit(ctx, "default-memos", 2000) - require.NoError(t, err) - require.Equal(t, int64(2000), updated.Value) - require.False(t, updated.UpdateTime.AsTime().Before(updated.CreateTime.AsTime())) - _, err = ts.UpdateUsageLimit(ctx, "missing", 1) - require.ErrorIs(t, err, store.ErrUsageLimitNotFound) - - require.NoError(t, ts.DeleteUsageLimit(ctx, "alice-storage")) - require.ErrorIs(t, ts.DeleteUsageLimit(ctx, "alice-storage"), store.ErrUsageLimitNotFound) - require.Equal(t, []string{"default-upload", "default-memo-content", "default-memos"}, storedUsageLimitUIDs(ctx, t, ts)) - - // The row is store only: the generic setting reads see it, but its key has - // no API counterpart, and it round-trips through the raw conversion. - stored, err := ts.GetStoredInstanceSetting(ctx, &store.FindInstanceSetting{Name: storepb.InstanceSettingKey_USAGE_LIMITS.String()}) - require.NoError(t, err) - require.Equal(t, int64(2000), stored.GetUsageLimitsSetting().Limits[2].Value) -} - -func TestUsageLimitDefaultSetting(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - defer ts.Close() - - setting, err := ts.GetInstanceUsageLimitsSetting(ctx) - require.NoError(t, err) - require.Len(t, setting.Limits, 2) - require.Equal(t, storepb.InstanceUsageLimitsSetting_UPLOAD_BYTES, setting.Limits[0].Metric) - require.NotNil(t, setting.Limits[0].GetAllUsers()) - require.Equal(t, store.DefaultUploadBytes, setting.Limits[0].Value) - require.Equal(t, store.DefaultMemoContentBytes, setting.Limits[1].Value) - require.Equal(t, storepb.InstanceUsageLimitsSetting_MEMO_CONTENT_BYTES, setting.Limits[1].Metric) + require.Len(t, setting.Limits, 1) + require.True(t, setting.Limits[0].AllUsers) + require.Equal(t, store.DefaultUploadBytes, setting.Limits[0].GetUploadBytes()) + require.Equal(t, store.DefaultMemoContentBytes, setting.Limits[0].GetMemoContentBytes()) + require.Nil(t, setting.Limits[0].MemoCount) + require.Nil(t, setting.Limits[0].StorageBytes) stored, err := ts.GetStoredInstanceSetting(ctx, &store.FindInstanceSetting{Name: storepb.InstanceSettingKey_USAGE_LIMITS.String()}) require.NoError(t, err) require.Nil(t, stored, "reading defaults does not persist a setting") - setting.Limits[0].Value = 1 + *setting.Limits[0].UploadBytes = 1 setting, err = ts.GetInstanceUsageLimitsSetting(ctx) require.NoError(t, err) - require.Equal(t, store.DefaultUploadBytes, setting.Limits[0].Value, "each read returns fresh defaults") + require.Equal(t, store.DefaultUploadBytes, setting.Limits[0].GetUploadBytes(), "each read returns fresh defaults") - updated, err := ts.UpdateUsageLimit(ctx, "default-upload", 0) - require.NoError(t, err) - require.Zero(t, updated.Value) + writeUsageLimits(ctx, t, ts, &usageLimitEntry{AllUsers: true, UploadBytes: new(int64(0))}) setting, err = ts.GetInstanceUsageLimitsSetting(ctx) require.NoError(t, err) - require.Zero(t, setting.Limits[0].Value, "an explicit zero remains unlimited") + require.Len(t, setting.Limits, 1) + require.Zero(t, setting.Limits[0].GetUploadBytes(), "an explicit zero is stored") + require.Nil(t, setting.Limits[0].MemoContentBytes, "the stored list is the whole truth; built-in defaults do not come back") - require.NoError(t, ts.DeleteUsageLimit(ctx, "default-upload")) - require.NoError(t, ts.DeleteUsageLimit(ctx, "default-memo-content")) - setting, err = ts.GetInstanceUsageLimitsSetting(ctx) + // The row round-trips through the generic setting reads too. + stored, err = ts.GetStoredInstanceSetting(ctx, &store.FindInstanceSetting{Name: storepb.InstanceSettingKey_USAGE_LIMITS.String()}) require.NoError(t, err) - require.Empty(t, setting.Limits, "a stored empty setting does not restore defaults") + require.True(t, proto.Equal(setting, stored.GetUsageLimitsSetting())) } -func TestUsageLimitCap(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - defer ts.Close() - - limits := make([]*storepb.InstanceUsageLimitsSetting_UsageLimit, 0, store.MaxUsageLimits) - for i := range store.MaxUsageLimits { - // Writes prune limits of missing users, so the cap is checked on the - // validator that every write runs. - limits = append(limits, usageLimit(fmt.Sprintf("limit-%d", i), storepb.InstanceUsageLimitsSetting_MEMO_COUNT, int32(i), 1)) +func TestValidateInstanceUsageLimitsSetting(t *testing.T) { + tooMany := make([]*usageLimitEntry, 0, store.MaxUsageLimitEntries+1) + for i := range store.MaxUsageLimitEntries { + tooMany = append(tooMany, &usageLimitEntry{UserIds: []int32{int32(i + 1)}}) + } + tooMany = append(tooMany, &usageLimitEntry{AllUsers: true}) + tooManyUsers := make([]int32, 0, store.MaxUsageLimitUsers+1) + for i := range store.MaxUsageLimitUsers + 1 { + tooManyUsers = append(tooManyUsers, int32(i+1)) } - require.NoError(t, store.ValidateInstanceUsageLimitsSetting(&storepb.InstanceUsageLimitsSetting{Limits: limits})) - limits = append(limits, usageLimit("one-too-many", storepb.InstanceUsageLimitsSetting_STORAGE_BYTES, 0, 1)) - require.ErrorIs(t, store.ValidateInstanceUsageLimitsSetting(&storepb.InstanceUsageLimitsSetting{Limits: limits}), store.ErrInvalidUsageLimit) -} -func TestUsageLimitTargetValidation(t *testing.T) { - for name, target := range map[string]*storepb.InstanceUsageLimitsSetting_UsageLimit{ - "missing target": {Uid: "invalid-target", Metric: storepb.InstanceUsageLimitsSetting_MEMO_COUNT, Value: 1}, - "zero user": {Uid: "invalid-target", Metric: storepb.InstanceUsageLimitsSetting_MEMO_COUNT, Value: 1, - Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_UserId{UserId: 0}}, - "negative user": usageLimit("invalid-target", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, -1, 1), - "nil all users": {Uid: "invalid-target", Metric: storepb.InstanceUsageLimitsSetting_MEMO_COUNT, Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_AllUsers{}}, + require.NoError(t, store.ValidateInstanceUsageLimitsSetting(usageLimitsSetting( + &usageLimitEntry{UserIds: []int32{1, 2}, MemoCount: new(int64(0))}, + &usageLimitEntry{UserIds: []int32{2}, StorageBytes: new(int64(1))}, + &usageLimitEntry{AllUsers: true}, + )), "a user may appear in several entries; the first wins") + require.NoError(t, store.ValidateInstanceUsageLimitsSetting(usageLimitsSetting(tooMany[1:]...))) + + for name, setting := range map[string]*storepb.InstanceUsageLimitsSetting{ + "nil": nil, + "empty": usageLimitsSetting(), + "no all-users entry": usageLimitsSetting(&usageLimitEntry{UserIds: []int32{1}}), + "all users not last": usageLimitsSetting(&usageLimitEntry{AllUsers: true}, &usageLimitEntry{UserIds: []int32{1}}), + "two all-users entries": usageLimitsSetting(&usageLimitEntry{AllUsers: true}, &usageLimitEntry{AllUsers: true}), + "users and all users": usageLimitsSetting(&usageLimitEntry{AllUsers: true, UserIds: []int32{1}}), + "entry without users": usageLimitsSetting(&usageLimitEntry{}, &usageLimitEntry{AllUsers: true}), + "nil entry": usageLimitsSetting(nil, &usageLimitEntry{AllUsers: true}), + "zero user ID": usageLimitsSetting(&usageLimitEntry{UserIds: []int32{0}}, &usageLimitEntry{AllUsers: true}), + "negative user ID": usageLimitsSetting(&usageLimitEntry{UserIds: []int32{-1}}, &usageLimitEntry{AllUsers: true}), + "repeated user in entry": usageLimitsSetting(&usageLimitEntry{UserIds: []int32{1, 1}}, &usageLimitEntry{AllUsers: true}), + "negative memo count": usageLimitsSetting(&usageLimitEntry{AllUsers: true, MemoCount: new(int64(-1))}), + "negative content bytes": usageLimitsSetting(&usageLimitEntry{AllUsers: true, MemoContentBytes: new(int64(-1))}), + "negative storage bytes": usageLimitsSetting(&usageLimitEntry{AllUsers: true, StorageBytes: new(int64(-1))}), + "negative upload bytes": usageLimitsSetting(&usageLimitEntry{AllUsers: true, UploadBytes: new(int64(-1))}), + "too many entries": usageLimitsSetting(tooMany...), + "too many users": usageLimitsSetting(&usageLimitEntry{UserIds: tooManyUsers}, &usageLimitEntry{AllUsers: true}), + "too many users in total": usageLimitsSetting(&usageLimitEntry{UserIds: tooManyUsers[:600]}, &usageLimitEntry{UserIds: tooManyUsers[600:]}, &usageLimitEntry{AllUsers: true}), + "all users with user list": usageLimitsSetting(&usageLimitEntry{UserIds: []int32{1}}, &usageLimitEntry{AllUsers: true, UserIds: []int32{2}}), } { t.Run(name, func(t *testing.T) { - err := store.ValidateInstanceUsageLimitsSetting(&storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{target}}) - require.ErrorIs(t, err, store.ErrInvalidUsageLimit) - }) - } - for name, userID := range map[string]int32{"duplicate default": 0, "duplicate user": 7} { - t.Run(name, func(t *testing.T) { - err := store.ValidateInstanceUsageLimitsSetting(&storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{ - usageLimit("first", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, userID, 1), - usageLimit("second", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, userID, 0), - }}) - require.ErrorIs(t, err, store.ErrUsageLimitAlreadyExists) + require.ErrorIs(t, store.ValidateInstanceUsageLimitsSetting(setting), store.ErrInvalidUsageLimits) }) } } -func TestUsageLimitsRejectMissingTarget(t *testing.T) { +func TestUpsertInstanceSettingValidatesUsageLimits(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) defer ts.Close() - invalid := &storepb.InstanceUsageLimitsSetting_UsageLimit{Uid: "missing-target", Metric: storepb.InstanceUsageLimitsSetting_MEMO_COUNT, Value: 1} - _, err := ts.CreateUsageLimit(ctx, invalid) - require.ErrorIs(t, err, store.ErrInvalidUsageLimit) + + _, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ + Key: storepb.InstanceSettingKey_USAGE_LIMITS, + Value: &storepb.InstanceSetting_UsageLimitsSetting{UsageLimitsSetting: usageLimitsSetting(&usageLimitEntry{UserIds: []int32{1}})}, + }) + require.ErrorIs(t, err, store.ErrInvalidUsageLimits) + stored, err := ts.GetStoredInstanceSetting(ctx, &store.FindInstanceSetting{Name: storepb.InstanceSettingKey_USAGE_LIMITS.String()}) + require.NoError(t, err) + require.Nil(t, stored, "an invalid write stores nothing") + + // A row that became invalid outside the store fails closed instead of + // silently removing every limit. _, err = ts.GetDriver().UpsertInstanceSetting(ctx, &store.InstanceSetting{ Name: storepb.InstanceSettingKey_USAGE_LIMITS.String(), - Value: `{"limits":[{"uid":"missing-target","metric":"MEMO_COUNT","value":"1"}]}`, + Value: `{"limits":[{"userIds":[1],"memoCount":"1"}]}`, }) require.NoError(t, err) _, err = ts.GetInstanceUsageLimitsSetting(ctx) - require.ErrorIs(t, err, store.ErrInvalidUsageLimit, "a missing target fails closed instead of removing a limit") - _, err = ts.UpdateUsageLimit(ctx, "missing-target", 2) - require.ErrorIs(t, err, store.ErrInvalidUsageLimit) + require.ErrorIs(t, err, store.ErrInvalidUsageLimits) } -func TestDeleteUserRemovesUsageLimits(t *testing.T) { +func TestDeleteUserRemovesFromUsageLimits(t *testing.T) { t.Parallel() ctx := context.Background() ts := NewTestingStore(ctx, t) @@ -273,39 +233,23 @@ func TestDeleteUserRemovesUsageLimits(t *testing.T) { require.NoError(t, err) bob, err := ts.CreateUser(ctx, &store.User{Username: "delete-bob", Role: store.RoleUser, PasswordHash: "hash"}) require.NoError(t, err) - for _, limit := range []*storepb.InstanceUsageLimitsSetting_UsageLimit{ - usageLimit("default-storage", storepb.InstanceUsageLimitsSetting_STORAGE_BYTES, 0, 1<<30), - usageLimit("alice-storage", storepb.InstanceUsageLimitsSetting_STORAGE_BYTES, alice.ID, 5<<30), - usageLimit("alice-memos", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, alice.ID, 10), - usageLimit("bob-memos", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, bob.ID, 20), - } { - _, err := ts.CreateUsageLimit(ctx, limit) - require.NoError(t, err) - } + writeUsageLimits(ctx, t, ts, + &usageLimitEntry{UserIds: []int32{alice.ID, bob.ID}, StorageBytes: new(int64(5 << 30))}, + &usageLimitEntry{UserIds: []int32{alice.ID}, MemoCount: new(int64(10))}, + &usageLimitEntry{UserIds: []int32{bob.ID}, MemoCount: new(int64(20))}, + &usageLimitEntry{AllUsers: true, StorageBytes: new(int64(1 << 30))}, + ) _, err = ts.DeleteUser(ctx, &store.DeleteUser{ID: alice.ID}) require.NoError(t, err) - require.Equal(t, []string{"default-upload", "default-memo-content", "default-storage", "bob-memos"}, storedUsageLimitUIDs(ctx, t, ts)) -} - -func TestUsageLimitWritesPruneMissingUsers(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - defer ts.Close() - alice, err := ts.CreateUser(ctx, &store.User{Username: "prune-alice", Role: store.RoleUser, PasswordHash: "hash"}) - require.NoError(t, err) - - // An entry for a user that no longer exists, as a failed cleanup would leave. - _, err = ts.GetDriver().UpsertInstanceSetting(ctx, &store.InstanceSetting{ - Name: storepb.InstanceSettingKey_USAGE_LIMITS.String(), - Value: `{"limits":[{"uid":"ghost-memos","metric":"MEMO_COUNT","userId":99999,"value":"1"}]}`, - }) + require.Equal(t, [][]int32{{bob.ID}, {bob.ID}, nil}, storedUserIDs(ctx, t, ts), "the user leaves shared entries and takes entries of their own with them") + setting, err := ts.GetInstanceUsageLimitsSetting(ctx) require.NoError(t, err) + require.Equal(t, int64(5<<30), setting.Limits[0].GetStorageBytes()) + require.True(t, setting.Limits[2].AllUsers) - _, err = ts.CreateUsageLimit(ctx, usageLimit("alice-memos", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, alice.ID, 5)) - require.NoError(t, err) - require.Equal(t, []string{"alice-memos"}, storedUsageLimitUIDs(ctx, t, ts)) + require.NoError(t, ts.RemoveUserFromUsageLimits(ctx, 99999), "an unknown user is a no-op") + require.Equal(t, [][]int32{{bob.ID}, {bob.ID}, nil}, storedUserIDs(ctx, t, ts)) } func TestUsageLimitsDeploymentFile(t *testing.T) { @@ -315,132 +259,45 @@ func TestUsageLimitsDeploymentFile(t *testing.T) { defer ts.Close() alice, err := ts.CreateUser(ctx, &store.User{Username: "alice", Role: store.RoleUser, PasswordHash: "hash"}) require.NoError(t, err) - _, err = ts.CreateUsageLimit(ctx, usageLimit("stored-memos", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, 0, 10)) - require.NoError(t, err) + writeUsageLimits(ctx, t, ts, &usageLimitEntry{AllUsers: true, MemoCount: new(int64(10))}) dir := t.TempDir() path := filepath.Join(dir, "memos-instance-setting-usage-limits.json") require.NoError(t, os.WriteFile(path, []byte(`{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[ - {"uid":"default-storage","metric":"STORAGE_BYTES","allUsers":{},"value":"1073741824"}, - {"uid":"alice-storage","metric":"STORAGE_BYTES","userId":`+fmt.Sprint(alice.ID)+`,"value":"5368709120"} + {"userIds":[`+fmt.Sprint(alice.ID)+`],"storageBytes":"5368709120"}, + {"allUsers":true,"storageBytes":"1073741824"} ]}}`), 0o600)) require.NoError(t, ts.LoadDeploymentConfigurationDir(ctx, dir)) - require.Equal(t, []string{"default-storage", "alice-storage"}, storedUsageLimitUIDs(ctx, t, ts), "the file shadows the stored row") effective, err := ts.GetInstanceUsageLimitsSetting(ctx) require.NoError(t, err) - require.NotNil(t, effective.Limits[0].GetAllUsers()) - require.Equal(t, alice.ID, effective.Limits[1].GetUserId()) - _, err = ts.CreateUsageLimit(ctx, usageLimit("new-memos", storepb.InstanceUsageLimitsSetting_UPLOAD_BYTES, 0, 1)) - require.ErrorIs(t, err, store.ErrUsageLimitsDeploymentConfigured) - require.ErrorIs(t, ts.DeleteUsageLimit(ctx, "default-storage"), store.ErrUsageLimitsDeploymentConfigured) - require.NoError(t, ts.DeleteUsageLimitsForUser(ctx, alice.ID), "user deletion cleanup skips file-provided limits") + require.Len(t, effective.Limits, 2, "the file shadows the stored row") + require.Equal(t, []int32{alice.ID}, effective.Limits[0].UserIds) + require.Equal(t, int64(5<<30), effective.Limits[0].GetStorageBytes()) + require.True(t, effective.Limits[1].AllUsers) + require.Nil(t, effective.Limits[1].MemoCount) + require.True(t, ts.IsInstanceSettingDeploymentConfigured(storepb.InstanceSettingKey_USAGE_LIMITS)) + require.NoError(t, ts.RemoveUserFromUsageLimits(ctx, alice.ID), "user deletion cleanup skips file-provided limits") stored, err := ts.GetStoredInstanceSetting(ctx, &store.FindInstanceSetting{Name: storepb.InstanceSettingKey_USAGE_LIMITS.String()}) require.NoError(t, err) - require.Equal(t, "stored-memos", stored.GetUsageLimitsSetting().Limits[2].Uid, "the stored row is unchanged") + require.Equal(t, int64(10), stored.GetUsageLimitsSetting().Limits[0].GetMemoCount(), "the stored row is unchanged") for name, content := range map[string]string{ - "missing value": `{"key":"USAGE_LIMITS"}`, - "negative value": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"uid":"bad","metric":"MEMO_COUNT","allUsers":{},"value":"-1"}]}}`, - "duplicate pair": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"uid":"a","metric":"MEMO_COUNT","allUsers":{},"value":"1"},{"uid":"b","metric":"MEMO_COUNT","allUsers":{},"value":"2"}]}}`, - "unknown field": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"uid":"a","metric":"MEMO_COUNT","value":"1","plan":"pro"}]}}`, + "missing value": `{"key":"USAGE_LIMITS"}`, + "negative value": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"allUsers":true,"memoCount":"-1"}]}}`, + "no all-users entry": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"userIds":[1],"memoCount":"1"}]}}`, + "all users not last": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"allUsers":true},{"userIds":[1]}]}}`, + "users and all users": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"allUsers":true,"userIds":[1]}]}}`, + "user resource names": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"users":["users/alice"]},{"allUsers":true}]}}`, + "unknown field": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"allUsers":true,"plan":"pro"}]}}`, + "old per-metric shape": `{"key":"USAGE_LIMITS","usageLimitsSetting":{"limits":[{"uid":"a","metric":"MEMO_COUNT","allUsers":{},"value":"1"}]}}`, } { invalidDir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(invalidDir, "memos-instance-setting-usage-limits.json"), []byte(content), 0o600)) require.Error(t, ts.LoadDeploymentConfigurationDir(ctx, invalidDir), name) - } -} - -// TestUsageLimitsAcrossReplicas runs two stores on one database, as two -// server replicas do. A change through one is read by the other at once, and -// concurrent writes never lose an update or break uniqueness. -func TestUsageLimitsAcrossReplicas(t *testing.T) { - t.Parallel() - ctx := context.Background() - profile := getTestingProfileForDriver(t, getDriverFromEnv()) - openReplica := func() *store.Store { - driver, err := storedb.NewDBDriver(profile) - require.NoError(t, err) - replica := store.New(driver, profile) - t.Cleanup(func() { _ = replica.Close() }) - require.NoError(t, replica.Migrate(ctx)) - return replica - } - first := openReplica() - second := openReplica() - - // Warm the second replica's settings cache; usage limit reads must bypass it. - _, err := second.GetInstanceUsageLimitsSetting(ctx) - require.NoError(t, err) - _, err = first.CreateUsageLimit(ctx, usageLimit("default-memos", storepb.InstanceUsageLimitsSetting_MEMO_COUNT, 0, 3)) - require.NoError(t, err) - require.Equal(t, []string{"default-upload", "default-memo-content", "default-memos"}, storedUsageLimitUIDs(ctx, t, second)) - - users := make([]*store.User, 6) - for i := range users { - users[i], err = first.CreateUser(ctx, &store.User{Username: fmt.Sprintf("replica-user-%d", i), Role: store.RoleUser, PasswordHash: "hash"}) + effective, err := ts.GetInstanceUsageLimitsSetting(ctx) require.NoError(t, err) + require.Len(t, effective.Limits, 2, "an invalid reload preserves the active configuration: %s", name) } - - // Concurrent creates for the same metric and user leave exactly one limit. - var wg sync.WaitGroup - var mu sync.Mutex - succeeded := 0 - for i := range 6 { - replica := first - if i%2 == 1 { - replica = second - } - wg.Go(func() { - _, err := replica.CreateUsageLimit(ctx, usageLimit(fmt.Sprintf("same-target-%d", i), storepb.InstanceUsageLimitsSetting_STORAGE_BYTES, users[0].ID, int64(i+1))) - if err == nil { - mu.Lock() - succeeded++ - mu.Unlock() - return - } - if !isExpectedUsageLimitRace(err) { - t.Errorf("unexpected error: %v", err) - } - }) - } - wg.Wait() - require.Equal(t, 1, succeeded) - - // Concurrent creates for different users never lose an update: every - // create that reports success is stored. - created := []string{} - for i, user := range users { - replica := first - if i%2 == 1 { - replica = second - } - uid := fmt.Sprintf("memos-user-%d", i) - wg.Go(func() { - _, err := replica.CreateUsageLimit(ctx, usageLimit(uid, storepb.InstanceUsageLimitsSetting_MEMO_COUNT, user.ID, 5)) - if err == nil { - mu.Lock() - created = append(created, uid) - mu.Unlock() - return - } - if !isExpectedUsageLimitRace(err) { - t.Errorf("unexpected error: %v", err) - } - }) - } - wg.Wait() - require.NotEmpty(t, created) - stored := storedUsageLimitUIDs(ctx, t, first) - for _, uid := range created { - require.Contains(t, stored, uid) - } - require.Len(t, stored, 4+len(created)) -} - -// isExpectedUsageLimitRace reports the errors a losing concurrent writer may -// see: the pair is taken, or the retries ran out. -func isExpectedUsageLimitRace(err error) bool { - return errors.Is(err, store.ErrUsageLimitAlreadyExists) || errors.Is(err, store.ErrUsageLimitWriteConflict) } diff --git a/store/usage_limit.go b/store/usage_limit.go index 577221340941f..0ae081f712b6a 100644 --- a/store/usage_limit.go +++ b/store/usage_limit.go @@ -2,138 +2,122 @@ package store import ( "context" - "log/slog" - "regexp" "slices" "github.com/pkg/errors" "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" storepb "github.com/usememos/memos/proto/gen/store" ) -// MaxUsageLimits is the most usage limits an instance can hold. All limits -// share one system_setting row, so the cap also bounds that row's size. -const MaxUsageLimits = 1000 +// MaxUsageLimitEntries is the most entries the usage limits setting can hold. +const MaxUsageLimitEntries = 100 -// DefaultUploadBytes is the default per-file upload limit in bytes. 32 MiB. +// MaxUsageLimitUsers is the most user IDs the usage limits setting can name +// across all entries. All entries share one system_setting row, so the cap +// also bounds that row's size. +const MaxUsageLimitUsers = 1000 + +// DefaultUploadBytes is the built-in per-file upload limit in bytes. 32 MiB. const DefaultUploadBytes int64 = 32 << 20 -// DefaultMemoContentBytes is the default per-memo content limit in bytes. 8 KiB. +// DefaultMemoContentBytes is the built-in per-memo content limit in bytes. 8 KiB. const DefaultMemoContentBytes int64 = 8 << 10 -func newDefaultInstanceUsageLimitsSetting() *storepb.InstanceUsageLimitsSetting { - return &storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{{ - Uid: "default-upload", Metric: storepb.InstanceUsageLimitsSetting_UPLOAD_BYTES, Value: DefaultUploadBytes, - Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_AllUsers{AllUsers: &emptypb.Empty{}}, - }, { - Uid: "default-memo-content", Metric: storepb.InstanceUsageLimitsSetting_MEMO_CONTENT_BYTES, Value: DefaultMemoContentBytes, - Target: &storepb.InstanceUsageLimitsSetting_UsageLimit_AllUsers{AllUsers: &emptypb.Empty{}}, - }}} -} - // usageLimitWriteAttempts bounds the read, modify, and conditional write cycle // when other writers keep changing the usage limits row. const usageLimitWriteAttempts = 5 -// UsageLimitUIDMatcher matches a usage limit ID: lowercase kebab case that -// starts with a letter, at most 32 characters. -var UsageLimitUIDMatcher = regexp.MustCompile(`^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`) - -const maxUsageLimitUIDLength = 32 - var ( - // ErrUsageLimitNotFound indicates that no usage limit has the given ID. - ErrUsageLimitNotFound = errors.New("usage limit not found") - // ErrUsageLimitAlreadyExists indicates that the ID, or the metric and user - // pair, is already taken by another usage limit. - ErrUsageLimitAlreadyExists = errors.New("usage limit already exists") - // ErrUsageLimitsDeploymentConfigured indicates that the usage limits come - // from a deployment file and cannot be changed at runtime. - ErrUsageLimitsDeploymentConfigured = errors.New("usage limits are configured by the deployment") + // ErrInvalidUsageLimits indicates a usage limits setting that breaks a + // validation rule. + ErrInvalidUsageLimits = errors.New("invalid usage limits") // ErrUsageLimitWriteConflict indicates that other writers changed the usage // limits on every attempt. ErrUsageLimitWriteConflict = errors.New("usage limits changed concurrently; retry") - // ErrInvalidUsageLimit indicates a usage limit that breaks a validation rule. - ErrInvalidUsageLimit = errors.New("invalid usage limit") ) -// ValidateInstanceUsageLimitsSetting checks the rules every stored list of -// usage limits meets, whether it comes from the API or from a deployment file: -// at most MaxUsageLimits entries, valid and unique IDs, known metrics, -// non-negative values, an explicit all-users target or a positive user ID, -// and at most one limit per metric and target. +// DefaultInstanceUsageLimitsSetting is the setting in effect when none is +// stored: one entry for every user that limits memo content to 8 KiB and +// uploads to 32 MiB. +func DefaultInstanceUsageLimitsSetting() *storepb.InstanceUsageLimitsSetting { + uploadBytes, contentBytes := DefaultUploadBytes, DefaultMemoContentBytes + return &storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{{ + AllUsers: true, + MemoContentBytes: &contentBytes, + UploadBytes: &uploadBytes, + }}} +} + +// ValidateInstanceUsageLimitsSetting checks the rules every stored usage +// limits setting meets, whether it comes from the API or from a deployment +// file: at most MaxUsageLimitEntries entries naming at most MaxUsageLimitUsers +// users, every entry names positive, unrepeated users or all users but not +// both, the last entry and only the last entry names all users, and no limit +// is negative. func ValidateInstanceUsageLimitsSetting(setting *storepb.InstanceUsageLimitsSetting) error { + if setting == nil { + return errors.Wrap(ErrInvalidUsageLimits, "usage limits must be populated") + } limits := setting.GetLimits() - if len(limits) > MaxUsageLimits { - return errors.Wrapf(ErrInvalidUsageLimit, "at most %d usage limits are allowed", MaxUsageLimits) + if len(limits) == 0 { + return errors.Wrap(ErrInvalidUsageLimits, "the last entry must apply to all users") } - type target struct { - metric storepb.InstanceUsageLimitsSetting_Metric - userID int32 + if len(limits) > MaxUsageLimitEntries { + return errors.Wrapf(ErrInvalidUsageLimits, "at most %d entries are allowed", MaxUsageLimitEntries) } - uids := make(map[string]struct{}, len(limits)) - targets := make(map[target]struct{}, len(limits)) + userCount := 0 for i, limit := range limits { if limit == nil { - return errors.Wrapf(ErrInvalidUsageLimit, "limits[%d] must not be null", i) - } - if len(limit.Uid) > maxUsageLimitUIDLength || !UsageLimitUIDMatcher.MatchString(limit.Uid) { - return errors.Wrapf(ErrInvalidUsageLimit, "limits[%d].uid %q must be lowercase kebab case, start with a letter, and have at most %d characters", i, limit.Uid, maxUsageLimitUIDLength) - } - if !IsValidUsageLimitMetric(limit.Metric) { - return errors.Wrapf(ErrInvalidUsageLimit, "limits[%d].metric must be MEMO_COUNT, STORAGE_BYTES, UPLOAD_BYTES, or MEMO_CONTENT_BYTES", i) - } - switch target := limit.Target.(type) { - case *storepb.InstanceUsageLimitsSetting_UsageLimit_AllUsers: - if target == nil || target.AllUsers == nil { - return errors.Wrapf(ErrInvalidUsageLimit, "limits[%d].allUsers must be populated", i) + return errors.Wrapf(ErrInvalidUsageLimits, "limits[%d] must not be null", i) + } + last := i == len(limits)-1 + switch { + case limit.AllUsers && len(limit.UserIds) > 0: + return errors.Wrapf(ErrInvalidUsageLimits, "limits[%d] names users and all users; choose one", i) + case limit.AllUsers && !last: + return errors.Wrapf(ErrInvalidUsageLimits, "limits[%d] applies to all users, so it must be the last entry", i) + case !limit.AllUsers && last: + return errors.Wrap(ErrInvalidUsageLimits, "the last entry must apply to all users") + case !limit.AllUsers && len(limit.UserIds) == 0: + return errors.Wrapf(ErrInvalidUsageLimits, "limits[%d] must name at least one user", i) + } + // The cap comes before the duplicate scan, so an oversized entry is + // refused before its quadratic check runs. + userCount += len(limit.UserIds) + if userCount > MaxUsageLimitUsers { + return errors.Wrapf(ErrInvalidUsageLimits, "at most %d users can be named across all entries", MaxUsageLimitUsers) + } + for j, userID := range limit.UserIds { + if userID <= 0 { + return errors.Wrapf(ErrInvalidUsageLimits, "limits[%d].userIds[%d] must be positive", i, j) } - case *storepb.InstanceUsageLimitsSetting_UsageLimit_UserId: - if target == nil || target.UserId <= 0 { - return errors.Wrapf(ErrInvalidUsageLimit, "limits[%d].userId must be positive", i) + if slices.Contains(limit.UserIds[:j], userID) { + return errors.Wrapf(ErrInvalidUsageLimits, "limits[%d] names user %d more than once", i, userID) } - default: - return errors.Wrapf(ErrInvalidUsageLimit, "limits[%d].target must be allUsers or userId", i) - } - if limit.Value < 0 { - return errors.Wrapf(ErrInvalidUsageLimit, "limits[%d].value must not be negative", i) } - if _, ok := uids[limit.Uid]; ok { - return errors.Wrapf(ErrUsageLimitAlreadyExists, "usage limit ID %q is used more than once", limit.Uid) - } - uids[limit.Uid] = struct{}{} - key := target{metric: limit.Metric, userID: limit.GetUserId()} - if _, ok := targets[key]; ok { - return errors.Wrapf(ErrUsageLimitAlreadyExists, "more than one %s limit for user ID %d", limit.Metric, limit.GetUserId()) + for _, value := range []struct { + name string + value *int64 + }{ + {"memoCount", limit.MemoCount}, + {"memoContentBytes", limit.MemoContentBytes}, + {"storageBytes", limit.StorageBytes}, + {"uploadBytes", limit.UploadBytes}, + } { + if value.value != nil && *value.value < 0 { + return errors.Wrapf(ErrInvalidUsageLimits, "limits[%d].%s must not be negative", i, value.name) + } } - targets[key] = struct{}{} } return nil } -// IsValidUsageLimitMetric reports whether metric is one that limits can be set for. -func IsValidUsageLimitMetric(metric storepb.InstanceUsageLimitsSetting_Metric) bool { - switch metric { - case storepb.InstanceUsageLimitsSetting_MEMO_COUNT, - storepb.InstanceUsageLimitsSetting_STORAGE_BYTES, - storepb.InstanceUsageLimitsSetting_UPLOAD_BYTES, - storepb.InstanceUsageLimitsSetting_MEMO_CONTENT_BYTES: - return true - default: - return false - } -} - -// GetInstanceUsageLimitsSetting returns every usage limit. A deployment file -// takes precedence over the database. The database row is read directly, +// GetInstanceUsageLimitsSetting returns the usage limits setting. A deployment +// file takes precedence over the database. The database row is read directly, // without the instance setting cache, so a change made through any replica -// applies at the next check on every replica. An absent setting defaults to -// a 32 MiB per-file limit and an 8 KiB per-memo content limit. Existing settings, -// including empty lists, are preserved. +// applies at the next check on every replica. An absent setting is +// DefaultInstanceUsageLimitsSetting. func (s *Store) GetInstanceUsageLimitsSetting(ctx context.Context) (*storepb.InstanceUsageLimitsSetting, error) { instanceSetting := s.getDeploymentInstanceSetting(storepb.InstanceSettingKey_USAGE_LIMITS) if instanceSetting == nil { @@ -149,144 +133,54 @@ func (s *Store) GetInstanceUsageLimitsSetting(ctx context.Context) (*storepb.Ins } return setting, nil } - return newDefaultInstanceUsageLimitsSetting(), nil + return DefaultInstanceUsageLimitsSetting(), nil } -// CreateUsageLimit adds a usage limit. create.Uid, Metric, Target, and Value -// must be set; the create and update times are set here. -func (s *Store) CreateUsageLimit(ctx context.Context, create *storepb.InstanceUsageLimitsSetting_UsageLimit) (*storepb.InstanceUsageLimitsSetting_UsageLimit, error) { - if err := ValidateInstanceUsageLimitsSetting(&storepb.InstanceUsageLimitsSetting{Limits: []*storepb.InstanceUsageLimitsSetting_UsageLimit{create}}); err != nil { - return nil, err - } - var created *storepb.InstanceUsageLimitsSetting_UsageLimit - err := s.mutateUsageLimits(ctx, func(limits []*storepb.InstanceUsageLimitsSetting_UsageLimit) ([]*storepb.InstanceUsageLimitsSetting_UsageLimit, bool, error) { - for _, limit := range limits { - if limit.Uid == create.Uid { - return nil, false, errors.Wrapf(ErrUsageLimitAlreadyExists, "usage limit ID %q already exists", create.Uid) - } - if limit.Metric == create.Metric && limit.GetUserId() == create.GetUserId() { - return nil, false, errors.Wrapf(ErrUsageLimitAlreadyExists, "usage limit %q already sets %s for this user", limit.Uid, limit.Metric) - } - } - now := timestamppb.Now() - created = proto.CloneOf(create) - created.CreateTime = now - created.UpdateTime = now - return append(limits, created), true, nil - }) - if err != nil { - return nil, err - } - return created, nil -} - -// UpdateUsageLimit sets the value of the usage limit uid. -func (s *Store) UpdateUsageLimit(ctx context.Context, uid string, value int64) (*storepb.InstanceUsageLimitsSetting_UsageLimit, error) { - var updated *storepb.InstanceUsageLimitsSetting_UsageLimit - err := s.mutateUsageLimits(ctx, func(limits []*storepb.InstanceUsageLimitsSetting_UsageLimit) ([]*storepb.InstanceUsageLimitsSetting_UsageLimit, bool, error) { - index := slices.IndexFunc(limits, func(limit *storepb.InstanceUsageLimitsSetting_UsageLimit) bool { return limit.Uid == uid }) - if index < 0 { - return nil, false, ErrUsageLimitNotFound - } - updated = limits[index] - if updated.Value == value { - return limits, false, nil - } - updated.Value = value - updated.UpdateTime = timestamppb.Now() - return limits, true, nil - }) - if err != nil { - return nil, err - } - return updated, nil -} - -// DeleteUsageLimit removes the usage limit uid. -func (s *Store) DeleteUsageLimit(ctx context.Context, uid string) error { - return s.mutateUsageLimits(ctx, func(limits []*storepb.InstanceUsageLimitsSetting_UsageLimit) ([]*storepb.InstanceUsageLimitsSetting_UsageLimit, bool, error) { - next := slices.DeleteFunc(limits, func(limit *storepb.InstanceUsageLimitsSetting_UsageLimit) bool { return limit.Uid == uid }) - if len(next) == len(limits) { - return nil, false, ErrUsageLimitNotFound - } - return next, true, nil - }) -} - -// DeleteUsageLimitsForUser removes every usage limit of userID. It does -// nothing when the limits come from a deployment file, because the store -// cannot change them; entries for a missing user are ignored at evaluation. -func (s *Store) DeleteUsageLimitsForUser(ctx context.Context, userID int32) error { +// RemoveUserFromUsageLimits drops userID from every entry of the stored usage +// limits, and drops entries left without users. It does nothing when the +// limits come from a deployment file, because the store cannot change them; +// an entry naming a missing user never matches anyone. +func (s *Store) RemoveUserFromUsageLimits(ctx context.Context, userID int32) error { if userID == 0 || s.IsInstanceSettingDeploymentConfigured(storepb.InstanceSettingKey_USAGE_LIMITS) { return nil } - return s.mutateUsageLimits(ctx, func(limits []*storepb.InstanceUsageLimitsSetting_UsageLimit) ([]*storepb.InstanceUsageLimitsSetting_UsageLimit, bool, error) { - before := len(limits) - next := slices.DeleteFunc(limits, func(limit *storepb.InstanceUsageLimitsSetting_UsageLimit) bool { return limit.GetUserId() == userID }) - return next, len(next) != before, nil - }) -} - -// usageLimitsMutation receives a private copy of the stored limits and returns -// the new list and whether it changed. Returning an error aborts the write. -type usageLimitsMutation func(limits []*storepb.InstanceUsageLimitsSetting_UsageLimit) ([]*storepb.InstanceUsageLimitsSetting_UsageLimit, bool, error) - -// mutateUsageLimits applies mutate as a read, modify, and conditional write of -// the USAGE_LIMITS row. The write succeeds only if the row is unchanged since -// it was read; otherwise the whole step runs again, so concurrent writers on -// any replica never overwrite each other. Uniqueness and the cap are checked -// on the list that is written, which makes them safe across replicas. -func (s *Store) mutateUsageLimits(ctx context.Context, mutate usageLimitsMutation) error { - if s.IsInstanceSettingDeploymentConfigured(storepb.InstanceSettingKey_USAGE_LIMITS) { - return ErrUsageLimitsDeploymentConfigured - } name := storepb.InstanceSettingKey_USAGE_LIMITS.String() for range usageLimitWriteAttempts { list, err := s.driver.ListInstanceSettings(ctx, &FindInstanceSetting{Name: name}) if err != nil { return errors.Wrap(err, "failed to read usage limits") } + if len(list) == 0 { + return nil + } + oldValue := list[0].Value current := &storepb.InstanceUsageLimitsSetting{} - var oldValue string - exists := len(list) > 0 - if exists { - oldValue = list[0].Value - if err := protojsonUnmarshaler.Unmarshal([]byte(oldValue), current); err != nil { - return errors.Wrap(err, "failed to parse stored usage limits") + if err := protojsonUnmarshaler.Unmarshal([]byte(oldValue), current); err != nil { + return errors.Wrap(err, "failed to parse stored usage limits") + } + changed := false + next := make([]*storepb.InstanceUsageLimitsSetting_UsageLimit, 0, len(current.Limits)) + for _, limit := range current.Limits { + if limit == nil { + continue } - } else { - current = newDefaultInstanceUsageLimitsSetting() - } - if err := ValidateInstanceUsageLimitsSetting(current); err != nil { - return errors.Wrap(err, "invalid stored usage limits") - } - - next, changed, err := mutate(current.Limits) - if err != nil { - return err - } - pruned, err := s.pruneUsageLimitsOfMissingUsers(ctx, next) - if err != nil { - return err + if slices.Contains(limit.UserIds, userID) { + changed = true + limit.UserIds = slices.DeleteFunc(limit.UserIds, func(id int32) bool { return id == userID }) + if !limit.AllUsers && len(limit.UserIds) == 0 { + continue + } + } + next = append(next, limit) } - if !changed && len(pruned) == len(next) { + if !changed { return nil } - setting := &storepb.InstanceUsageLimitsSetting{Limits: pruned} - if err := ValidateInstanceUsageLimitsSetting(setting); err != nil { - return err - } - valueBytes, err := protojson.Marshal(setting) + valueBytes, err := protojson.Marshal(&storepb.InstanceUsageLimitsSetting{Limits: next}) if err != nil { return errors.Wrap(err, "failed to marshal usage limits") } - - var written bool - if exists { - written, err = s.driver.UpdateInstanceSettingIfUnchanged(ctx, name, oldValue, string(valueBytes)) - } else { - written, err = s.driver.CreateInstanceSettingIfNotExists(ctx, &InstanceSetting{Name: name, Value: string(valueBytes)}) - } + written, err := s.driver.UpdateInstanceSettingIfUnchanged(ctx, name, oldValue, string(valueBytes)) if err != nil { return errors.Wrap(err, "failed to write usage limits") } @@ -294,36 +188,6 @@ func (s *Store) mutateUsageLimits(ctx context.Context, mutate usageLimitsMutatio s.instanceSettingCache.Delete(ctx, name) return nil } - slog.Debug("usage limits changed concurrently; retrying") } return ErrUsageLimitWriteConflict } - -// pruneUsageLimitsOfMissingUsers drops limits whose user no longer exists, so -// entries left behind by a failed cleanup do not count toward the cap forever. -func (s *Store) pruneUsageLimitsOfMissingUsers(ctx context.Context, limits []*storepb.InstanceUsageLimitsSetting_UsageLimit) ([]*storepb.InstanceUsageLimitsSetting_UsageLimit, error) { - userIDs := []int32{} - for _, limit := range limits { - if limit.GetUserId() != 0 && !slices.Contains(userIDs, limit.GetUserId()) { - userIDs = append(userIDs, limit.GetUserId()) - } - } - if len(userIDs) == 0 { - return limits, nil - } - users, err := s.driver.ListUsers(ctx, &FindUser{IDList: userIDs}) - if err != nil { - return nil, errors.Wrap(err, "failed to list users of usage limits") - } - existing := make(map[int32]struct{}, len(users)) - for _, user := range users { - existing[user.ID] = struct{}{} - } - return slices.DeleteFunc(limits, func(limit *storepb.InstanceUsageLimitsSetting_UsageLimit) bool { - if limit.GetUserId() == 0 { - return false - } - _, ok := existing[limit.GetUserId()] - return !ok - }), nil -} diff --git a/store/user.go b/store/user.go index eaf5abf97c3e5..001cc141cb823 100644 --- a/store/user.go +++ b/store/user.go @@ -243,10 +243,10 @@ func (s *Store) DeleteUser(ctx context.Context, delete *DeleteUser) (*DeleteUser return nil, errors.New("unexpected nil delete user result") } s.deleteUserCache(ctx, delete.ID, result) - // The user is already gone, so a failure here only leaves limits that are - // ignored at evaluation and pruned by the next usage limit write. - if err := s.DeleteUsageLimitsForUser(ctx, delete.ID); err != nil { - slog.Warn("failed to delete usage limits of deleted user", slog.Int("userID", int(delete.ID)), slog.String("error", err.Error())) + // The user is already gone, so a failure here only leaves an ID that + // never matches anyone. + if err := s.RemoveUserFromUsageLimits(ctx, delete.ID); err != nil { + slog.Warn("failed to remove deleted user from usage limits", slog.Int("userID", int(delete.ID)), slog.String("error", err.Error())) } return result, nil } diff --git a/web/src/components/AppSidebar/SpaceSwitcher.tsx b/web/src/components/AppSidebar/SpaceSwitcher.tsx index 35f42e8f8a9e0..25ef3d63b40b2 100644 --- a/web/src/components/AppSidebar/SpaceSwitcher.tsx +++ b/web/src/components/AppSidebar/SpaceSwitcher.tsx @@ -152,6 +152,7 @@ function SpaceSwitcher({ className, size = "md" }: { className?: string; size?: {selectedSpaceName ? spaceLabel : allSpacesLabel} - + diff --git a/web/src/components/Settings/UsageLimitDialog.tsx b/web/src/components/Settings/UsageLimitDialog.tsx index 5efdd68996eaa..16093eac97d68 100644 --- a/web/src/components/Settings/UsageLimitDialog.tsx +++ b/web/src/components/Settings/UsageLimitDialog.tsx @@ -1,126 +1,89 @@ +import { XIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; -import { toast } from "react-hot-toast"; +import UserAvatar from "@/components/UserAvatar"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { findUsageLimit, USAGE_LIMIT_METRICS, useSetUsageLimit, useUsageLimits } from "@/hooks/useUsageLimitQueries"; -import { handleError } from "@/lib/error"; -import { UsageLimit_Metric } from "@/types/proto/api/instance_service_pb"; +import type { UsageLimitEntry } from "@/hooks/useUsageLimitQueries"; +import { extractUsernameFromName } from "@/lib/resource-names"; import type { User } from "@/types/proto/api/user_service_pb"; import { useTranslate } from "@/utils/i18n"; -import { parseLimitInput, toLimitInput } from "./usageLimitValues"; +import { newUsageLimitEntry, parseLimitInput, toLimitInput, USAGE_METRICS, type UsageMetric } from "./usageLimitValues"; -type Inputs = Record; - -/** Which row the dialog edits: the limits for all users, one user's limits, or a user still to pick. */ -export type UsageLimitDialogTarget = { kind: "all" } | { kind: "user"; user: User } | { kind: "add" }; +type Inputs = Record; interface Props { open: boolean; onOpenChange: (open: boolean) => void; - target: UsageLimitDialogTarget; - /** The users that can be added, which are those without limits of their own. */ - candidates: User[]; + /** The entry to edit, or undefined to add one. */ + entry?: UsageLimitEntry; + /** The all-users entry, which pre-fills a new entry's fields. */ + defaults: UsageLimitEntry; + users: User[]; + saving: boolean; + onSave: (entry: UsageLimitEntry) => Promise; } const userLabel = (user: User) => (user.displayName ? `${user.displayName} (@${user.username})` : `@${user.username}`); +const inputsOf = (entry: UsageLimitEntry): Inputs => + Object.fromEntries(USAGE_METRICS.map((metric) => [metric, toLimitInput(metric, entry[metric])])) as Inputs; + /** - * Edits one row of the usage limits table. For all users, an empty field - * means no limit. For one user, an empty field means the all-users limit - * applies, and 0 means no limit for that user. + * Edits one entry of the usage limits list: the users it names and one value + * per metric. An empty field means no limit. A new entry starts from the + * default's values so that leaving a field alone keeps the default rather than + * removing the limit. */ -function UsageLimitDialog({ open, onOpenChange, target, candidates }: Props) { +function UsageLimitDialog({ open, onOpenChange, entry, defaults, users, saving, onSave }: Props) { const t = useTranslate(); - const { data: limits } = useUsageLimits({ enabled: open }); - const [pickedName, setPickedName] = useState(""); - const setUsageLimit = useSetUsageLimit(); + const forAll = entry?.allUsers === true; + // The entry the fields start from: the one being edited, or the default + // for a new entry. + const source = entry ?? defaults; + const sourceInputs = useMemo(() => inputsOf(source), [source]); + const [names, setNames] = useState([]); + const [inputs, setInputs] = useState(sourceInputs); useEffect(() => { - if (open) setPickedName(""); - }, [open]); + if (!open) return; + setNames(entry?.users ?? []); + setInputs(sourceInputs); + }, [open, entry, sourceInputs]); - const forAll = target.kind === "all"; - const user = target.kind === "user" ? target.user : candidates.find((candidate) => candidate.name === pickedName); - // The user the limits belong to: empty for all users, undefined until a user is picked. - const owner = forAll ? "" : user?.name; - - const savedInputs = useMemo( - () => - Object.fromEntries( - USAGE_LIMIT_METRICS.map((metric) => { - const value = owner === undefined ? undefined : findUsageLimit(limits, metric, owner)?.value; - // For a user, empty means "use the all-users limit", so a user limit of 0 (no limit) shows as 0. - return [metric, !forAll && value === 0n ? "0" : toLimitInput(metric, value)]; - }), - ) as Inputs, - [limits, owner, forAll], - ); - const [inputs, setInputs] = useState(savedInputs); - useEffect(() => setInputs(savedInputs), [savedInputs]); + const usersByName = useMemo(() => new Map(users.map((user) => [user.name, user])), [users]); + const candidateItems = users.filter((user) => !names.includes(user.name)).map((user) => ({ value: user.name, label: userLabel(user) })); - /** The value to save: undefined removes the limit. */ - const savedValue = (metric: UsageLimit_Metric): bigint | undefined => { - const value = parseLimitInput(metric, inputs[metric]); - // A limit of 0 for all users is the same as none, so it is removed. - if (forAll) return value ? value : undefined; - return inputs[metric].trim() === "" ? undefined : value; - }; - const isInvalid = (metric: UsageLimit_Metric) => parseLimitInput(metric, inputs[metric]) === undefined; - const placeholder = (metric: UsageLimit_Metric) => { - if (forAll) return t("setting.usage-limits.unlimited"); - const value = findUsageLimit(limits, metric)?.value; - return t("setting.usage-limits.all-users-placeholder", { - value: value ? toLimitInput(metric, value) : t("setting.usage-limits.unlimited"), - }); - }; - const warning = (metric: UsageLimit_Metric) => (isInvalid(metric) ? t("setting.usage-limits.invalid") : undefined); + const parsed = (metric: UsageMetric) => parseLimitInput(metric, inputs[metric]); + const isInvalid = (metric: UsageMetric) => parsed(metric) === null; + const isValid = USAGE_METRICS.every((metric) => !isInvalid(metric)) && (forAll || names.length > 0); - const hasChanges = USAGE_LIMIT_METRICS.some((metric) => inputs[metric] !== savedInputs[metric]); - const isValid = USAGE_LIMIT_METRICS.every((metric) => !isInvalid(metric)); + // The field text is a rounded view of a byte count, so a field the admin + // did not touch keeps the stored value instead of a re-parsed one. + const savedValue = (metric: UsageMetric) => (inputs[metric] === sourceInputs[metric] ? source[metric] : (parsed(metric) ?? undefined)); const handleSave = async () => { - if (owner === undefined) return; - try { - for (const metric of USAGE_LIMIT_METRICS) { - if (inputs[metric] === savedInputs[metric]) continue; - await setUsageLimit.mutateAsync({ - metric, - user: owner, - value: savedValue(metric), - existing: findUsageLimit(limits, metric, owner), - }); - } - toast.success(t("setting.usage-limits.saved")); - onOpenChange(false); - } catch (error) { - handleError(error, toast.error, { context: "Update usage limits" }); - } + const next = newUsageLimitEntry({ + users: forAll ? [] : names, + allUsers: forAll, + ...Object.fromEntries(USAGE_METRICS.map((metric) => [metric, savedValue(metric)])), + }); + await onSave(next); }; - const fields = [ - { metric: UsageLimit_Metric.MEMO_COUNT, label: t("setting.usage-limits.memos"), help: t("setting.usage-limits.memos-description") }, - { - metric: UsageLimit_Metric.MEMO_CONTENT_BYTES, - label: t("setting.usage-limits.content-size"), - help: t("setting.usage-limits.content-size-description"), - }, - { - metric: UsageLimit_Metric.STORAGE_BYTES, - label: t("setting.usage-limits.storage"), - help: t("setting.usage-limits.storage-description"), - }, - { metric: UsageLimit_Metric.UPLOAD_BYTES, label: t("setting.usage-limits.file-size"), help: undefined }, + const fields: { metric: UsageMetric; label: string; help?: string }[] = [ + { metric: "memoCount", label: t("setting.usage-limits.memos"), help: t("setting.usage-limits.memos-description") }, + { metric: "memoContentBytes", label: t("setting.usage-limits.content-size"), help: t("setting.usage-limits.content-size-description") }, + { metric: "storageBytes", label: t("setting.usage-limits.storage"), help: t("setting.usage-limits.storage-description") }, + { metric: "uploadBytes", label: t("setting.usage-limits.file-size") }, ]; - const userItems = candidates.map((candidate) => ({ value: candidate.name, label: userLabel(candidate) })); - const title = - target.kind === "all" - ? t("setting.usage-limits.all-users-title") - : target.kind === "user" - ? t("setting.usage-limits.user-title", { user: target.user.displayName || target.user.username }) - : t("setting.usage-limits.add-user"); + const title = forAll + ? t("setting.usage-limits.default-title") + : entry + ? t("setting.usage-limits.edit-title") + : t("setting.usage-limits.add-title"); return ( @@ -129,26 +92,53 @@ function UsageLimitDialog({ open, onOpenChange, target, candidates }: Props) { {title}
- {target.kind === "add" && ( + {!forAll && (
- - name && setNames((current) => [...current, name])}> + - {userItems.map((item) => ( + {candidateItems.map((item) => ( {item.label} ))} - {userItems.length === 0 &&

{t("setting.usage-limits.no-candidates")}

} +

+ {names.length === 0 ? t("setting.usage-limits.users-required") : t("setting.usage-limits.users-description")} +

)} {fields.map(({ metric, label, help }) => { - const message = warning(metric) ?? help; + const message = isInvalid(metric) ? t("setting.usage-limits.invalid") : help; return (
@@ -156,8 +146,7 @@ function UsageLimitDialog({ open, onOpenChange, target, candidates }: Props) { id={`usage-limit-${metric}`} className="font-mono" inputMode="decimal" - disabled={owner === undefined} - placeholder={placeholder(metric)} + placeholder={t("setting.usage-limits.unlimited")} value={inputs[metric]} aria-invalid={isInvalid(metric)} aria-describedby={message ? `usage-limit-help-${metric}` : undefined} @@ -172,14 +161,14 @@ function UsageLimitDialog({ open, onOpenChange, target, candidates }: Props) { ); })}

- {forAll ? t("setting.usage-limits.all-users-hint") : t("setting.usage-limits.user-hint")} + {forAll ? t("setting.usage-limits.default-hint") : t("setting.usage-limits.entry-hint")}

- diff --git a/web/src/components/Settings/UsageLimitsSection.tsx b/web/src/components/Settings/UsageLimitsSection.tsx index 061c2b2554e8e..c37093e02e484 100644 --- a/web/src/components/Settings/UsageLimitsSection.tsx +++ b/web/src/components/Settings/UsageLimitsSection.tsx @@ -1,107 +1,159 @@ -import { sortBy } from "lodash-es"; -import { MoreVerticalIcon, PlusIcon, UsersIcon } from "lucide-react"; +import { ArrowDownIcon, ArrowUpIcon, MoreVerticalIcon, PlusIcon, UsersIcon } from "lucide-react"; import { useMemo, useState } from "react"; import { toast } from "react-hot-toast"; import ConfirmDialog from "@/components/ConfirmDialog"; import UserAvatar from "@/components/UserAvatar"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { findUsageLimit, USAGE_LIMIT_METRICS, useSetUsageLimit, useUsageLimits } from "@/hooks/useUsageLimitQueries"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { type UsageLimitEntry, useSaveUsageLimits, useUsageLimits } from "@/hooks/useUsageLimitQueries"; import { useListUsers } from "@/hooks/useUserQueries"; import { handleError } from "@/lib/error"; -import { cn } from "@/lib/utils"; -import { UsageLimit_Metric } from "@/types/proto/api/instance_service_pb"; -import { User } from "@/types/proto/api/user_service_pb"; +import { extractUsernameFromName } from "@/lib/resource-names"; +import type { User } from "@/types/proto/api/user_service_pb"; import { useTranslate } from "@/utils/i18n"; import SettingSection from "./SettingSection"; import SettingTable from "./SettingTable"; -import UsageLimitDialog, { type UsageLimitDialogTarget } from "./UsageLimitDialog"; -import { formatLimitValue } from "./usageLimitValues"; +import UsageLimitDialog from "./UsageLimitDialog"; +import { formatLimitValue, isLimited, type UsageMetric } from "./usageLimitValues"; -/** - * A table row: name is empty for the all-users row, otherwise users/{user}. - * The user is missing when the user list has not loaded. - */ -type LimitRow = { name: string; user?: User } & Record; +/** A table row: one entry and its position in the list. */ +type LimitRow = { index: number; entry: UsageLimitEntry; shadowed: boolean } & Record; + +const metricColumns: { + key: string; + metric: UsageMetric; + headerKey: + | "setting.usage-limits.memos" + | "setting.usage-limits.content-size-column" + | "setting.usage-limits.storage-column" + | "setting.usage-limits.file-size-column"; +}[] = [ + { key: "memos", metric: "memoCount", headerKey: "setting.usage-limits.memos" }, + { key: "contentSize", metric: "memoContentBytes", headerKey: "setting.usage-limits.content-size-column" }, + { key: "storage", metric: "storageBytes", headerKey: "setting.usage-limits.storage-column" }, + { key: "fileSize", metric: "uploadBytes", headerKey: "setting.usage-limits.file-size-column" }, +]; /** - * Edits the usage limits as one table. The first row holds the limits for all - * users; each further row holds one user's own limits, which replace them. + * Edits the usage limits as the ordered list the server checks: the first + * entry that names a user is that user's limit set, and the last entry, for + * all users, is the default. Every change replaces the whole list. */ const UsageLimitsSection = () => { const t = useTranslate(); - const { data: limits, isLoading } = useUsageLimits(); + const { limits, isLoading } = useUsageLimits(); const { data: users = [] } = useListUsers(); - const setUsageLimit = useSetUsageLimit(); - const [dialog, setDialog] = useState<{ open: boolean; target: UsageLimitDialogTarget }>({ open: false, target: { kind: "all" } }); - const [removeTarget, setRemoveTarget] = useState(); - - const userRows = useMemo(() => { - const names = [...new Set((limits ?? []).filter((limit) => limit.user).map((limit) => limit.user))]; - return sortBy( - names.map((name) => ({ name, user: users.find((user) => user.name === name) })), - (row) => row.user?.username ?? row.name, - ); - }, [limits, users]); - const candidates = useMemo( - () => - sortBy( - users.filter((user) => !userRows.some((row) => row.name === user.name)), - "username", - ), - [users, userRows], - ); + const save = useSaveUsageLimits(); + const [dialog, setDialog] = useState<{ open: boolean; index?: number }>({ open: false }); + const [removeIndex, setRemoveIndex] = useState(); - const openDialog = (target: UsageLimitDialogTarget) => setDialog({ open: true, target }); + const usersByName = useMemo(() => new Map(users.map((user) => [user.name, user])), [users]); + const rows = useMemo(() => { + const seen = new Set(); + return (limits ?? []).map((entry, index) => { + const shadowed = entry.users.length > 0 && entry.users.every((name) => seen.has(name)); + for (const name of entry.users) seen.add(name); + return { index, entry, shadowed }; + }); + }, [limits]); + const defaults = limits?.find((entry) => entry.allUsers); + const lastMovable = (limits?.length ?? 1) - 2; - const handleRemove = async () => { - if (!removeTarget) return; + const replace = async (next: UsageLimitEntry[], successKey: "setting.usage-limits.saved" | "setting.usage-limits.removed") => { try { - for (const metric of USAGE_LIMIT_METRICS) { - const existing = findUsageLimit(limits, metric, removeTarget.name); - if (existing) await setUsageLimit.mutateAsync({ metric, user: removeTarget.name, value: undefined, existing }); - } - toast.success(t("setting.usage-limits.removed")); + await save.mutateAsync(next); + toast.success(t(successKey)); + return true; } catch (error) { - handleError(error, toast.error, { context: "Remove usage limits" }); + handleError(error, toast.error, { context: "Update usage limits" }); + return false; } - setRemoveTarget(undefined); }; - const usernameOf = (row: LimitRow) => row.user?.username ?? row.name.replace(/^users\//, ""); + const move = (index: number, direction: -1 | 1) => { + if (!limits) return; + const target = index + direction; + if (target < 0 || target > lastMovable) return; + const next = [...limits]; + [next[index], next[target]] = [next[target], next[index]]; + void replace(next, "setting.usage-limits.saved"); + }; + + const handleSave = async (entry: UsageLimitEntry) => { + if (!limits) return; + const next = [...limits]; + if (dialog.index === undefined) { + // A new entry goes right above the default, where it is checked last + // among the named entries; the admin moves it up if it should win. + next.splice(next.length - 1, 0, entry); + } else { + next[dialog.index] = entry; + } + if (await replace(next, "setting.usage-limits.saved")) { + setDialog({ open: false }); + } + }; + + const handleRemove = async () => { + if (!limits || removeIndex === undefined) return; + await replace( + limits.filter((_, index) => index !== removeIndex), + "setting.usage-limits.removed", + ); + setRemoveIndex(undefined); + }; + + const renderUser = (name: string, user: User | undefined) => { + const label = user?.displayName || user?.username || extractUsernameFromName(name); + return ( +
  • + + {label} +
  • + ); + }; - // A user row shows its own limit, or the all-users limit in gray when it has none. - const renderLimit = (metric: UsageLimit_Metric) => (_: unknown, row: LimitRow) => { - const own = findUsageLimit(limits, metric, row.name); - const value = (own ?? findUsageLimit(limits, metric))?.value ?? 0n; - const inherited = row.name !== "" && !own; + const renderLimit = (metric: UsageMetric) => (_: unknown, row: LimitRow) => { + const value = row.entry[metric]; return ( - - {value > 0n ? formatLimitValue(metric, value) : t("setting.usage-limits.unlimited")} + + {isLimited(value) ? formatLimitValue(metric, value) : t("setting.usage-limits.unlimited")} ); }; - const rows: LimitRow[] = [{ name: "" }, ...userRows]; - return ( openDialog({ kind: "add" })}> + } > +

    {t("setting.usage-limits.order-note")}

    {row.index + 1}, + }, + { + key: "users", + header: t("setting.usage-limits.applies-to"), render: (_, row: LimitRow) => - row.name === "" ? ( + row.entry.allUsers ? (
    @@ -112,34 +164,23 @@ const UsageLimitsSection = () => {
    ) : ( -
    - -
    - {row.user?.displayName || usernameOf(row)} - @{usernameOf(row)} -
    +
    +
      {row.entry.users.map((name) => renderUser(name, usersByName.get(name)))}
    + {row.shadowed && ( + + {t("setting.usage-limits.never-applies")} + + )}
    ), }, - { key: "memos", header: t("setting.usage-limits.memos"), render: renderLimit(UsageLimit_Metric.MEMO_COUNT) }, - { - key: "contentSize", - header: t("setting.usage-limits.content-size-column"), - render: renderLimit(UsageLimit_Metric.MEMO_CONTENT_BYTES), - }, - { key: "storage", header: t("setting.usage-limits.storage-column"), render: renderLimit(UsageLimit_Metric.STORAGE_BYTES) }, - { key: "fileSize", header: t("setting.usage-limits.file-size-column"), render: renderLimit(UsageLimit_Metric.UPLOAD_BYTES) }, + ...metricColumns.map(({ key, metric, headerKey }) => ({ key, header: t(headerKey), render: renderLimit(metric) })), { key: "actions", header: "", className: "w-px text-right", render: (_, row: LimitRow) => { - const name = row.name === "" ? t("setting.usage-limits.all-users") : usernameOf(row); - const user = row.user; + const name = row.entry.allUsers ? t("setting.usage-limits.all-users") : String(row.index + 1); return ( {