Complete artifact binary transfer streaming

This commit is contained in:
npc0-hue
2026-09-03 13:27:22 +08:00
parent fe09d21a56
commit ec2462a312
8 changed files with 178 additions and 35 deletions
@@ -175,7 +175,7 @@ func uploadCompletedArtifact(t *testing.T, router http.Handler, sessionToken str
Checksum: validator.BytesChecksum(part),
Payload: part,
}
assertStatus(t, performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", chunk), http.StatusOK)
assertStatus(t, performArtifactChunkUpload(t, router, chunk), http.StatusOK)
}
completeRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/complete", dto.ArtifactTransferCompleteRequest{RunEndpointID: "run-local", SessionToken: sessionToken, TransferID: opened.TransferID, ArtifactID: artifactID, Checksum: validator.BytesChecksum(payload), SizeBytes: int64(len(payload))})
assertStatus(t, completeRecorder, http.StatusOK)
@@ -1,8 +1,10 @@
package api
import (
"bytes"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"browser.local/platform/domain"
@@ -23,14 +25,14 @@ func TestArtifactTransferAPIWorkflow(t *testing.T) {
t.Fatalf("unexpected open response: %+v", opened)
}
chunkRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, 0, 8))
chunkRecorder := performArtifactChunkUpload(t, router, validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, 0, 8))
assertStatus(t, chunkRecorder, http.StatusOK)
chunk := decodeBody[dto.ArtifactChunkUploadResponse](t, chunkRecorder)
if !chunk.Accepted || chunk.NextMissingChunkIndex != 1 || len(chunk.ReceivedChunkIndexes) != 1 {
t.Fatalf("unexpected chunk response: %+v", chunk)
}
duplicateRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, 0, 8))
duplicateRecorder := performArtifactChunkUpload(t, router, validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, 0, 8))
assertStatus(t, duplicateRecorder, http.StatusOK)
duplicate := decodeBody[dto.ArtifactChunkUploadResponse](t, duplicateRecorder)
if !duplicate.Duplicate {
@@ -48,7 +50,7 @@ func TestArtifactTransferAPIWorkflow(t *testing.T) {
assertErrorResponse(t, missingComplete, http.StatusBadRequest, errorCodeValidation)
for index := 1; index < opened.TotalChunks; index++ {
partRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, index, 8))
partRecorder := performArtifactChunkUpload(t, router, validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, index, 8))
assertStatus(t, partRecorder, http.StatusOK)
}
completeRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/complete", dto.ArtifactTransferCompleteRequest{RunEndpointID: "run-local", SessionToken: hello.SessionToken, TransferID: opened.TransferID, ArtifactID: "artifact-1", Checksum: validator.BytesChecksum(payload), SizeBytes: int64(len(payload))})
@@ -68,7 +70,7 @@ func TestArtifactTransferAPIErrors(t *testing.T) {
badChunk := validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, 0, 8)
badChunk.Checksum = validator.BytesChecksum([]byte("different"))
badChunkRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", badChunk)
badChunkRecorder := performArtifactChunkUpload(t, router, badChunk)
assertErrorResponse(t, badChunkRecorder, http.StatusBadRequest, errorCodeValidation)
invalidSession := validArtifactTransferOpenRequest("stale-token", payload, 8)
@@ -108,6 +110,23 @@ func performArtifactTransferOpen(t *testing.T, router http.Handler, request dto.
return recorder
}
func performArtifactChunkUpload(t *testing.T, router http.Handler, request dto.ArtifactChunkUploadRequest) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/api/v1/run/artifacts/chunks", bytes.NewReader(request.Payload))
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-Run-Endpoint", request.RunEndpointID)
req.Header.Set("X-Run-Session-Token", request.SessionToken)
req.Header.Set("X-Artifact-Transfer-Id", request.TransferID)
req.Header.Set("X-Artifact-Id", request.ArtifactID)
req.Header.Set("X-Artifact-Chunk-Index", strconv.Itoa(request.ChunkIndex))
req.Header.Set("X-Artifact-Offset", strconv.FormatInt(request.Offset, 10))
req.Header.Set("X-Artifact-Size", strconv.Itoa(request.SizeBytes))
req.Header.Set("X-Artifact-Checksum", request.Checksum)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
return recorder
}
func validArtifactTransferOpenRequest(sessionToken string, payload []byte, chunkSize int) dto.ArtifactTransferOpenRequest {
return dto.ArtifactTransferOpenRequest{
RunEndpointID: "run-local",
@@ -33,7 +33,7 @@ func TestRunChannelAPIInterleavedRequestsMutateIndependentState(t *testing.T) {
openRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/open", artifactOpenForChannelIsolation(hello.SessionToken, payload, 8))
assertStatus(t, openRecorder, http.StatusOK)
open := decodeBody[dto.ArtifactTransferOpenResponse](t, openRecorder)
firstChunkRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", artifactChunkForChannelIsolation(hello.SessionToken, open.TransferID, payload, 0, 8))
firstChunkRecorder := performArtifactChunkUpload(t, router, artifactChunkForChannelIsolation(hello.SessionToken, open.TransferID, payload, 0, 8))
assertStatus(t, firstChunkRecorder, http.StatusOK)
firstChunk := decodeBody[dto.ArtifactChunkUploadResponse](t, firstChunkRecorder)
if !firstChunk.Accepted || firstChunk.NextMissingChunkIndex != 1 {
+23 -6
View File
@@ -14,10 +14,11 @@ import (
)
const (
runEndpointHeader = "X-Run-Endpoint"
runTimestampHeader = "X-Run-Timestamp"
runNonceHeader = "X-Run-Nonce"
runSignatureHeader = "X-Run-Signature"
runEndpointHeader = "X-Run-Endpoint"
runSessionTokenHeader = "X-Run-Session-Token"
runTimestampHeader = "X-Run-Timestamp"
runNonceHeader = "X-Run-Nonce"
runSignatureHeader = "X-Run-Signature"
)
type runRequestEnvelope struct {
@@ -37,8 +38,8 @@ func (h *coreHandlers) requireRunSignature(next http.HandlerFunc) http.HandlerFu
return
}
r.Body = io.NopCloser(bytes.NewReader(body))
var envelope runRequestEnvelope
if err := json.Unmarshal(body, &envelope); err != nil {
envelope, ok := signedRunRequestEnvelope(r, body)
if !ok {
next(w, r)
return
}
@@ -65,3 +66,19 @@ func (h *coreHandlers) requireRunSignature(next http.HandlerFunc) http.HandlerFu
next(w, r)
}
}
func signedRunRequestEnvelope(r *http.Request, body []byte) (runRequestEnvelope, bool) {
if isOctetStream(r.Header.Get("Content-Type")) {
return runRequestEnvelope{RunEndpointID: strings.TrimSpace(r.Header.Get(runEndpointHeader)), SessionToken: strings.TrimSpace(r.Header.Get(runSessionTokenHeader))}, true
}
var envelope runRequestEnvelope
if err := json.Unmarshal(body, &envelope); err != nil {
return runRequestEnvelope{}, false
}
return envelope, true
}
func isOctetStream(contentType string) bool {
mediaType := strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
return mediaType == "application/octet-stream"
}