package service import ( "errors" "fmt" "net/url" "sort" "strings" "time" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) const artifactDownloadStorageBehavior = "platform-durable-artifact-store" 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") } 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]) filename, contentType := svc.artifactDownloadPresentation(artifact) content := domain.ArtifactContent{ ArtifactID: artifact.ID, Filename: filename, ContentType: contentType, 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() if payload, exists := svc.artifactPayloads[artifactID]; exists { return domain.CopyBytes(payload), nil } if payload, err := svc.artifactStore.GetPayload(artifactID); err == nil { svc.artifactPayloads[artifactID] = domain.CopyBytes(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.artifactPayloads[artifactID] = domain.CopyBytes(payload) 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) } 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" } }