feat(submit): give the upload lane a lifecycle — withdraw + admin delete (#76)

Nothing ever removed a submission: users could not retract a pending row, no
route deleted blobs or rows, and the reaper never touches uploads — so every
upload accumulated on the 5 GiB PVC forever and the only cleanup was SQL or
kubectl against the store.

- Blobs.Delete on both transports (local: RemoveAll of the id-namespaced dir,
  id re-validated at the boundary; S3: idempotent object DELETE).
- Store: DeleteSubmission (admin, any status) and DeletePendingSubmission
  (owner+pending CAS — a reviewed row can never be withdrawn out from under
  its build).
- Manager.Delete / Manager.Withdraw delete the ROW first (under the CAS for
  withdraw) and the blob after, so a live row can never point at a reaped
  blob; a cleanup failure names the orphan explicitly instead of failing mute.
- API: DELETE /me/submissions/{id} (withdraw, app tier) and
  DELETE /api/v1/submissions/{id} (admin) both return the row as it was;
  audit events submission.withdraw / submission.delete; openapi documents both
  paths; admin route pinned in the admin-only table.
- Panel: two-step withdraw on a pending row (frees the pending slot and the
  storage budget); two-step delete on every admin row; zh/en copy; wire tests.

Unit: submit (withdraw happy path / wrong owner / reviewed row / no transport /
blob-cleanup failure), local+S3 delete idempotence, api handlers (200/404/409/
503 + route tier); pgint: withdraw CAS + admin delete exactly-once.
go vet/go test/gofmt clean; panel vitest 120 + typecheck green.
This commit is contained in:
Lemon-miaow committed 2026-09-24 10:24:23 +08:00
1 parent ad4d256d8f
commit 854320ac3f
20 files changed
+828 -45

No files matched your search

+67
View File
@@ -4355,6 +4355,41 @@ paths:
'503':
$ref: '#/components/responses/ServiceUnavailable'
/api/v1/me/submissions/{id}:
delete:
tags: [submissions]
operationId: withdrawSubmission
summary: Withdraw your own pending submission (user side; user-directed lane over §16).
description: >-
Retracts the caller's own submission while it is still pending review:
the row and its uploaded build context are deleted, freeing the pending
slot and the per-user storage budget for a fresh submission. A reviewed
submission is frozen (409 — its build may already be consuming the
context), and a submission the caller does not own reads back as 404, so
this endpoint cannot probe or clear another user's uploads.
x-felis-face: [external]
x-felis-tier: app
security: [{ accessJWT: [] }]
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'200':
description: The withdrawn submission, as it was before the deletion.
content:
application/json:
schema: { $ref: '#/components/schemas/Submission' }
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'409':
description: Submission has already been reviewed and cannot be withdrawn.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'503':
$ref: '#/components/responses/ServiceUnavailable'
# ------------------------------------------------------ external: admin ----
/api/v1/servers/{name}:
patch:
@@ -4750,3 +4785,35 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'503':
$ref: '#/components/responses/ServiceUnavailable'
/api/v1/submissions/{id}:
delete:
tags: [submissions]
operationId: deleteSubmission
summary: Retire a submission outright — row and uploaded context (admin; user-directed lane over §16).
description: >-
Removes the submission and its uploaded build context, any status — the
lane's only lifecycle valve, and the path that reclaims a rejected or
consumed upload from the uploads PVC. The reviewer identity is recorded
in the audit event, not on the (now deleted) row. Deleting an approved
submission whose build is still running fails that build's context
fetch; the admin has explicitly chosen to retire the artifact.
x-felis-face: [external]
x-felis-tier: admin
security: [{ accessJWT: [] }]
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'200':
description: The deleted submission, as it was before the deletion.
content:
application/json:
schema: { $ref: '#/components/schemas/Submission' }
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'503':
$ref: '#/components/responses/ServiceUnavailable'