Speed up artifact downloads
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -55,6 +56,27 @@ func TestArtifactDownloadAPIWorkflowIsPlatformMediated(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactContentWithoutRangeReturnsWholePayload(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest())
|
||||
hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, artifactDownloadHelloRequest()))
|
||||
postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-download-full", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Download Full"}, adminSession)
|
||||
postJSON[dto.JobResponse](t, router, "/api/v1/jobs", dto.JobCreateRequest{ID: "job-download-full", ServerInstanceID: "server-download-full", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-download-full"})
|
||||
|
||||
payload := bytes.Repeat([]byte("full-download-payload-"), (validator.MaxArtifactDownloadBytes/len("full-download-payload-"))+2)
|
||||
uploadCompletedArtifact(t, router, hello.SessionToken, "artifact-download-full", "job-download-full", payload, validator.MaxArtifactChunkBytes)
|
||||
|
||||
contentRecorder := requestWithAuth(t, router, http.MethodGet, "/api/v1/artifacts/artifact-download-full/content", "", adminSession)
|
||||
assertStatus(t, contentRecorder, http.StatusOK)
|
||||
if got := contentRecorder.Body.Bytes(); !bytes.Equal(got, payload) {
|
||||
t.Fatalf("expected whole payload length %d, got %d", len(payload), len(got))
|
||||
}
|
||||
if contentRecorder.Header().Get("Content-Range") != "" || contentRecorder.Header().Get("Content-Length") != strconv.Itoa(len(payload)) {
|
||||
t.Fatalf("expected full content headers, got %+v", contentRecorder.Header())
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactDownloadAPIDeniesUnavailableAndUnauthorizedArtifacts(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
|
||||
@@ -220,7 +220,7 @@ Artifact/file transfer is lower priority than control, job lifecycle metadata, a
|
||||
|
||||
- `GET /api/v1/artifacts/{id}`: returns authorized artifact metadata for the current bearer session.
|
||||
- `POST /api/v1/artifacts/{id}/download`: returns `ArtifactDownloadReferenceResponse` with filename, content type, size, checksum, expiry, supported chunk size, and a platform-owned `downloadUrl`.
|
||||
- `GET /api/v1/artifacts/{id}/content`: returns a bounded byte range using `offset`/`limit` query parameters or a `Range: bytes=start-end` header. Responses include `Content-Length`, `Accept-Ranges`, optional `Content-Range`, `X-Artifact-Checksum`, `X-Artifact-Content-Checksum`, and `X-Artifact-Storage` headers.
|
||||
- `GET /api/v1/artifacts/{id}/content`: streams the full artifact when no range is supplied, or returns a bounded byte range using `offset`/`limit` query parameters or a `Range: bytes=start-end` header. Responses include `Content-Length`, `Accept-Ranges`, optional `Content-Range`, `X-Artifact-Checksum`, `X-Artifact-Content-Checksum`, and `X-Artifact-Storage` headers.
|
||||
|
||||
Browser artifact downloads require an available artifact plus user access to the owning job/server context. Platform/plugin-owned artifacts are limited to platform administrators until a future storage policy adds narrower ownership. Current content reads use the private durable artifact body store; external object storage adapters are deferred behind the same service contract. Browser and plugin pages receive only platform routes and integrity metadata, never raw storage backend URLs, host paths, direct run sockets, run tokens, bearer tokens, or storage credentials.
|
||||
|
||||
|
||||
@@ -76,7 +76,11 @@ func (svc *CoreService) ReadArtifactContentForSession(sessionID string, request
|
||||
}
|
||||
limit := request.Limit
|
||||
if limit == 0 {
|
||||
limit = validator.MaxArtifactDownloadBytes
|
||||
if request.Offset == 0 {
|
||||
limit = int(artifact.SizeBytes)
|
||||
} else {
|
||||
limit = validator.MaxArtifactDownloadBytes
|
||||
}
|
||||
}
|
||||
if request.Offset > artifact.SizeBytes {
|
||||
return domain.ArtifactContent{}, validationError("artifact range exceeds metadata")
|
||||
|
||||
@@ -94,8 +94,12 @@ func ValidateArtifactContent(content domain.ArtifactContent) error {
|
||||
if content.TotalSizeBytes <= 0 {
|
||||
violations = append(violations, "totalSizeBytes must be positive")
|
||||
}
|
||||
if content.SizeBytes > MaxArtifactDownloadBytes {
|
||||
violations = append(violations, fmt.Sprintf("sizeBytes must not exceed %d", MaxArtifactDownloadBytes))
|
||||
maxContentBytes := int64(MaxArtifactDownloadBytes)
|
||||
if !content.Partial {
|
||||
maxContentBytes = MaxArtifactBytes
|
||||
}
|
||||
if content.SizeBytes > maxContentBytes {
|
||||
violations = append(violations, fmt.Sprintf("sizeBytes must not exceed %d", maxContentBytes))
|
||||
}
|
||||
if int64(len(content.Payload)) != content.SizeBytes {
|
||||
violations = append(violations, "payload size must match sizeBytes")
|
||||
|
||||
Reference in New Issue
Block a user