first commit
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
const artifactDownloadStorageBehavior = "platform-memory-transfer-session"
|
||||
|
||||
func (svc *CoreService) GetArtifactForSession(sessionID string, artifactID string) (domain.Artifact, error) {
|
||||
artifact, err := svc.store.Artifacts().Get(strings.TrimSpace(artifactID))
|
||||
if err != nil {
|
||||
return domain.Artifact{}, err
|
||||
}
|
||||
if err := svc.authorizeArtifactAccess(sessionID, artifact); err != nil {
|
||||
return domain.Artifact{}, err
|
||||
}
|
||||
return domain.CopyArtifact(artifact), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) OpenArtifactDownloadForSession(sessionID string, request domain.ArtifactDownloadReferenceRequest) (domain.ArtifactDownloadReference, error) {
|
||||
request = domain.CopyArtifactDownloadReferenceRequest(request)
|
||||
if err := validator.ValidateArtifactDownloadReferenceRequest(request); err != nil {
|
||||
return domain.ArtifactDownloadReference{}, err
|
||||
}
|
||||
artifact, err := svc.GetArtifactForSession(sessionID, request.ArtifactID)
|
||||
if err != nil {
|
||||
return domain.ArtifactDownloadReference{}, err
|
||||
}
|
||||
if artifact.State != domain.ArtifactStateAvailable {
|
||||
return domain.ArtifactDownloadReference{}, validationError("artifact must be available before download")
|
||||
}
|
||||
|
||||
reference := domain.ArtifactDownloadReference{
|
||||
ArtifactID: artifact.ID,
|
||||
OwnerKind: artifact.OwnerKind,
|
||||
OwnerID: artifact.OwnerID,
|
||||
Filename: artifactDownloadFilename(artifact.ID),
|
||||
ContentType: "application/octet-stream",
|
||||
SizeBytes: artifact.SizeBytes,
|
||||
Checksum: artifact.Checksum,
|
||||
State: artifact.State,
|
||||
DownloadURL: "/api/v1/artifacts/" + url.PathEscape(artifact.ID) + "/content",
|
||||
ExpiresAt: svc.now().Add(15 * time.Minute),
|
||||
RangeSupported: true,
|
||||
ChunkSizeBytes: validator.MaxArtifactDownloadBytes,
|
||||
StorageBehavior: artifactDownloadStorageBehavior,
|
||||
}
|
||||
if err := validator.ValidateArtifactDownloadReference(reference); err != nil {
|
||||
return domain.ArtifactDownloadReference{}, err
|
||||
}
|
||||
return domain.CopyArtifactDownloadReference(reference), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ReadArtifactContentForSession(sessionID string, request domain.ArtifactContentRequest) (domain.ArtifactContent, error) {
|
||||
request = domain.CopyArtifactContentRequest(request)
|
||||
if err := validator.ValidateArtifactContentRequest(request); err != nil {
|
||||
return domain.ArtifactContent{}, err
|
||||
}
|
||||
artifact, err := svc.GetArtifactForSession(sessionID, request.ArtifactID)
|
||||
if err != nil {
|
||||
return domain.ArtifactContent{}, err
|
||||
}
|
||||
if artifact.State != domain.ArtifactStateAvailable {
|
||||
return domain.ArtifactContent{}, validationError("artifact must be available before download")
|
||||
}
|
||||
payload, err := svc.artifactPayload(artifact.ID)
|
||||
if err != nil {
|
||||
return domain.ArtifactContent{}, err
|
||||
}
|
||||
if int64(len(payload)) != artifact.SizeBytes {
|
||||
return domain.ArtifactContent{}, validationError("artifact content size does not match metadata")
|
||||
}
|
||||
if checksum := validator.BytesChecksum(payload); checksum != artifact.Checksum {
|
||||
return domain.ArtifactContent{}, validationError("artifact content checksum does not match metadata")
|
||||
}
|
||||
if request.Offset >= artifact.SizeBytes {
|
||||
return domain.ArtifactContent{}, validationError("offset must be inside artifact content")
|
||||
}
|
||||
limit := request.Limit
|
||||
if limit == 0 {
|
||||
limit = validator.MaxArtifactDownloadBytes
|
||||
}
|
||||
remaining := artifact.SizeBytes - request.Offset
|
||||
if int64(limit) > remaining {
|
||||
limit = int(remaining)
|
||||
}
|
||||
end := int(request.Offset) + limit
|
||||
part := domain.CopyBytes(payload[int(request.Offset):end])
|
||||
content := domain.ArtifactContent{
|
||||
ArtifactID: artifact.ID,
|
||||
Filename: artifactDownloadFilename(artifact.ID),
|
||||
ContentType: "application/octet-stream",
|
||||
Offset: request.Offset,
|
||||
SizeBytes: int64(len(part)),
|
||||
TotalSizeBytes: artifact.SizeBytes,
|
||||
Checksum: artifact.Checksum,
|
||||
ContentChecksum: validator.BytesChecksum(part),
|
||||
Partial: request.Offset != 0 || int64(len(part)) != artifact.SizeBytes,
|
||||
RangeSupported: true,
|
||||
Payload: part,
|
||||
StorageBehavior: artifactDownloadStorageBehavior,
|
||||
ServedAt: svc.now(),
|
||||
}
|
||||
if err := validator.ValidateArtifactContent(content); err != nil {
|
||||
return domain.ArtifactContent{}, err
|
||||
}
|
||||
return domain.CopyArtifactContent(content), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) authorizeArtifactAccess(sessionID string, artifact domain.Artifact) error {
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch artifact.OwnerKind {
|
||||
case domain.ArtifactOwnerKindJob:
|
||||
job, err := svc.store.Jobs().Get(artifact.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(job.ServerInstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canAccessServer(user, instance) {
|
||||
return ErrForbidden
|
||||
}
|
||||
case domain.ArtifactOwnerKindServerInstance:
|
||||
instance, err := svc.store.ServerInstances().Get(artifact.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canAccessServer(user, instance) {
|
||||
return ErrForbidden
|
||||
}
|
||||
case domain.ArtifactOwnerKindPlatform, domain.ArtifactOwnerKindPlugin:
|
||||
if !isPlatformAdmin(user) {
|
||||
return ErrForbidden
|
||||
}
|
||||
default:
|
||||
return validationError("artifact ownerKind is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) artifactPayload(artifactID string) ([]byte, error) {
|
||||
svc.artifactMu.Lock()
|
||||
defer svc.artifactMu.Unlock()
|
||||
|
||||
sessions := make([]domain.ArtifactTransferSession, 0, len(svc.artifactTransfers))
|
||||
for _, session := range svc.artifactTransfers {
|
||||
if session.ArtifactID == artifactID && session.Completed {
|
||||
sessions = append(sessions, domain.CopyArtifactTransferSession(session))
|
||||
}
|
||||
}
|
||||
if len(sessions) == 0 {
|
||||
return nil, validationError("artifact content is not available from platform storage")
|
||||
}
|
||||
sort.Slice(sessions, func(i int, j int) bool { return sessions[i].UpdatedAt.After(sessions[j].UpdatedAt) })
|
||||
session := sessions[0]
|
||||
payload := make([]byte, 0, int(session.SizeBytes))
|
||||
for index := 0; index < session.TotalChunks; index++ {
|
||||
record, exists := session.ReceivedChunks[index]
|
||||
if !exists {
|
||||
return nil, validationError("artifact content has missing chunks")
|
||||
}
|
||||
payload = append(payload, record.Payload...)
|
||||
}
|
||||
if int64(len(payload)) != session.SizeBytes {
|
||||
return nil, validationError("artifact content size does not match transfer")
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func artifactDownloadFilename(artifactID string) string {
|
||||
name := strings.TrimSpace(artifactID)
|
||||
if name == "" || strings.Contains(name, "/") || strings.Contains(name, `\`) || strings.Contains(name, "://") {
|
||||
return "artifact.bin"
|
||||
}
|
||||
return fmt.Sprintf("%s.bin", name)
|
||||
}
|
||||
Reference in New Issue
Block a user