feat(submit): local + S3 backends for modpack upload contexts, installer-selectable

This commit is contained in:
Lemon-miaow committed 2026-07-02 23:38:46 +08:00
1 parent 191640c3f5
commit 598f3d31f4
22 files changed
+2177 -34

No files matched your search

+38 -2
View File
@@ -3,6 +3,7 @@ package api
import (
"context"
"errors"
"io"
"net/http"
"felis.lolicon.best/internal/submit"
@@ -27,6 +28,10 @@ type SubmissionService interface {
// Create records a pending_review submission. It starts NO build (the whole
// point of the lane — nothing is built until an admin approves).
Create(ctx context.Context, req submit.CreateRequest) (*submit.Submission, error)
// UploadContext stores the modpack blob for the caller's own pending
// submission at the platform-derived context ref. submittedBy is the principal,
// never the body, so a user can only upload to a submission they own.
UploadContext(ctx context.Context, id, submittedBy string, r io.Reader) (*submit.Submission, error)
// ListBy returns one user's submissions, newest first (the "my uploads" view).
ListBy(ctx context.Context, submittedBy string) ([]submit.Submission, error)
// List returns every submission, newest first (the admin review queue).
@@ -80,6 +85,32 @@ func (a *API) handleCreateSubmission(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, sub)
}
// handleUploadSubmissionContext stores the caller's modpack blob as the build
// context for their own pending submission (app-tier). The request body IS the
// raw gzip tarball (context.tar.gz) — not JSON, not multipart — streamed straight
// to the transport; the submit layer sniffs the gzip magic and caps the size. The
// submitter is the authenticated principal, never the body, and a submission the
// caller does not own is reported as 404, so this endpoint cannot upload to — or
// probe the existence of — another user's submission.
//
// Uploading does not change the submission row (there is no "uploaded" column):
// the blob store is the presence source of truth, which admin approval consults.
func (a *API) handleUploadSubmissionContext(w http.ResponseWriter, r *http.Request) {
if a.Submissions == nil {
writeError(w, r, errSubmissionsUnavailable)
return
}
p := principalFromContext(r.Context())
id := r.PathValue("id")
sub, err := a.Submissions.UploadContext(r.Context(), id, p.UserID, r.Body)
if err != nil {
writeSubmitError(w, r, err)
return
}
a.audit(r, p.Email, "submission.upload", sub.ID)
writeJSON(w, http.StatusOK, sub)
}
// handleMySubmissions lists the caller's own submissions (app-tier). It scopes
// strictly to the principal's id; there is no parameter that could widen the
// query to another user's uploads.
@@ -162,8 +193,10 @@ var errSubmissionsUnavailable = newError(http.StatusServiceUnavailable, "submiss
"modpack submission subsystem is not configured")
// writeSubmitError maps submit-package errors onto HTTP status codes. Only the
// three business sentinels are client-facing: a validation failure is 400, a
// missing submission is 404, an already-reviewed submission is 409. Everything
// business sentinels are client-facing: a validation failure is 400, a missing
// submission is 404, an already-reviewed submission is 409, and an unconfigured
// upload transport is 503 (the store this deployment set has no implemented
// transport — an honest "not available here", not a client error). Everything
// else — including a build.ErrInvalid raised by the pre-CAS build.Validate (a
// platform registry/context MISCONFIGURATION, never client input, since every
// build input is platform-derived) and a post-CAS Submit hand-off failure — is a
@@ -178,6 +211,9 @@ func writeSubmitError(w http.ResponseWriter, r *http.Request, err error) {
case errors.Is(err, submit.ErrAlreadyReviewed):
writeError(w, r, newError(http.StatusConflict, "already_reviewed",
"submission has already been reviewed"))
case errors.Is(err, submit.ErrUploadsUnavailable):
writeError(w, r, newError(http.StatusServiceUnavailable, "uploads_unavailable",
"modpack upload transport is not configured"))
default:
writeError(w, r, err)
}