-
-
Notifications
You must be signed in to change notification settings - Fork 22
/
upload_operation.go
127 lines (114 loc) · 3.28 KB
/
upload_operation.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package asc
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"sync"
)
// UploadOperation defines model for UploadOperation.
//
// https://developer.apple.com/documentation/appstoreconnectapi/uploadoperation
// https://developer.apple.com/documentation/appstoreconnectapi/uploading_assets_to_app_store_connect
type UploadOperation struct {
Length *int `json:"length,omitempty"`
Method *string `json:"method,omitempty"`
Offset *int `json:"offset,omitempty"`
RequestHeaders []UploadOperationHeader `json:"requestHeaders,omitempty"`
URL *string `json:"url,omitempty"`
}
// UploadOperationHeader defines model for UploadOperationHeader.
//
// https://developer.apple.com/documentation/appstoreconnectapi/uploadoperationheader
type UploadOperationHeader struct {
Name *string `json:"name,omitempty"`
Value *string `json:"value,omitempty"`
}
// UploadOperationError pairs a failed operation and its associated error so it
// can be retried later.
type UploadOperationError struct {
Operation UploadOperation
Err error
}
func (e UploadOperationError) Error() string {
return e.Err.Error()
}
// chunk returns the bytes in the file from the given offset and with the given length.
func (op *UploadOperation) chunk(f io.ReadSeeker) (io.Reader, error) {
if op.Offset == nil || op.Length == nil {
return nil, errors.New("could not establish bounds of upload operation")
}
_, err := f.Seek(int64(*op.Offset), 0)
if err != nil {
return nil, err
}
data := make([]byte, *op.Length)
_, err = f.Read(data)
if err != nil {
return nil, err
}
return bytes.NewBuffer(data), nil
}
// request creates a new http.request instance from the given UploadOperation and buffer.
func (op *UploadOperation) request(ctx context.Context, data io.Reader) (*http.Request, error) {
if op.Method == nil || op.URL == nil {
return nil, errors.New("could not establish destination of upload operation")
}
req, err := http.NewRequestWithContext(ctx, *op.Method, *op.URL, data)
if err != nil {
return nil, err
}
if op.RequestHeaders != nil {
for _, h := range op.RequestHeaders {
if h.Name == nil || h.Value == nil {
continue
}
req.Header.Add(*h.Name, *h.Value)
}
}
return req, nil
}
// Upload takes a file path and concurrently uploads each part of the file to App Store Connect.
func (c *Client) Upload(ctx context.Context, ops []UploadOperation, file io.ReadSeeker) error {
var wg sync.WaitGroup
errs := make(chan UploadOperationError)
for i, operation := range ops {
chunk, err := operation.chunk(file)
if err != nil {
errs <- UploadOperationError{
Operation: operation,
Err: err,
}
continue
}
wg.Add(1)
go c.uploadChunk(ctx, ops[i], chunk, errs, &wg)
}
go func() {
wg.Wait()
close(errs)
}()
for err := range errs {
return err
}
return nil
}
func (c *Client) uploadChunk(ctx context.Context, op UploadOperation, chunk io.Reader, errs chan<- UploadOperationError, wg *sync.WaitGroup) {
defer wg.Done()
req, err := op.request(ctx, chunk)
if err != nil {
errs <- UploadOperationError{
Operation: op,
Err: err,
}
return
}
_, err = c.do(ctx, req, nil)
if err != nil {
errs <- UploadOperationError{
Operation: op,
Err: err,
}
}
}