package service import ( "errors" "fmt" "io" "net/url" "sort" "strings" "time" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) 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 { 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") } filename, contentType := svc.artifactDownloadPresentation(artifact) reference := domain.ArtifactDownloadReference{ ArtifactID: artifact.ID, OwnerKind: artifact.OwnerKind, OwnerID: artifact.OwnerID, Filename: filename, ContentType: contentType, 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") } offset, length, partial, err := artifactContentBounds(artifact, request) if err != nil { return domain.ArtifactContent{}, err } payload, err := svc.artifactStore.ReadPayloadRange(artifact.ID, offset, int(length)) if err != nil { return domain.ArtifactContent{}, err } part := domain.CopyBytes(payload) filename, contentType := svc.artifactDownloadPresentation(artifact) content := domain.ArtifactContent{ ArtifactID: artifact.ID, Filename: filename, ContentType: contentType, Offset: offset, SizeBytes: int64(len(part)), TotalSizeBytes: artifact.SizeBytes, Checksum: artifact.Checksum, ContentChecksum: validator.BytesChecksum(part), Partial: partial, 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) 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 { 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() if payload, exists := svc.artifactPayloads[artifactID]; exists { return domain.CopyBytes(payload), nil } if payload, err := svc.artifactStore.GetPayload(artifactID); err == nil { svc.cacheArtifactPayload(artifactID, payload) return payload, nil } else if !errors.Is(err, repo.ErrNotFound) { return nil, err } 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") } if err := svc.artifactStore.PutPayload(artifactID, payload); err != nil { return nil, err } svc.cacheArtifactPayload(artifactID, payload) return payload, nil } func (svc *CoreService) cacheArtifactPayload(artifactID string, payload []byte) { if len(payload) > artifactPayloadCacheLimit { delete(svc.artifactPayloads, artifactID) return } svc.artifactPayloads[artifactID] = domain.CopyBytes(payload) } 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) } func (svc *CoreService) artifactDownloadPresentation(artifact domain.Artifact) (string, string) { if filename, contentType, ok := svc.distributionArtifactDownloadPresentation(artifact.ID); ok { return filename, contentType } return artifactDownloadFilename(artifact.ID), "application/octet-stream" } func (svc *CoreService) distributionArtifactDownloadPresentation(artifactID string) (string, string, bool) { runs, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{}) if err == nil { for _, distribution := range runs { if distribution.ArtifactID == artifactID { return distributionPackageFilename("run", distribution.TargetOS, distribution.TargetArch, distribution.PackageFormat), packageContentType(distribution.PackageFormat), true } } } clients, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{}) if err == nil { for _, distribution := range clients { if distribution.ArtifactID == artifactID { format := packageFormatForTarget(distribution.TargetOS) return distributionPackageFilename(distribution.ProfileKey, distribution.TargetOS, distribution.TargetArch, format), packageContentType(format), true } } } return "", "", false } func distributionPackageFilename(base string, targetOS string, targetArch string, packageFormat string) string { name := sanitizeIDPart(base) os := sanitizeIDPart(targetOS) arch := sanitizeIDPart(targetArch) format := strings.TrimPrefix(strings.TrimSpace(packageFormat), ".") if name == "" { name = "artifact" } if os != "" { name += "-" + os } if arch != "" { name += "-" + arch } if format == "raw-executable" { if os == "windows" { return name + ".exe" } return name } if format == "" { format = "bin" } return name + "." + format } func packageContentType(packageFormat string) string { switch strings.TrimSpace(packageFormat) { case "zip": return "application/zip" case "tar.gz", "tgz": return "application/gzip" case "raw-executable": return "application/octet-stream" default: return "application/octet-stream" } }