internal/assets/assets.go
Rama main · réplica de lectura del repositorio original.
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
}