Complete artifact binary transfer streaming
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -24,6 +25,7 @@ type ArtifactBodyStore interface {
|
||||
PutPayload(string, []byte) error
|
||||
GetPayload(string) ([]byte, error)
|
||||
ReadPayloadRange(string, int64, int) ([]byte, error)
|
||||
OpenPayloadRange(string, int64, int64) (io.ReadCloser, error)
|
||||
CommitTransferPayload(domain.ArtifactTransferSession) error
|
||||
}
|
||||
|
||||
@@ -89,6 +91,17 @@ func (store *MemoryArtifactBodyStore) ReadPayloadRange(artifactID string, offset
|
||||
return domain.CopyBytes(payload[int(offset) : int(offset)+length]), nil
|
||||
}
|
||||
|
||||
func (store *MemoryArtifactBodyStore) OpenPayloadRange(artifactID string, offset int64, length int64) (io.ReadCloser, error) {
|
||||
if length < 0 || length > int64(int(^uint(0)>>1)) {
|
||||
return nil, fmt.Errorf("artifact range is invalid")
|
||||
}
|
||||
payload, err := store.ReadPayloadRange(artifactID, offset, int(length))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(payload)), nil
|
||||
}
|
||||
|
||||
func (store *MemoryArtifactBodyStore) CommitTransferPayload(session domain.ArtifactTransferSession) error {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
@@ -243,6 +256,40 @@ func (store *FileArtifactBodyStore) ReadPayloadRange(artifactID string, offset i
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (store *FileArtifactBodyStore) OpenPayloadRange(artifactID string, offset int64, length int64) (io.ReadCloser, error) {
|
||||
if offset < 0 || length < 0 {
|
||||
return nil, fmt.Errorf("artifact range is invalid")
|
||||
}
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
file, err := os.Open(store.payloadPath(artifactID))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, repo.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open artifact payload: %w", err)
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, fmt.Errorf("stat artifact payload: %w", err)
|
||||
}
|
||||
if offset > info.Size() || length > info.Size()-offset {
|
||||
_ = file.Close()
|
||||
return nil, fmt.Errorf("artifact range is invalid")
|
||||
}
|
||||
return sectionReadCloser{SectionReader: io.NewSectionReader(file, offset, length), file: file}, nil
|
||||
}
|
||||
|
||||
type sectionReadCloser struct {
|
||||
*io.SectionReader
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func (reader sectionReadCloser) Close() error {
|
||||
return reader.file.Close()
|
||||
}
|
||||
|
||||
func (store *FileArtifactBodyStore) CommitTransferPayload(session domain.ArtifactTransferSession) error {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -16,6 +17,11 @@ import (
|
||||
const artifactDownloadStorageBehavior = "platform-durable-artifact-store"
|
||||
const artifactPayloadCacheLimit = 4 * 1024 * 1024
|
||||
|
||||
type ArtifactContentStream struct {
|
||||
Content domain.ArtifactContent
|
||||
Body io.ReadCloser
|
||||
}
|
||||
|
||||
func (svc *CoreService) GetArtifactForSession(sessionID string, artifactID string) (domain.Artifact, error) {
|
||||
artifact, err := svc.store.Artifacts().Get(strings.TrimSpace(artifactID))
|
||||
if err != nil {
|
||||
@@ -74,22 +80,11 @@ func (svc *CoreService) ReadArtifactContentForSession(sessionID string, request
|
||||
if artifact.State != domain.ArtifactStateAvailable {
|
||||
return domain.ArtifactContent{}, validationError("artifact must be available before download")
|
||||
}
|
||||
limit := request.Limit
|
||||
if limit == 0 {
|
||||
if request.Offset == 0 {
|
||||
limit = int(artifact.SizeBytes)
|
||||
} else {
|
||||
limit = validator.MaxArtifactDownloadBytes
|
||||
}
|
||||
offset, length, partial, err := artifactContentBounds(artifact, request)
|
||||
if err != nil {
|
||||
return domain.ArtifactContent{}, err
|
||||
}
|
||||
if request.Offset > artifact.SizeBytes {
|
||||
return domain.ArtifactContent{}, validationError("artifact range exceeds metadata")
|
||||
}
|
||||
remaining := artifact.SizeBytes - request.Offset
|
||||
if int64(limit) > remaining {
|
||||
limit = int(remaining)
|
||||
}
|
||||
payload, err := svc.artifactStore.ReadPayloadRange(artifact.ID, request.Offset, limit)
|
||||
payload, err := svc.artifactStore.ReadPayloadRange(artifact.ID, offset, int(length))
|
||||
if err != nil {
|
||||
return domain.ArtifactContent{}, err
|
||||
}
|
||||
@@ -99,12 +94,12 @@ func (svc *CoreService) ReadArtifactContentForSession(sessionID string, request
|
||||
ArtifactID: artifact.ID,
|
||||
Filename: filename,
|
||||
ContentType: contentType,
|
||||
Offset: request.Offset,
|
||||
Offset: offset,
|
||||
SizeBytes: int64(len(part)),
|
||||
TotalSizeBytes: artifact.SizeBytes,
|
||||
Checksum: artifact.Checksum,
|
||||
ContentChecksum: validator.BytesChecksum(part),
|
||||
Partial: request.Offset != 0 || int64(len(part)) != artifact.SizeBytes,
|
||||
Partial: partial,
|
||||
RangeSupported: true,
|
||||
Payload: part,
|
||||
StorageBehavior: artifactDownloadStorageBehavior,
|
||||
@@ -116,6 +111,66 @@ func (svc *CoreService) ReadArtifactContentForSession(sessionID string, request
|
||||
return domain.CopyArtifactContent(content), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) OpenArtifactContentStreamForSession(sessionID string, request domain.ArtifactContentRequest) (ArtifactContentStream, error) {
|
||||
request = domain.CopyArtifactContentRequest(request)
|
||||
if err := validator.ValidateArtifactContentRequest(request); err != nil {
|
||||
return ArtifactContentStream{}, err
|
||||
}
|
||||
artifact, err := svc.GetArtifactForSession(sessionID, request.ArtifactID)
|
||||
if err != nil {
|
||||
return ArtifactContentStream{}, err
|
||||
}
|
||||
if artifact.State != domain.ArtifactStateAvailable {
|
||||
return ArtifactContentStream{}, validationError("artifact must be available before download")
|
||||
}
|
||||
offset, length, partial, err := artifactContentBounds(artifact, request)
|
||||
if err != nil {
|
||||
return ArtifactContentStream{}, err
|
||||
}
|
||||
contentChecksum := artifact.Checksum
|
||||
if partial {
|
||||
payload, err := svc.artifactStore.ReadPayloadRange(artifact.ID, offset, int(length))
|
||||
if err != nil {
|
||||
return ArtifactContentStream{}, err
|
||||
}
|
||||
contentChecksum = validator.BytesChecksum(payload)
|
||||
}
|
||||
reader, err := svc.artifactStore.OpenPayloadRange(artifact.ID, offset, length)
|
||||
if err != nil {
|
||||
return ArtifactContentStream{}, err
|
||||
}
|
||||
filename, contentType := svc.artifactDownloadPresentation(artifact)
|
||||
content := domain.ArtifactContent{ArtifactID: artifact.ID, Filename: filename, ContentType: contentType, Offset: offset, SizeBytes: length, TotalSizeBytes: artifact.SizeBytes, Checksum: artifact.Checksum, ContentChecksum: contentChecksum, Partial: partial, RangeSupported: true, StorageBehavior: artifactDownloadStorageBehavior, ServedAt: svc.now()}
|
||||
if err := validator.ValidateArtifactContent(content); err != nil {
|
||||
_ = reader.Close()
|
||||
return ArtifactContentStream{}, err
|
||||
}
|
||||
return ArtifactContentStream{Content: domain.CopyArtifactContent(content), Body: reader}, nil
|
||||
}
|
||||
|
||||
func artifactContentBounds(artifact domain.Artifact, request domain.ArtifactContentRequest) (int64, int64, bool, error) {
|
||||
if request.Offset > artifact.SizeBytes {
|
||||
return 0, 0, false, validationError("artifact range exceeds metadata")
|
||||
}
|
||||
length := int64(request.Limit)
|
||||
if request.Limit == 0 {
|
||||
if request.Offset == 0 {
|
||||
length = artifact.SizeBytes
|
||||
} else {
|
||||
length = int64(validator.MaxArtifactDownloadBytes)
|
||||
}
|
||||
}
|
||||
remaining := artifact.SizeBytes - request.Offset
|
||||
if length > remaining {
|
||||
length = remaining
|
||||
}
|
||||
if length <= 0 {
|
||||
return 0, 0, false, validationError("artifact range is empty")
|
||||
}
|
||||
partial := request.Offset != 0 || length != artifact.SizeBytes
|
||||
return request.Offset, length, partial, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) authorizeArtifactAccess(sessionID string, artifact domain.Artifact) error {
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user