-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
UploadPreview.swift
239 lines (221 loc) · 8.75 KB
/
UploadPreview.swift
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import AppStoreAPI
import AppStoreConnect
import ArgumentParser
import Crypto
import Foundation
import Utilities
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
@main struct UploadPreview: AsyncParsableCommand {
enum Platform: String, ExpressibleByArgument {
case iOS
case macOS
case tvOS
case visionOS
}
enum PreviewType: String, ExpressibleByArgument {
case iphone67
case iphone61
case iphone65
case iphone58
case iphone55
case iphone47
case iphone40
case iphone35
case ipadPro3gen129
case ipadPro3gen11
case ipadPro129
case ipad105
case ipad97
case desktop
case appleTv
case appleVisionPro
}
@Option var bundleID: String
@Option var platform: Platform
@Option var version: String
@Option var locale: String
@Option var previewType: PreviewType
@Option(
name: [.customLong("file"), .customShort("f")],
completion: .file(),
transform: URL.init(fileURLWithPath:)
)
var previewFileURL: URL
mutating func run() async throws {
// 0. Open preview file for reading
let previewFile = try Data(contentsOf: previewFileURL)
var md5 = Insecure.MD5()
md5.update(data: previewFile)
let digest = md5.finalize()
let previewFileChecksum = digest.description
// 1. Create the App Store Connect client
let client = try AppStoreConnectClient(authenticator: EnvAuthenticator())
// 2. Look up the app by bundle ID.
let app = try await client
.send(
Resources.v1.apps.get(
filterBundleID: [bundleID]
)
)
.data.first!
// 3. Look up the version version by platform and version number.
let version = try await client
.send(
Resources.v1.apps.id(app.id).appStoreVersions
.get(filterPlatform: [.init(platform)], filterVersionString: [version])
)
.data.first!
// 4. Get all localizations for the version and look for the requested locale.
let localizations: [AppStoreVersionLocalization]
let existingLocalizations =
try await client.send(Resources.v1.appStoreVersions.id(version.id).appStoreVersionLocalizations.get()).data
.filter { $0.attributes?.locale == locale }
// 4a. If the requested localization does not exist, create it. Localized attributes are copied from the primary locale so there's no need to worry about them here.
if existingLocalizations.isEmpty {
let newLocalization = try await client.send(
Resources.v1.appStoreVersionLocalizations.post(
.init(
data: .init(
attributes: .init(locale: locale),
relationships: .init(appStoreVersion: .init(data: .init(id: version.id)))
)
)
)
)
localizations = [newLocalization.data]
} else {
localizations = existingLocalizations
}
for localization in localizations {
// 5. Get all available app preview sets from the localization. If a preview set for the desired preview type already exists, use it. Otherwise, make a new one.
var previewSets: [AppPreviewSet] = []
if let related = localization.relationships?.appPreviewSets?.links?.related {
let previewSetsResponse: AppPreviewSetsResponse = try await client.send(.get(related))
previewSets.append(
contentsOf: previewSetsResponse.data.filter { $0.attributes?.previewType == .init(previewType) }
)
}
if previewSets.isEmpty {
let newPreviewSet = try await client.send(
Resources.v1.appPreviewSets.post(
.init(
data: .init(
attributes: .init(previewType: .init(previewType)),
relationships: .init(
appStoreVersionLocalization: .init(data: .init(id: localization.id))
)
)
)
)
)
previewSets.append(newPreviewSet.data)
}
for previewSet in previewSets {
// 6. Reserve an app preview in the selected app preview set. Tell the API to create a preview before uploading the preview data.
print("Reserving space for a new app preview.")
let preview =
try await client.send(
Resources.v1.appPreviews.post(
.init(
data: .init(
attributes: .init(
fileSize: previewFile.count,
fileName: previewFileURL.lastPathComponent
),
relationships: .init(appPreviewSet: .init(data: .init(id: previewSet.id)))
)
)
)
)
.data
guard let uploadOperations = preview.attributes?.uploadOperations else { continue }
print("Uploading \(uploadOperations.count) preview components.")
// 7. Upload each part according to the returned upload operations. The reservation returned uploadOperations, which instructs us how to split the asset into parts. Upload each part individually.
// Note: To speed up the process, upload multiple parts asynchronously if you have the bandwidth.
try await withThrowingTaskGroup(of: Void.self) { group in
for operation in uploadOperations {
group.addTask {
try await client.upload(operation: operation, from: previewFile)
}
}
try await group.waitForAll()
}
// 8. Commit the reservation and provide a checksum. Committing tells App Store Connect the script is finished uploading parts. App Store Connect uses the checksum to ensure the parts were uploaded successfully.
print("Commit the reservation.")
_ = try await client.send(
Resources.v1.appPreviews.id(preview.id)
.patch(
.init(
data: .init(
id: preview.id,
attributes: .init(sourceFileChecksum: previewFileChecksum, isUploaded: true)
)
)
)
)
// 9. Report success to the caller.
print(
"""
App Preview successfully uploaded to:
\(preview.links?.this?.absoluteString ?? "<no preview url>")
You can verify success in App Store Connect or using the API.
"""
)
}
}
}
}
extension Resources.V1.Apps.WithID.AppStoreVersions.FilterPlatform {
init(_ platform: UploadPreview.Platform) {
switch platform {
case .iOS:
self = .iOS
case .macOS:
self = .macOS
case .tvOS:
self = .tvOS
case .visionOS:
self = .visionOS
}
}
}
extension PreviewType {
init(_ previewType: UploadPreview.PreviewType) {
switch previewType {
case .iphone67:
self = .iphone67
case .iphone61:
self = .iphone61
case .iphone65:
self = .iphone65
case .iphone58:
self = .iphone58
case .iphone55:
self = .iphone55
case .iphone47:
self = .iphone47
case .iphone40:
self = .iphone40
case .iphone35:
self = .iphone35
case .ipadPro3gen129:
self = .ipadPro3gen129
case .ipadPro3gen11:
self = .ipadPro3gen11
case .ipadPro129:
self = .ipadPro129
case .ipad105:
self = .ipad105
case .ipad97:
self = .ipad97
case .desktop:
self = .desktop
case .appleTv:
self = .appleTv
case .appleVisionPro:
self = .appleVisionPro
}
}
}