Ctrl K
ComunidadGuías…

internal/assets/assets.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package assets

import (
	"context"
	"errors"
	"io"
	"log"
	"net/http"
	"xprem/internal/bucket"
	"xprem/internal/cdn"
	"xprem/internal/types"
	"xprem/internal/update"
)

type AssetsRequest struct {
	AppId          string
	Branch         string
	AssetName      string
	RuntimeVersion string
	Platform       types.Platform
	RequestID      string
	Update         *types.Update
}

type AssetsResponse struct {
	StatusCode  int
	Headers     map[string]string
	Body        []byte
	ContentType string
	URL         string
}

// validatedAsset is the outcome of the shared request validation: the update
// the asset is served from and its manifest entry.
type validatedAsset struct {
	update        *types.Update
	isLaunchAsset bool
	assetMetadata types.Asset
}

// validateAssetRequest runs the checks common to both delivery paths and must
// stay their single entry point: the URL path hands req.AssetName to the CDN
// layer, which builds (and for CloudFront signs) whatever key it is given,
// and the file path would otherwise proxy internal objects like
// update-metadata.json living next to real assets. A non-nil response means
// the request must be refused as-is.
func validateAssetRequest(ctx context.Context, req AssetsRequest) (validatedAsset, *AssetsResponse) {
	requestID := req.RequestID

	if req.AssetName == "" {
		log.Printf("[RequestID: %s] No asset name provided", requestID)
		return validatedAsset{}, &AssetsResponse{StatusCode: http.StatusBadRequest, Body: []byte("No asset name provided")}
	}

	if req.RuntimeVersion == "" {
		log.Printf("[RequestID: %s] No runtime version provided", requestID)
		return validatedAsset{}, &AssetsResponse{StatusCode: http.StatusBadRequest, Body: []byte("No runtime version provided")}
	}

	if req.Update == nil {
		// The service resolves the update and 404s before reaching here, so
		// a nil Update means "no matching update". Guard it rather than
		// dereferencing into a SIGSEGV below.
		log.Printf("[RequestID: %s] No update found", requestID)
		return validatedAsset{}, &AssetsResponse{StatusCode: http.StatusNotFound, Body: []byte("No update found")}
	}

	metadata, err := update.GetMetadata(ctx, *req.Update)
	if errors.Is(err, update.ErrUpdateMetadataMissing) {
		// No metadata means no manifest ever advertised this asset: answer
		// like any unknown asset instead of surfacing a server error.
		log.Printf("[RequestID: %s] %v", requestID, err)
		return validatedAsset{}, &AssetsResponse{StatusCode: http.StatusNotFound, Body: []byte("Asset not found")}
	}
	if err != nil {
		log.Printf("[RequestID: %s] Error getting metadata: %v", requestID, err)
		return validatedAsset{}, &AssetsResponse{StatusCode: http.StatusInternalServerError, Body: []byte("Error getting metadata")}
	}

	platformMetadata, err := metadata.MetadataJSON.FileMetadata.PlatformMetadata(req.Platform)
	if err != nil || platformMetadata.Bundle == "" {
		log.Printf("[RequestID: %s] Error getting platform metadata: %v", requestID, err)
		return validatedAsset{}, &AssetsResponse{StatusCode: http.StatusBadRequest, Body: []byte("Platform not supported")}
	}
	isLaunchAsset := platformMetadata.Bundle == req.AssetName

	var assetMetadata types.Asset
	for _, asset := range platformMetadata.Assets {
		if asset.Path == req.AssetName {
			assetMetadata = asset
		}
	}

	if !isLaunchAsset && assetMetadata == (types.Asset{}) {
		log.Printf("[RequestID: %s] Asset not found in metadata: %s", requestID, req.AssetName)
		return validatedAsset{}, &AssetsResponse{StatusCode: http.StatusNotFound, Body: []byte("Asset not found")}
	}

	return validatedAsset{update: req.Update, isLaunchAsset: isLaunchAsset, assetMetadata: assetMetadata}, nil
}

// ExpoProtocolHeaders are the response headers every served asset carries,
// blob or legacy.
func ExpoProtocolHeaders() map[string]string {
	return map[string]string{
		"expo-protocol-version": "1",
		"expo-sfv-version":      "0",
		"Cache-Control":         "public, max-age=31536000",
	}
}

func HandleAssetsWithFile(ctx context.Context, req AssetsRequest) (AssetsResponse, error) {
	validated, errResp := validateAssetRequest(ctx, req)
	if errResp != nil {
		return *errResp, nil
	}

	asset, err := bucket.GetBucket().UpdateStore.GetFile(ctx, *validated.update, req.AssetName)
	if err != nil {
		log.Printf("[RequestID: %s] Error getting asset: %v", req.RequestID, err)
		return AssetsResponse{StatusCode: http.StatusInternalServerError, Body: []byte("Error getting asset")}, nil
	}
	if asset == nil {
		log.Printf("[RequestID: %s] Resolved file is nil", req.RequestID)
		return AssetsResponse{StatusCode: http.StatusInternalServerError, Body: []byte("Resolved file is nil")}, nil
	}

	defer asset.Reader.Close()
	buffer, err := io.ReadAll(asset.Reader)
	if err != nil {
		log.Printf("[RequestID: %s] Error converting asset to buffer: %v", req.RequestID, err)
		return AssetsResponse{StatusCode: http.StatusInternalServerError, Body: []byte("Error converting asset to buffer")}, err
	}

	contentType := update.AssetContentType(string(validated.assetMetadata.Ext), validated.isLaunchAsset)

	headers := ExpoProtocolHeaders()
	headers["Content-Type"] = contentType

	return AssetsResponse{
		StatusCode:  http.StatusOK,
		Headers:     headers,
		ContentType: contentType,
		Body:        buffer,
	}, nil
}

func HandleAssetsWithURL(ctx context.Context, req AssetsRequest, resolvedCDN cdn.CDN) (AssetsResponse, error) {
	validated, errResp := validateAssetRequest(ctx, req)
	if errResp != nil {
		return *errResp, nil
	}

	redirectURL, err := resolvedCDN.ComputeRedirectionURLForAsset(req.AppId, req.Branch, req.RuntimeVersion, validated.update.UpdateId, req.AssetName)
	if err != nil {
		log.Printf("[RequestID: %s] Error computing redirection URL: %v", req.RequestID, err)
		return AssetsResponse{StatusCode: http.StatusInternalServerError, Body: []byte("Error computing redirection URL")}, err
	}
	return AssetsResponse{
		StatusCode: http.StatusOK,
		Headers:    ExpoProtocolHeaders(),
		URL:        redirectURL,
	}, nil
}