fix(submit): 已批准的投稿不再占上传者的存储额度,只计入存储总量

This commit is contained in:
Lemon-miaow committed 2026-09-27 13:38:34 +08:00
1 parent 0d485992c6
commit 372c8972f9
6 files changed
+82 -21

No files matched your search

+21 -15
View File
@@ -155,10 +155,12 @@ const (
// enough to stage a couple of packs, far short of a flood. Override per
// Manager via MaxPendingPerUser.
defaultMaxPendingPerUser = 5
// defaultMaxStoredBytesPerUser caps the total bytes one user's stored
// contexts may occupy on the uploads store. The uploads PVC renders at a
// fixed 5Gi (platform/workloads.go); without a per-user budget one account
// could fill it and every other user's upload would start failing. Two GiB
// defaultMaxStoredBytesPerUser caps the total bytes one user's unapproved
// contexts (pending, and rejected ones until ReapRejected takes them) may
// occupy on the uploads store; approved ones count only toward the total,
// since an admin chose to keep them. The uploads PVC renders at a fixed 5Gi
// (platform/workloads.go); without a per-user budget one account could fill
// it and every other user's upload would start failing. Two GiB
// leaves room for a couple of full-size modpacks (a single blob may be 1 GiB)
// while keeping a small user base from exhausting the volume; size the PVC
// above users × this budget before raising it. Override per Manager via
@@ -597,7 +599,7 @@ func (m *Manager) Create(ctx context.Context, req CreateRequest) (*Submission, e
// has already consumed it, once rejected it is dead;
// - the body must be a gzip tarball (context.tar.gz) and is size-capped, so a
// wrong-format or oversize upload is rejected as a 400 without persisting;
// - a user's stored contexts are budgeted (MaxStoredBytesPerUser): the write
// - a user's unapproved contexts are budgeted (MaxStoredBytesPerUser): the write
// is capped at the remaining budget, so an upload that would exceed it is
// refused as a spent allowance (403) before the excess is persisted;
// - so are everyone's together (MaxStoredBytesTotal), and a local store checks
@@ -825,15 +827,19 @@ func (m *Manager) ReapStaleParts(olderThan time.Duration) (int, error) {
return m.Parts.Reap(m.now().Add(-olderThan))
}
// storedBytes sums the stored-blob sizes of submittedBy's submissions (user) and
// of everyone's (total), excluding excludeID — the submission a pending re-upload
// is about to replace, whose bytes must not be counted twice. Sizes are read from
// the blob store itself, the same source of truth uploads/approval consult, so
// the sums cannot drift from what is actually occupying the volume (including
// blobs uploaded before any budget existed). A staged chunked upload counts as
// well, so parts spread over several pending submissions cannot hold more than
// the budget allows. With fresh unset, a blob size read or written within
// blobSizeTTL is used as it stands (blobSize). A size that cannot be read is
// storedBytes sums the stored-blob sizes of submittedBy's unapproved submissions
// (user) and of everyone's submissions (total), excluding excludeID — the
// submission a pending re-upload is about to replace, whose bytes must not be
// counted twice. An approved context leaves its uploader's budget: an admin chose
// to keep it (it rebuilds the image after a registry loss), and the uploader can
// neither withdraw nor replace it, so charging it would spend their allowance for
// good. It still fills the store, so it stays in total. Sizes are read from the
// blob store itself, the same source of truth uploads/approval consult, so the
// sums cannot drift from what is actually occupying the volume (including blobs
// uploaded before any budget existed). A staged chunked upload counts as well, so
// parts spread over several pending submissions cannot hold more than the budget
// allows. With fresh unset, a blob size read or written within blobSizeTTL is
// used as it stands (blobSize). A size that cannot be read is
// ErrStoreUnavailable, for the caller to try again.
func (m *Manager) storedBytes(ctx context.Context, submittedBy, excludeID string, fresh bool) (user, total int64, err error) {
subs, err := m.Store.ListSubmissions(ctx)
@@ -856,7 +862,7 @@ func (m *Manager) storedBytes(ctx context.Context, submittedBy, excludeID string
n += staged
}
total += n
if s.SubmittedBy == submittedBy {
if s.SubmittedBy == submittedBy && s.Status != StatusApproved {
user += n
}
}
+52
View File
@@ -887,6 +887,58 @@ func TestUploadContextStorageBudget(t *testing.T) {
}
}
// An approved context stays on the store for rebuilds, out of its uploader's
// hands; it leaves their budget, so approval gives the allowance back. It still
// fills the store's total, and a rejected context keeps its uploader's budget
// until it is reaped.
func TestApprovedContextsLeaveTheUploadersBudget(t *testing.T) {
m, _, _ := newManager()
fb := newFakeBlobs()
m.Blobs = fb
m.MaxStoredBytesPerUser = 5 // one gzBody("x") of 5 bytes
m.MaxStoredBytesTotal = 10
ctx := context.Background()
create := func(user string) *Submission {
t.Helper()
sub, err := m.Create(ctx, CreateRequest{DisplayName: "P", SubmittedBy: user})
if err != nil {
t.Fatalf("create for %s: %v", user, err)
}
return sub
}
upload := func(sub *Submission) (*Submission, error) {
return m.UploadContext(ctx, sub.ID, sub.SubmittedBy, strings.NewReader(gzBody("x")))
}
a, b := create("user-1"), create("user-1")
a, err := upload(a)
if err != nil {
t.Fatalf("upload A: %v", err)
}
if _, err := upload(b); !errors.Is(err, ErrQuotaExceeded) {
t.Fatalf("upload B beside a pending A = %v, want ErrQuotaExceeded", err)
}
if _, err := m.Approve(ctx, a.ID, "[email protected]", a.ContextSHA256); err != nil {
t.Fatalf("approve A: %v", err)
}
if _, err := upload(b); err != nil {
t.Fatalf("upload B beside an approved A = %v, want accepted", err)
}
// A (approved) and B hold the store's 10 bytes between them.
if _, err := upload(create("user-2")); !errors.Is(err, ErrUploadsFull) {
t.Fatalf("upload into a store the approved A helps fill = %v, want ErrUploadsFull", err)
}
if _, err := m.Reject(ctx, b.ID, "[email protected]", "no"); err != nil {
t.Fatalf("reject B: %v", err)
}
m.MaxStoredBytesTotal = 100
if _, err := upload(create("user-1")); !errors.Is(err, ErrQuotaExceeded) {
t.Fatalf("upload beside a rejected B = %v, want ErrQuotaExceeded", err)
}
}
// A single oversize blob stays a 400 (ErrInvalid), distinct from the 403 the
// per-user budget answers with — the two failure classes must not collapse.
func TestUploadContextOversizeIsNotQuotaError(t *testing.T) {