Skip to content

Commit

Permalink
Implement asset uploading
Browse files Browse the repository at this point in the history
  • Loading branch information
alexprogrammr committed Jun 8, 2024
1 parent 257522b commit 3903149
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 12 deletions.
8 changes: 0 additions & 8 deletions achievements.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,3 @@ type AchievementLocalizationAttr struct {
BeforeEarnedDescription string `json:"beforeEarnedDescription"`
AfterEarnedDescription string `json:"afterEarnedDescription"`
}

type AchievementImageID string

// https://developer.apple.com/documentation/appstoreconnectapi/gamecenterachievementimage
type AchievementImage struct {
ID AchievementImageID `json:"id"`
Attr Asset `json:"attributes"`
}
27 changes: 27 additions & 0 deletions achievements_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,30 @@ func (c *Client) CreateAchievementLocalization(ctx context.Context, achievementI

return resp, nil
}

type CreateAchievementImageRequest struct {
Name string
Data []byte
}

// https://developer.apple.com/documentation/appstoreconnectapi/create_an_achievement_image
func (c *Client) CreateAchievementImage(ctx context.Context, id AchievementLocalizationID, r *CreateAchievementImageRequest) (*Asset, error) {
url := "https://api.appstoreconnect.apple.com/v1/gameCenterAchievementImages"
attr := CreateAssetAttr{
Name: r.Name,
Size: len(r.Data),
}
req := newCreateRequest(attr, "gameCenterAchievementImages", relation{ID: string(id), Type: "gameCenterAchievementLocalizations"})

asset, err := doCreate[Asset](c, ctx, url, req)
if err != nil {
return nil, fmt.Errorf("failed to create achievement image: %w", err)
}

asset, err = c.uploadAsset(ctx, asset, r.Data)
if err != nil {
return nil, fmt.Errorf("failed to upload achievement image: %w", err)
}

return asset, nil
}
2 changes: 1 addition & 1 deletion achievements_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (c *Client) DeleteAchievementLocalization(ctx context.Context, id Achieveme
}

// https://developer.apple.com/documentation/appstoreconnectapi/delete_an_achievement_image
func (c *Client) DeleteAchievementImage(ctx context.Context, id AchievementImageID) error {
func (c *Client) DeleteAchievementImage(ctx context.Context, id string) error {
url := fmt.Sprintf("https://api.appstoreconnect.apple.com/v1/gameCenterAchievementImages/%s", id)

if err := doDelete(c, ctx, url); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions achievements_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
)

// https://developer.apple.com/documentation/appstoreconnectapi/read_achievement_image_information
func (c *Client) GetAchievementImage(ctx context.Context, id AchievementLocalizationID) (*AchievementImage, error) {
func (c *Client) GetAchievementImage(ctx context.Context, id AchievementLocalizationID) (*Asset, error) {
url := fmt.Sprintf("https://api.appstoreconnect.apple.com/v1/gameCenterAchievementLocalizations/%s/gameCenterAchievementImage", id)

resp, err := doGet[getResponse[AchievementImage]](c, ctx, url)
resp, err := doGet[getResponse[Asset]](c, ctx, url)
if err != nil {
return nil, fmt.Errorf("failed to get achievement image: %w", err)
}
Expand Down
28 changes: 27 additions & 1 deletion assets.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package appstore

import "fmt"

type AssetState string

const (
Expand All @@ -20,6 +22,18 @@ type AssetDeliveryState struct {
Warnings []AssetStateError `json:"warnings"`
}

func (s AssetDeliveryState) IsComplete() bool {
return s.State == AssetStateComplete
}

func (s AssetDeliveryState) Error() error {
if s.State == AssetStateFailed {
return fmt.Errorf("asset failed: %v", s.Errors)
}

return nil
}

type ImageAsset struct {
TemplateURL string `json:"templateUrl"`
Width int `json:"width"`
Expand All @@ -39,10 +53,22 @@ type UploadOperation struct {
Headers []HttpHeader `json:"requestHeaders"`
}

type Asset struct {
type AssetAttr struct {
Name string `json:"fileName"`
Size int `json:"fileSize"`
State AssetDeliveryState `json:"assetDeliveryState"`
Image ImageAsset `json:"imageAsset"`
Operations []UploadOperation `json:"uploadOperations"`
}

type Asset struct {
ID string `json:"id"`
Attr AssetAttr `json:"attributes"`
Type string `json:"type"`
Links links `json:"links"`
}

type CreateAssetAttr struct {
Name string `json:"fileName"`
Size int `json:"fileSize"`
}
119 changes: 119 additions & 0 deletions assets_upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package appstore

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
)

func (c *Client) uploadAsset(ctx context.Context, asset *Asset, data []byte) (*Asset, error) {
wg := sync.WaitGroup{}
errs := make(chan error, len(asset.Attr.Operations))

for _, op := range asset.Attr.Operations {
wg.Add(1)

go func(op UploadOperation) {
defer wg.Done()

buff := bytes.NewBuffer(data[op.Offset : op.Offset+op.Length])
err := c.uploadAssetChunk(ctx, op, buff)
if err != nil {
errs <- fmt.Errorf("failed to upload asset chunk: %w", err)
}
}(op)
}

wg.Wait()
close(errs)

if len(errs) > 0 {
return nil, fmt.Errorf("failed to upload asset: %w", <-errs)
}

if err := c.commitAsset(ctx, asset); err != nil {
return nil, fmt.Errorf("failed to commit asset: %w", err)
}

asset, err := doGet[Asset](c, ctx, asset.Links.Self)
if err != nil {
return nil, fmt.Errorf("failed to get asset: %w", err)
}

return asset, nil
}

func (c *Client) uploadAssetChunk(ctx context.Context, op UploadOperation, data io.Reader) error {
req, err := http.NewRequestWithContext(ctx, op.Method, op.URL, data)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

for _, h := range op.Headers {
req.Header.Set(h.Name, h.Value)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

return nil
}

type commitAssetRequest struct {
Data struct {
ID string `json:"id"`
Type string `json:"type"`
Attr struct {
Uploaded bool `json:"uploaded"`
Checksum string `json:"sourceFileChecksum,omitempty"`
} `json:"attributes"`
} `json:"data"`
}

func (c *Client) commitAsset(ctx context.Context, asset *Asset) error {
commit := commitAssetRequest{}
commit.Data.ID = asset.ID
commit.Data.Type = asset.Type
commit.Data.Attr.Uploaded = true

body, err := json.Marshal(commit)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPatch, asset.Links.Self, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

token, err := c.tokenSource.Token()
if err != nil {
return fmt.Errorf("failed to get token: %w", err)
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Content-Type", "application/json")

resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

return nil
}

0 comments on commit 3903149

Please sign in to comment.