feat(api): 单次上传上限改为 context_max_bytes 可配,Cloudflare 边缘后默认 95Mi,新增 GET /me/submissions/limits 供面板预检

This commit is contained in:
Lemon-miaow committed 2026-09-25 18:31:10 +08:00
1 parent e7326e6315
commit 87dee3e5a5
10 files changed
+158 -2

No files matched your search

+1
View File
@@ -610,6 +610,7 @@ func (a *API) externalAPIRoutes() []apiRoute {
// session is the correct gate (the admin verdict lives below, behind adminOnly).
{Method: "POST", Pattern: "/api/v1/me/submissions", h: a.handleCreateSubmission},
{Method: "GET", Pattern: "/api/v1/me/submissions", h: a.handleMySubmissions},
{Method: "GET", Pattern: "/api/v1/me/submissions/limits", h: a.handleSubmissionLimits},
// The blob upload for a submission the caller owns: the request body is the
// raw gzip build context, streamed to the derived, id-namespaced location.
// App-tier and owner-scoped (the id must belong to the principal), exactly
+17
View File
@@ -202,6 +202,23 @@ func (a *API) handleUploadSubmissionContext(w http.ResponseWriter, r *http.Reque
// query to another user's uploads. Each row is enriched with its linked build's
// outcome — this list is the only player-visible outlet for a build result, so
// a failed build is not invisible to the person who submitted it.
// SubmissionLimits is what one upload may carry, read before sending it.
type SubmissionLimits struct {
MaxContextBytes int64 `json:"max_context_bytes"`
}
// handleSubmissionLimits reports the effective per-upload context cap
// ([registry] context_max_bytes), so the panel can refuse an oversized file
// before streaming it into the edge's own body limit.
func (a *API) handleSubmissionLimits(w http.ResponseWriter, r *http.Request) {
l, ok := a.Submissions.(interface{ ContextLimit() int64 })
if !ok {
writeError(w, r, errSubmissionsUnavailable)
return
}
writeJSON(w, http.StatusOK, SubmissionLimits{MaxContextBytes: l.ContextLimit()})
}
func (a *API) handleMySubmissions(w http.ResponseWriter, r *http.Request) {
if a.Submissions == nil {
writeError(w, r, errSubmissionsUnavailable)
+24
View File
@@ -934,3 +934,27 @@ func TestUploadSubmissionContextRateLimited(t *testing.T) {
t.Fatalf("retry at the same instant after failure: code = %d, want 200 (%s)", w.Code, w.Body.String())
}
}
type limitedSubmissions struct {
*fakeSubmissions
limit int64
}
func (l limitedSubmissions) ContextLimit() int64 { return l.limit }
// The panel reads the per-upload cap before sending a file; a lane that cannot
// report one answers like any unwired submissions endpoint.
func TestSubmissionLimitsReportsTheContextCap(t *testing.T) {
api := appSubAPI(&fakeSubmissions{})
api.Submissions = limitedSubmissions{&fakeSubmissions{}, 99614720}
w := do(api.ExternalHandler(), "GET", "/api/v1/me/submissions/limits", "", nil)
if w.Code != 200 || strings.TrimSpace(w.Body.String()) != `{"max_context_bytes":99614720}` {
t.Fatalf("limits = %d %s", w.Code, w.Body.String())
}
api.Submissions = nil
w = do(api.ExternalHandler(), "GET", "/api/v1/me/submissions/limits", "", nil)
if w.Code != 503 {
t.Fatalf("unwired limits = %d %s", w.Code, w.Body.String())
}
}
+12
View File
@@ -166,6 +166,13 @@ func (a AuthConfig) EffectiveClientIPHeader() string {
return ""
}
// BehindCloudflare reports whether requests reach the API through the
// Cloudflare edge: an Access audience (set only by the edge setup) or
// CF-Connecting-IP as the client address header.
func (a AuthConfig) BehindCloudflare() bool {
return strings.EqualFold(a.EffectiveClientIPHeader(), "CF-Connecting-IP")
}
// K8sConfig is the [k8s] table.
type K8sConfig struct {
Namespace string `toml:"namespace"`
@@ -232,6 +239,11 @@ type RegistryConfig struct {
// own; this bounds the sum, which on k3s local-path is the only bound, since
// the uploads PVC's size is not enforced there. Empty keeps 4Gi.
UserUploadsMaxBytes string `toml:"user_uploads_max_bytes"`
// ContextMaxBytes caps one uploaded build context, as a quantity ("95Mi").
// Empty keeps 1Gi, except behind the Cloudflare edge (see BehindCloudflare),
// whose proxy refuses request bodies over 100 MB before they reach the API;
// there it keeps 95Mi, so the API's own 413 is what the uploader sees.
ContextMaxBytes string `toml:"context_max_bytes"`
// S3 configures the object-store backend for user_uploads_context when it is an
// s3:// base (the alternative to a local uploads path). It mirrors
// ArchiveS3Config: Endpoint + Region locate the store and the *Ref fields NAME
+3
View File
@@ -384,6 +384,9 @@ type Manager struct {
IDGen func() string
}
// ContextLimit is the effective cap on one uploaded context.
func (m *Manager) ContextLimit() int64 { return m.maxContextBytes() }
func (m *Manager) maxContextBytes() int64 {
if m.MaxContextBytes > 0 {
return m.MaxContextBytes