fastlane-plugin-deploy_file_provider 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/fastlane/plugin/deploy_file_provider.rb +1 -4
- data/lib/fastlane/plugin/deploy_file_provider/actions/deploy_file_provider_action.rb +13 -330
- data/lib/fastlane/plugin/deploy_file_provider/helper/android_file_helper.rb +197 -0
- data/lib/fastlane/plugin/deploy_file_provider/helper/ios_file_helper.rb +153 -0
- data/lib/fastlane/plugin/deploy_file_provider/model/metadata.rb +3 -0
- data/lib/fastlane/plugin/deploy_file_provider/provider/metadata_changes_provider.rb +91 -0
- data/lib/fastlane/plugin/deploy_file_provider/version.rb +1 -1
- metadata +6 -3
- data/lib/fastlane/plugin/deploy_file_provider/helper/deploy_file_provider_helper.rb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ebed9df987808b0bcc218eae765472a58a7b5ecc
|
4
|
+
data.tar.gz: 074595e51ab2ceb40544f2b6eabb243836402a10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48079145a09494425492b6a7b74c0c57d43a838e7566e5f53ab5b352d9f4b48d288698dc1ea5b74c76aa079b406e1f9196b96f1c3b05129d364622448b36cab9
|
7
|
+
data.tar.gz: e198b6d609b5742093faaccaa90b0924534a13b52db6c84cbe807903ec7e2301778de7765a1013d117806e8dea398400902e0ad86db23c88ad8246bba1d177d0
|
@@ -2,15 +2,12 @@ require 'fastlane/plugin/deploy_file_provider/version'
|
|
2
2
|
|
3
3
|
module Fastlane
|
4
4
|
module DeployFileProvider
|
5
|
-
# Return all .rb files inside the "actions" and "helper" directory
|
6
5
|
def self.all_classes
|
7
|
-
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
6
|
+
Dir[File.expand_path('**/{actions,provider,model,helper}/*.rb', File.dirname(__FILE__))]
|
8
7
|
end
|
9
8
|
end
|
10
9
|
end
|
11
10
|
|
12
|
-
# By default we want to import all available actions and helpers
|
13
|
-
# A plugin can contain any number of actions and plugins
|
14
11
|
Fastlane::DeployFileProvider.all_classes.each do |current|
|
15
12
|
require current
|
16
13
|
end
|
@@ -4,346 +4,27 @@ require 'fileutils'
|
|
4
4
|
module Fastlane
|
5
5
|
module Actions
|
6
6
|
|
7
|
-
class MetaData
|
8
|
-
attr_accessor :description_android, :description_ios, :language, :releaseNotesiOS, :releaseNotesAndroid
|
9
|
-
end
|
10
|
-
|
11
7
|
class DeployFileProviderAction < Action
|
12
8
|
RUN_VARIANT_ANDROID = "android"
|
13
9
|
RUN_VARIANT_IOS = "ios"
|
14
10
|
|
15
|
-
ANDROID_CHANGELOG_DIR = "changelogs/"
|
16
|
-
ANDROID_APK_INFO_DIR = ""
|
17
|
-
ANDROID_APK_INFO_FILENAME = "apkInfo.txt"
|
18
|
-
#ANDROID_CHANGELOG_FILENAME -> changes depending on build, fetched from api
|
19
|
-
|
20
|
-
ANDROID_FULL_DESCRIPTION_DIR = ""
|
21
|
-
ANDROID_FULL_DESCRIPTION_FILENAME = "full_description.txt"
|
22
|
-
|
23
|
-
ANDROID_METADATA_LOC = {
|
24
|
-
"german" => "de-DE/",
|
25
|
-
"english" => "en-GB/",
|
26
|
-
"spanish" => "es-ES/",
|
27
|
-
"french" => "fr-FR/",
|
28
|
-
"italian" => "it-IT/",
|
29
|
-
"polish" => "pl-PL/",
|
30
|
-
"portugese" => "pt-PT/",
|
31
|
-
"romanian" => "ro/",
|
32
|
-
"russian" => "ru-RU/",
|
33
|
-
"turkish" => "tr-TR/"
|
34
|
-
}
|
35
|
-
|
36
|
-
IOS_RELEASE_NOTES_DIR = ""
|
37
|
-
IOS_RELEASE_NOTES_FILENAME = "release_notes.txt"
|
38
|
-
|
39
|
-
IOS_DESCRIPTION_DIR = ""
|
40
|
-
IOS_DESCRIPTION_FILENAME = "description.txt"
|
41
|
-
|
42
|
-
IOS_METADATA_LOC = {
|
43
|
-
"german" => "de-DE/",
|
44
|
-
"english" => "en-GB/",
|
45
|
-
"spanish" => "es-ES/",
|
46
|
-
"french" => "fr-FR/",
|
47
|
-
"italian" => "it/",
|
48
|
-
"portugese" => "pt-PT/",
|
49
|
-
"russian" => "ru/",
|
50
|
-
"turkish" => "tr/"
|
51
|
-
}
|
52
|
-
|
53
11
|
def self.run(params)
|
54
|
-
|
12
|
+
UI.message("DeployFileProvider plugin starts running!".yellow)
|
13
|
+
|
14
|
+
# Init variables
|
55
15
|
platform = "#{params[:platform]}".downcase
|
56
|
-
metadata_root = "#{params[:metaDataRoot]}"
|
57
|
-
store_credentials_dir = "#{params[:json_key]}" unless params[:json_key].nil?
|
58
16
|
|
17
|
+
# Checking for metadata changes
|
18
|
+
country_MetaData = Provider::MetadataChangesProvider.fetchMetaDataChanges(params)
|
19
|
+
|
20
|
+
# Running plugin for specific platform
|
59
21
|
if platform.eql? RUN_VARIANT_ANDROID
|
60
|
-
|
61
|
-
prepareFilesFor_Android(params, countryMetaData, platform, metadata_root)
|
22
|
+
Helper::AndroidFileHelper.prepareFiles(params, country_MetaData)
|
62
23
|
elsif platform.eql? RUN_VARIANT_IOS
|
63
|
-
|
64
|
-
prepareFilesFor_iOS(params, countryMetaData, platform, metadata_root)
|
24
|
+
Helper::AndroidFileHelper.prepareFiles(params, country_MetaData)
|
65
25
|
else
|
66
|
-
|
67
|
-
|
68
|
-
end
|
69
|
-
|
70
|
-
def self.fetchMetadataUpdates(params)
|
71
|
-
logStep("Attempting to fetch JSON with metadata updates.")
|
72
|
-
|
73
|
-
descriptionsArray = Helper::DeployFileProviderHelper.get_descriptions_array(params)
|
74
|
-
|
75
|
-
if descriptionsArray == nil || descriptionsArray.empty?
|
76
|
-
throwError("Couldn't fetch metadata updates. Plugin won't run!")
|
77
|
-
else
|
78
|
-
logStep("Fetch successfull!")
|
79
|
-
end
|
80
|
-
|
81
|
-
return metaDataObjectsArray(descriptionsArray)
|
82
|
-
end
|
83
|
-
|
84
|
-
def self.metaDataObjectsArray(array)
|
85
|
-
tempArray = []
|
86
|
-
array.each do |row|
|
87
|
-
tempMetaData = MetaData.new
|
88
|
-
tempMetaData.language = row[0]
|
89
|
-
tempMetaData.description_android = row[1]
|
90
|
-
tempMetaData.description_ios = row[2]
|
91
|
-
tempMetaData.releaseNotesiOS = row[3]
|
92
|
-
tempMetaData.releaseNotesAndroid = row[4]
|
93
|
-
tempArray << tempMetaData
|
94
|
-
end
|
95
|
-
return tempArray
|
96
|
-
end
|
97
|
-
|
98
|
-
def self.fetchCurrentMetaDataFor_Android(metadata_root, json_key)
|
99
|
-
clearMetaDataDir(metadata_root)
|
100
|
-
fetchTask_Android = "supply init --metadata_path #{metadata_root} --json_key #{json_key}"
|
101
|
-
|
102
|
-
logStep("Executing command: " + fetchTask_Android)
|
103
|
-
|
104
|
-
Action.sh(fetchTask_Android)
|
105
|
-
end
|
106
|
-
|
107
|
-
def self.fetchCurrentMetaDataFor_iOS(metadata_root)
|
108
|
-
clearMetaDataDir(metadata_root)
|
109
|
-
fetchTask_iOS = "deliver download_metadata --force -m #{metadata_root} --overwrite_screenshots"
|
110
|
-
puts fetchTask_iOS
|
111
|
-
logStep("Executing command: " + fetchTask_iOS)
|
112
|
-
|
113
|
-
Action.sh(fetchTask_iOS)
|
114
|
-
end
|
115
|
-
|
116
|
-
def self.clearMetaDataDir(metadata_root)
|
117
|
-
if (File.directory?(metadata_root))
|
118
|
-
logStep("Removed old metadata folder in location: " + metadata_root)
|
119
|
-
FileUtils.rm_rf(metadata_root)
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def self.prepareFilesFor_Android(params, countryMetaData, platform, metadata_root)
|
124
|
-
logStep("The deploy_file_provider plugin runs for platform: " + platform + ".")
|
125
|
-
android_changelog_filename = fetchVersionCodeFor_Android(params) + ".txt"
|
126
|
-
|
127
|
-
if (locationExists(metadata_root))
|
128
|
-
numOfLanguagesToHandle = ANDROID_METADATA_LOC.size
|
129
|
-
numOfLanguagesProcessed = 0
|
130
|
-
|
131
|
-
for i in 0 ... countryMetaData.size
|
132
|
-
languageMetaData = countryMetaData[i]
|
133
|
-
languageKey = languageMetaData.language.downcase
|
134
|
-
unless ANDROID_METADATA_LOC.key?(languageKey)
|
135
|
-
next
|
136
|
-
end
|
137
|
-
logStep("---- Attempting to create files for language - " + languageKey)
|
138
|
-
|
139
|
-
create_descriptionFile_forLanguage_forANDROID(metadata_root, languageKey, languageMetaData)
|
140
|
-
create_changeLogFile_forLanguage_withName(metadata_root, languageKey, languageMetaData, android_changelog_filename)
|
141
|
-
|
142
|
-
numOfLanguagesProcessed += 1
|
143
|
-
logStep("---- Files updated")
|
144
|
-
end
|
145
|
-
|
146
|
-
if numOfLanguagesProcessed != numOfLanguagesToHandle
|
147
|
-
throwError("Android expected to receive metadata for: #{numOfLanguagesToHandle} languages, but received for: #{numOfLanguagesProcessed}!")
|
148
|
-
end
|
149
|
-
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def self.fetchVersionCodeFor_Android(params)
|
154
|
-
logStep("Attempting to get Android .apk versionCode from " + ANDROID_APK_INFO_FILENAME + " located in: " + ANDROID_APK_INFO_DIR)
|
155
|
-
|
156
|
-
file = ANDROID_APK_INFO_DIR + ANDROID_APK_INFO_FILENAME
|
157
|
-
|
158
|
-
version_code_header = "Version code: "
|
159
|
-
android_apk_version_code = nil
|
160
|
-
if File.exists?(file)
|
161
|
-
File.open(file) do |f|
|
162
|
-
f.each_line do |line|
|
163
|
-
if line.include?(version_code_header)
|
164
|
-
android_apk_version_code = line.gsub(/\D/, '')
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
if android_apk_version_code.empty? || android_apk_version_code == nil
|
171
|
-
throwError("Unable to fetch Android versionCode. No #{ANDROID_APK_INFO_FILENAME} was found.")
|
172
|
-
else
|
173
|
-
logStep("Fetch successfull! Version code: #{android_apk_version_code}")
|
174
|
-
end
|
175
|
-
|
176
|
-
return android_apk_version_code
|
177
|
-
end
|
178
|
-
|
179
|
-
def self.create_descriptionFile_forLanguage_forANDROID(metadata_root, languageKey, languageMetaData)
|
180
|
-
fulldescription_metaDataDir = [
|
181
|
-
metadata_root,
|
182
|
-
ANDROID_METADATA_LOC[languageKey],
|
183
|
-
ANDROID_FULL_DESCRIPTION_DIR].join
|
184
|
-
logStep("Attempting to prepare " + ANDROID_FULL_DESCRIPTION_FILENAME + " for language: " + languageKey + " - in location: " + fulldescription_metaDataDir)
|
185
|
-
|
186
|
-
if (locationExists(fulldescription_metaDataDir))
|
187
|
-
file = fulldescription_metaDataDir + ANDROID_FULL_DESCRIPTION_FILENAME
|
188
|
-
|
189
|
-
if File.exists?(file)
|
190
|
-
File.delete(file)
|
191
|
-
end
|
192
|
-
|
193
|
-
newFullDescriptionFile = File.new(file, "w")
|
194
|
-
begin
|
195
|
-
newFullDescriptionFile.puts(languageMetaData.description_android)
|
196
|
-
ensure
|
197
|
-
newFullDescriptionFile.close
|
198
|
-
end
|
199
|
-
|
200
|
-
newFullDescriptionFile = File.open(file, "r")
|
201
|
-
begin
|
202
|
-
content = newFullDescriptionFile.read
|
203
|
-
logStep(ANDROID_FULL_DESCRIPTION_FILENAME + " - length: #{content.length}")
|
204
|
-
ensure
|
205
|
-
newFullDescriptionFile.close
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
def self.create_changeLogFile_forLanguage_withName(metadata_root, languageKey, languageMetaData, filename)
|
211
|
-
changelog_metaDataDir = [
|
212
|
-
metadata_root,
|
213
|
-
ANDROID_METADATA_LOC[languageKey],
|
214
|
-
ANDROID_CHANGELOG_DIR].join
|
215
|
-
logStep("Attempting to prepare changelog file with name: " + filename + " for language: " + languageKey + " - in location: " + changelog_metaDataDir)
|
216
|
-
|
217
|
-
if (locationExists(changelog_metaDataDir))
|
218
|
-
file = changelog_metaDataDir + filename
|
219
|
-
|
220
|
-
if File.exists?(file)
|
221
|
-
File.delete(file)
|
222
|
-
end
|
223
|
-
|
224
|
-
newChangeLogFile = File.new(file, "w")
|
225
|
-
begin
|
226
|
-
newChangeLogFile.puts(languageMetaData.releaseNotesAndroid)
|
227
|
-
ensure
|
228
|
-
newChangeLogFile.close
|
229
|
-
end
|
230
|
-
|
231
|
-
newChangeLogFile = File.open(file, "r")
|
232
|
-
begin
|
233
|
-
content = newChangeLogFile.read
|
234
|
-
logStep(filename + " - length: #{content.length}")
|
235
|
-
ensure
|
236
|
-
newChangeLogFile.close
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def self.prepareFilesFor_iOS(params, countryMetaData, platform, metadata_root)
|
242
|
-
logStep("The deploy_file_provider plugin runs for platform: " + platform + ".")
|
243
|
-
|
244
|
-
if (locationExists(metadata_root))
|
245
|
-
numOfLanguagesToHandle = IOS_METADATA_LOC.size
|
246
|
-
numOfLanguagesProcessed = 0
|
247
|
-
|
248
|
-
for i in 0 ... countryMetaData.size
|
249
|
-
languageMetaData = countryMetaData[i]
|
250
|
-
languageKey = languageMetaData.language.downcase
|
251
|
-
unless IOS_METADATA_LOC.key?(languageKey)
|
252
|
-
next
|
253
|
-
end
|
254
|
-
|
255
|
-
create_descriptionFile_forLanguage_forIOS(metadata_root, languageKey, languageMetaData)
|
256
|
-
create_releaseNotesFile_forLanguage(metadata_root, languageKey, languageMetaData)
|
257
|
-
|
258
|
-
numOfLanguagesProcessed += 1
|
259
|
-
logStep("---- Files updated")
|
260
|
-
end
|
261
|
-
|
262
|
-
if numOfLanguagesProcessed != numOfLanguagesToHandle
|
263
|
-
throwError("iOS expected to receive metadata for: #{numOfLanguagesToHandle} languages, but received for: #{numOfLanguagesProcessed}!")
|
264
|
-
end
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
def self.create_descriptionFile_forLanguage_forIOS(metadata_root, languageKey, languageMetaData)
|
269
|
-
description_metaDataDir = [
|
270
|
-
metadata_root,
|
271
|
-
IOS_METADATA_LOC[languageKey],
|
272
|
-
IOS_DESCRIPTION_DIR].join
|
273
|
-
logStep("Attempting to prepare " + IOS_DESCRIPTION_FILENAME + " for language: " + languageKey + " - in location: " + description_metaDataDir)
|
274
|
-
|
275
|
-
if (locationExists(description_metaDataDir))
|
276
|
-
file = description_metaDataDir + IOS_DESCRIPTION_FILENAME
|
277
|
-
|
278
|
-
if File.exists?(file)
|
279
|
-
File.delete(file)
|
280
|
-
end
|
281
|
-
|
282
|
-
newFullDescriptionFile = File.new(file, "w+")
|
283
|
-
begin
|
284
|
-
newFullDescriptionFile.puts(languageMetaData.description_ios)
|
285
|
-
logStep(IOS_DESCRIPTION_FILENAME + " - length: #{newFullDescriptionFile.read.length}")
|
286
|
-
ensure
|
287
|
-
newFullDescriptionFile.close
|
288
|
-
end
|
289
|
-
|
290
|
-
newFullDescriptionFile = File.open(file, "r")
|
291
|
-
begin
|
292
|
-
content = newFullDescriptionFile.read
|
293
|
-
logStep(IOS_DESCRIPTION_FILENAME + " - length: #{content.length}")
|
294
|
-
ensure
|
295
|
-
newFullDescriptionFile.close
|
296
|
-
end
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
|
-
def self.create_releaseNotesFile_forLanguage(metadata_root, languageKey, languageMetaData)
|
301
|
-
releasenotes_metaDataDir = [
|
302
|
-
metadata_root,
|
303
|
-
IOS_METADATA_LOC[languageKey],
|
304
|
-
IOS_RELEASE_NOTES_DIR].join
|
305
|
-
logStep("Attempting to prepare " + IOS_RELEASE_NOTES_FILENAME + " file for language: " + languageKey + " - in location: " + releasenotes_metaDataDir)
|
306
|
-
|
307
|
-
if (locationExists(releasenotes_metaDataDir))
|
308
|
-
file = releasenotes_metaDataDir + IOS_RELEASE_NOTES_FILENAME
|
309
|
-
|
310
|
-
if File.exists?(file)
|
311
|
-
File.delete(file)
|
312
|
-
end
|
313
|
-
|
314
|
-
newReleaseNotesFile = File.new(file, "w+")
|
315
|
-
begin
|
316
|
-
newReleaseNotesFile.puts(languageMetaData.releaseNotesiOS)
|
317
|
-
logStep(IOS_RELEASE_NOTES_FILENAME + " - length: #{newReleaseNotesFile.read.length}")
|
318
|
-
ensure
|
319
|
-
newReleaseNotesFile.close
|
320
|
-
end
|
321
|
-
|
322
|
-
newReleaseNotesFile = File.open(file, "r")
|
323
|
-
begin
|
324
|
-
content = newReleaseNotesFile.read
|
325
|
-
logStep(IOS_RELEASE_NOTES_FILENAME + " - length: #{content.length}")
|
326
|
-
ensure
|
327
|
-
newReleaseNotesFile.close
|
328
|
-
end
|
329
|
-
end
|
330
|
-
end
|
331
|
-
|
332
|
-
def self.logStep(message)
|
333
|
-
UI.message("Step: ".blue + message.blue)
|
334
|
-
end
|
335
|
-
|
336
|
-
def self.throwError(message)
|
337
|
-
UI.message("Error: ".red + message.red)
|
338
|
-
raise Exception, "Lane was stopped by script"
|
339
|
-
end
|
340
|
-
|
341
|
-
def self.locationExists(path)
|
342
|
-
if File.directory?(path)
|
343
|
-
return true
|
344
|
-
else
|
345
|
-
throwError("Could not find location '" + path + "'. Did you fetch metadata from store before launching plugin?")
|
346
|
-
return false
|
26
|
+
UI.message("Error: Unknown platform. Plugin won't run!".red)
|
27
|
+
raise Exception, "Lane was stopped by script"
|
347
28
|
end
|
348
29
|
end
|
349
30
|
|
@@ -362,6 +43,7 @@ module Fastlane
|
|
362
43
|
description: "For which platform files should be prepared",
|
363
44
|
is_string: true,
|
364
45
|
optional: false),
|
46
|
+
|
365
47
|
FastlaneCore::ConfigItem.new(key: :apiCredentialsPath,
|
366
48
|
env_name: "API_CREDENTIALS_PATH",
|
367
49
|
description: "File that contains your OAuth2.0 data",
|
@@ -382,6 +64,7 @@ module Fastlane
|
|
382
64
|
description: "Sphreadsheet OAuth2 credentials url",
|
383
65
|
is_string: true,
|
384
66
|
optional: false),
|
67
|
+
|
385
68
|
FastlaneCore::ConfigItem.new(key: :metaDataRoot,
|
386
69
|
env_name: "METADATA_ROOT",
|
387
70
|
description: "Path to metadata root location",
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module Fastlane
|
2
|
+
module Helper
|
3
|
+
class AndroidFileHelper
|
4
|
+
|
5
|
+
ANDROID_CHANGELOG_DIR = "changelogs/"
|
6
|
+
ANDROID_APK_INFO_DIR = ""
|
7
|
+
ANDROID_APK_INFO_FILENAME = "apkInfo.txt"
|
8
|
+
#ANDROID_CHANGELOG_FILENAME -> changes depending on apk version code
|
9
|
+
|
10
|
+
ANDROID_FULL_DESCRIPTION_DIR = ""
|
11
|
+
ANDROID_FULL_DESCRIPTION_FILENAME = "full_description.txt"
|
12
|
+
|
13
|
+
ANDROID_METADATA_LOC = {
|
14
|
+
"german" => "de-DE/",
|
15
|
+
"english" => "en-GB/",
|
16
|
+
"spanish" => "es-ES/",
|
17
|
+
"french" => "fr-FR/",
|
18
|
+
"italian" => "it-IT/",
|
19
|
+
"polish" => "pl-PL/",
|
20
|
+
"portugese" => "pt-PT/",
|
21
|
+
"romanian" => "ro/",
|
22
|
+
"russian" => "ru-RU/",
|
23
|
+
"turkish" => "tr-TR/"
|
24
|
+
}
|
25
|
+
|
26
|
+
public
|
27
|
+
def self.prepareFiles(params, country_MetaData)
|
28
|
+
UI.message("Preparing files for ANDROID PlayStore release.".yellow)
|
29
|
+
|
30
|
+
# MetaData root
|
31
|
+
metadata_root_dir = "#{params[:metaDataRoot]}"
|
32
|
+
UI.message(["Step: Location where metadata will be downloaded:", metadata_root_dir].join(" ").blue)
|
33
|
+
|
34
|
+
# Android PlayStore credentials
|
35
|
+
store_credentials_dir = "#{params[:json_key]}" unless params[:json_key].nil?
|
36
|
+
UI.message(["Step: Location of credentials for Android PlayStore:", store_credentials_dir].join(" ").blue)
|
37
|
+
|
38
|
+
# Get current metadata from Android PlayStore
|
39
|
+
UI.message("Preparing location for metadata download.".yellow)
|
40
|
+
clearMetaDataDir(metadata_root_dir)
|
41
|
+
|
42
|
+
UI.message("Step: Fetching current metadata from Android PlayStore".blue)
|
43
|
+
fetchCurrentMetaData(metadata_root_dir, store_credentials_dir)
|
44
|
+
|
45
|
+
# Get current *.apk version
|
46
|
+
UI.message("Checking version of *.apk which is about to be pushed.".yellow)
|
47
|
+
versionCode = fetchVersionCode(params)
|
48
|
+
android_changelog_filename = [versionCode, ".txt"].join("")
|
49
|
+
|
50
|
+
# Prepare files for release
|
51
|
+
UI.message("Applying changes to fetched metadata.".yellow)
|
52
|
+
num_of_languages = ANDROID_METADATA_LOC.size
|
53
|
+
num_of_processed_languages = 0
|
54
|
+
for i in 0...country_MetaData.size
|
55
|
+
language_MetaData = country_MetaData[i]
|
56
|
+
language_Key = language_MetaData.language.downcase
|
57
|
+
|
58
|
+
unless ANDROID_METADATA_LOC.key?(language_Key)
|
59
|
+
next
|
60
|
+
end
|
61
|
+
|
62
|
+
UI.message(["Step: ---- Attempting to create files for language -", language_Key].join(" ").blue)
|
63
|
+
|
64
|
+
create_description(metadata_root_dir, language_Key, language_MetaData)
|
65
|
+
create_changelog(metadata_root_dir, language_Key, language_MetaData, android_changelog_filename)
|
66
|
+
num_of_processed_languages += 1
|
67
|
+
|
68
|
+
UI.message("Step: ---- Files updated".blue)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check if all languages were processed
|
72
|
+
if num_of_processed_languages != num_of_languages
|
73
|
+
UI.message("Error: expected to receive metadata for: #{num_of_languages} languages, but received for: #{num_of_processed_languages}!".red)
|
74
|
+
raise Exception, "Lane was stopped by script"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def self.clearMetaDataDir(metadata_root_dir)
|
80
|
+
if (File.directory?(metadata_root_dir))
|
81
|
+
UI.message(["Step: Removed old metadata folder in location:", metadata_root_dir].join(" ").blue)
|
82
|
+
FileUtils.rm_rf(metadata_root_dir)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def self.fetchCurrentMetaData(metadata_root_dir, store_credentials_dir)
|
88
|
+
fetch_command = "supply init --metadata_path #{metadata_root_dir} --json_key #{store_credentials_dir}"
|
89
|
+
Action.sh(fetch_command)
|
90
|
+
|
91
|
+
unless File.directory?(metadata_root_dir)
|
92
|
+
UI.message(["Error: Could not find location'", metadata_root_dir, "'."].join(" ").red)
|
93
|
+
raise Exception, "Lane was stopped by script"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def self.fetchVersionCode(params)
|
99
|
+
UI.message("Note: Android changelog filename is created from .apk version code.".green)
|
100
|
+
UI.message(["Step: Attempting to get Android .apk versionCode from", ANDROID_APK_INFO_FILENAME, "located in:", ANDROID_APK_INFO_DIR].join(" ").blue)
|
101
|
+
|
102
|
+
file = ANDROID_APK_INFO_DIR + ANDROID_APK_INFO_FILENAME
|
103
|
+
|
104
|
+
version_code_header = "Version code: "
|
105
|
+
android_apk_version_code = nil
|
106
|
+
if File.exists?(file)
|
107
|
+
File.open(file) do |f|
|
108
|
+
f.each_line do |line|
|
109
|
+
if line.include?(version_code_header)
|
110
|
+
android_apk_version_code = line.gsub(/\D/, '')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
if android_apk_version_code.empty? || android_apk_version_code == nil
|
117
|
+
UI.message("Error: Unable to fetch Android versionCode. No #{ANDROID_APK_INFO_FILENAME} was found.".red)
|
118
|
+
raise Exception, "Lane was stopped by script"
|
119
|
+
else
|
120
|
+
UI.message("Step: Fetch successfull! Version code: #{android_apk_version_code}".blue)
|
121
|
+
end
|
122
|
+
|
123
|
+
return android_apk_version_code
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def self.create_changelog(metadata_root_dir, language_Key, language_MetaData, filename)
|
128
|
+
changelog_metaDataDir = [
|
129
|
+
metadata_root_dir,
|
130
|
+
ANDROID_METADATA_LOC[language_Key],
|
131
|
+
ANDROID_CHANGELOG_DIR].join
|
132
|
+
|
133
|
+
UI.message(["Step: Attempting to prepare changelog file with name:", filename, "for language:", language_Key, "- in location:", changelog_metaDataDir].join(" ").blue)
|
134
|
+
|
135
|
+
unless File.directory?(changelog_metaDataDir)
|
136
|
+
UI.message(["Error: Could not find location'", changelog_metaDataDir, "'."].join(" ").red)
|
137
|
+
raise Exception, "Lane was stopped by script"
|
138
|
+
end
|
139
|
+
|
140
|
+
file = changelog_metaDataDir + filename
|
141
|
+
if File.exists?(file)
|
142
|
+
File.delete(file)
|
143
|
+
end
|
144
|
+
|
145
|
+
new_File = File.new(file, "w")
|
146
|
+
begin
|
147
|
+
new_File.puts(language_MetaData.releaseNotesAndroid)
|
148
|
+
ensure
|
149
|
+
new_File.close
|
150
|
+
end
|
151
|
+
|
152
|
+
new_File = File.open(file, "r")
|
153
|
+
begin
|
154
|
+
content = new_File.read
|
155
|
+
UI.message(["Step:", filename, "- length: #{content.length}"].join(" ").blue)
|
156
|
+
ensure
|
157
|
+
new_File.close
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
def self.create_description(metadata_root_dir, language_Key, language_MetaData)
|
163
|
+
fulldescription_metaDataDir = [
|
164
|
+
metadata_root_dir,
|
165
|
+
ANDROID_METADATA_LOC[language_Key],
|
166
|
+
ANDROID_FULL_DESCRIPTION_DIR].join
|
167
|
+
|
168
|
+
UI.message(["Step: Attempting to prepare", ANDROID_FULL_DESCRIPTION_FILENAME, "for language:", language_Key, "- in location:",fulldescription_metaDataDir].join(" ").blue)
|
169
|
+
|
170
|
+
unless File.directory?(fulldescription_metaDataDir)
|
171
|
+
UI.message(["Error: Could not find location'", fulldescription_metaDataDir, "'."].join(" ").red)
|
172
|
+
raise Exception, "Lane was stopped by script"
|
173
|
+
end
|
174
|
+
|
175
|
+
file = fulldescription_metaDataDir + ANDROID_FULL_DESCRIPTION_FILENAME
|
176
|
+
if File.exists?(file)
|
177
|
+
File.delete(file)
|
178
|
+
end
|
179
|
+
|
180
|
+
new_File = File.new(file, "w")
|
181
|
+
begin
|
182
|
+
new_File.puts(language_MetaData.description_android)
|
183
|
+
ensure
|
184
|
+
new_File.close
|
185
|
+
end
|
186
|
+
|
187
|
+
new_File = File.open(file, "r")
|
188
|
+
begin
|
189
|
+
content = new_File.read
|
190
|
+
UI.message(["Step:", ANDROID_FULL_DESCRIPTION_FILENAME, "- length: #{content.length}"].join(" ").blue)
|
191
|
+
ensure
|
192
|
+
new_File.close
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module Fastlane
|
2
|
+
module Helper
|
3
|
+
class IOSFileHelper
|
4
|
+
|
5
|
+
IOS_RELEASE_NOTES_DIR = ""
|
6
|
+
IOS_RELEASE_NOTES_FILENAME = "release_notes.txt"
|
7
|
+
|
8
|
+
IOS_DESCRIPTION_DIR = ""
|
9
|
+
IOS_DESCRIPTION_FILENAME = "description.txt"
|
10
|
+
|
11
|
+
IOS_METADATA_LOC = {
|
12
|
+
"german" => "de-DE/",
|
13
|
+
"english" => "en-GB/",
|
14
|
+
"spanish" => "es-ES/",
|
15
|
+
"french" => "fr-FR/",
|
16
|
+
"italian" => "it/",
|
17
|
+
"portugese" => "pt-PT/",
|
18
|
+
"russian" => "ru/",
|
19
|
+
"turkish" => "tr/"
|
20
|
+
}
|
21
|
+
|
22
|
+
public
|
23
|
+
def self.prepareFiles(params, country_MetaData)
|
24
|
+
UI.message("Preparing files for IOS AppStore release.".yellow)
|
25
|
+
|
26
|
+
# MetaData root
|
27
|
+
metadata_root_dir = "#{params[:metaDataRoot]}"
|
28
|
+
UI.message(["Step: Location where metadata will be downloaded:", metadata_root_dir].join(" ").blue)
|
29
|
+
|
30
|
+
# Get current metadata from iOS AppStore
|
31
|
+
UI.message("Preparing location for metadata download.".yellow)
|
32
|
+
clearMetaDataDir(metadata_root_dir)
|
33
|
+
|
34
|
+
UI.message("Step: Fetching current metadata from IOS AppStore".blue)
|
35
|
+
fetchCurrentMetaData(metadata_root_dir)
|
36
|
+
|
37
|
+
# Prepare files for release
|
38
|
+
UI.message("Applying changes to fetched metadata.".yellow)
|
39
|
+
num_of_languages = IOS_METADATA_LOC.size
|
40
|
+
num_of_processed_languages = 0
|
41
|
+
for i in 0 ... country_MetaData.size
|
42
|
+
language_MetaData = country_MetaData[i]
|
43
|
+
language_Key = language_MetaData.language.downcase
|
44
|
+
|
45
|
+
unless IOS_METADATA_LOC.key?(language_Key)
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
UI.message(["Step: ---- Attempting to create files for language -", language_Key].join(" ").blue)
|
50
|
+
|
51
|
+
create_description(metadata_root_dir, language_Key, language_MetaData)
|
52
|
+
create_releaseNotes(metadata_root_dir, language_Key, language_MetaData)
|
53
|
+
num_of_processed_languages += 1
|
54
|
+
|
55
|
+
UI.message("Step: ---- Files updated".blue)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if all languages were processed
|
59
|
+
if num_of_processed_languages != num_of_languages
|
60
|
+
UI.message("Error: expected to receive metadata for: #{num_of_languages} languages, but received for: #{num_of_processed_languages}!".red)
|
61
|
+
raise Exception, "Lane was stopped by script"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def self.clearMetaDataDir(metadata_root_dir)
|
67
|
+
if (File.directory?(metadata_root_dir))
|
68
|
+
UI.message(["Step: Removed old metadata folder in location:", metadata_root_dir].join(" ").blue)
|
69
|
+
FileUtils.rm_rf(metadata_root_dir)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def self.fetchCurrentMetaData(metadata_root_dir)
|
75
|
+
fetch_command = "deliver download_metadata --force -m #{metadata_root_dir} --overwrite_screenshots"
|
76
|
+
Action.sh(fetch_command)
|
77
|
+
|
78
|
+
unless File.directory?(metadata_root_dir)
|
79
|
+
UI.message(["Error: Could not find location'", metadata_root_dir, "'."].join(" ").red)
|
80
|
+
raise Exception, "Lane was stopped by script"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.create_releaseNotes(metadata_root_dir, language_Key, language_MetaData)
|
85
|
+
releaseNotes_metaDataDir = [
|
86
|
+
metadata_root_dir,
|
87
|
+
IOS_METADATA_LOC[language_Key],
|
88
|
+
IOS_RELEASE_NOTES_DIR].join
|
89
|
+
|
90
|
+
UI.message(["Step: Attempting to prepare", IOS_RELEASE_NOTES_FILENAME, "file for language:", language_Key, "- in location:", releaseNotes_metaDataDir].join(" ").blue)
|
91
|
+
|
92
|
+
unless File.directory?(releaseNotes_metaDataDir)
|
93
|
+
UI.message(["Error: Could not find location'", releaseNotes_metaDataDir, "'."].join(" ").red)
|
94
|
+
raise Exception, "Lane was stopped by script"
|
95
|
+
end
|
96
|
+
|
97
|
+
file = releaseNotes_metaDataDir + IOS_RELEASE_NOTES_FILENAME
|
98
|
+
if File.exists?(file)
|
99
|
+
File.delete(file)
|
100
|
+
end
|
101
|
+
|
102
|
+
new_File = File.new(file, "w")
|
103
|
+
begin
|
104
|
+
new_File.puts(language_MetaData.releaseNotesiOS)
|
105
|
+
ensure
|
106
|
+
new_File.close
|
107
|
+
end
|
108
|
+
|
109
|
+
new_File = File.open(file, "r")
|
110
|
+
begin
|
111
|
+
content = new_File.read
|
112
|
+
UI.message(["Step:", IOS_RELEASE_NOTES_FILENAME, "- length: #{content.length}"].join(" ").blue)
|
113
|
+
ensure
|
114
|
+
new_File.close
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.create_description(metadata_root_dir, language_Key, language_MetaData)
|
119
|
+
fulldescription_metaDataDir = [
|
120
|
+
metadata_root_dir,
|
121
|
+
IOS_METADATA_LOC[language_Key],
|
122
|
+
IOS_DESCRIPTION_DIR].join
|
123
|
+
|
124
|
+
UI.message(["Step: Attempting to prepare", IOS_DESCRIPTION_FILENAME, "for language:", language_Key, "- in location:",fulldescription_metaDataDir].join(" ").blue)
|
125
|
+
|
126
|
+
unless File.directory?(fulldescription_metaDataDir)
|
127
|
+
UI.message(["Error: Could not find location'", fulldescription_metaDataDir, "'."].join(" ").red)
|
128
|
+
raise Exception, "Lane was stopped by script"
|
129
|
+
end
|
130
|
+
|
131
|
+
file = fulldescription_metaDataDir + IOS_DESCRIPTION_FILENAME
|
132
|
+
if File.exists?(file)
|
133
|
+
File.delete(file)
|
134
|
+
end
|
135
|
+
|
136
|
+
new_File = File.new(file, "w")
|
137
|
+
begin
|
138
|
+
new_File.puts(language_MetaData.description_ios)
|
139
|
+
ensure
|
140
|
+
new_File.close
|
141
|
+
end
|
142
|
+
|
143
|
+
new_File = File.open(file, "r")
|
144
|
+
begin
|
145
|
+
content = new_File.read
|
146
|
+
UI.message(["Step:", IOS_DESCRIPTION_FILENAME, "- length: #{content.length}"].join(" ").blue)
|
147
|
+
ensure
|
148
|
+
new_File.close
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'google/apis/sheets_v4'
|
2
|
+
require 'googleauth'
|
3
|
+
require 'googleauth/stores/file_token_store'
|
4
|
+
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module Fastlane
|
8
|
+
module Provider
|
9
|
+
class MetadataChangesProvider
|
10
|
+
|
11
|
+
OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'
|
12
|
+
SCOPE = Google::Apis::SheetsV4::AUTH_SPREADSHEETS_READONLY
|
13
|
+
credentials = ''
|
14
|
+
|
15
|
+
public
|
16
|
+
def self.fetchMetaDataChanges(params)
|
17
|
+
UI.message("Attempting to fetch metadata changes.".yellow)
|
18
|
+
|
19
|
+
countryMetaDataArray = getMetaDataArray(params)
|
20
|
+
|
21
|
+
if countryMetaDataArray == nil || countryMetaDataArray.empty?
|
22
|
+
UI.message("Error: Couldn't fetch metadata updates. Plugin won't run!".red)
|
23
|
+
raise Exception, "Lane was stopped by script"
|
24
|
+
else
|
25
|
+
UI.message("Step: Fetch successfull!".blue)
|
26
|
+
end
|
27
|
+
|
28
|
+
return countryMetaDataArray
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def self.getMetaDataArray(params)
|
33
|
+
changesArray = getDescriptionsArray(params)
|
34
|
+
|
35
|
+
tempArray = []
|
36
|
+
changesArray.each do |row|
|
37
|
+
tempMetaData = MetaData.new
|
38
|
+
tempMetaData.language = row[0]
|
39
|
+
tempMetaData.description_android = row[1]
|
40
|
+
tempMetaData.description_ios = row[2]
|
41
|
+
tempMetaData.releaseNotesiOS = row[3]
|
42
|
+
tempMetaData.releaseNotesAndroid = row[4]
|
43
|
+
tempArray << tempMetaData
|
44
|
+
end
|
45
|
+
|
46
|
+
return tempArray
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def self.getDescriptionsArray(params)
|
51
|
+
spreadsheet_id = params[:spreadsheetId]
|
52
|
+
service = self.authorizedService(params)
|
53
|
+
|
54
|
+
range = 'Master!B1:K7'
|
55
|
+
response = service.get_spreadsheet_values(spreadsheet_id, range)
|
56
|
+
puts 'No data found.' if response.values.empty?
|
57
|
+
return self.parseResponseJSON(response)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def self.authorizedService(params)
|
62
|
+
client_secret_path_param = params[:apiCredentialsPath]
|
63
|
+
application_name = params[:spreadsheetApplicationName]
|
64
|
+
credentials_path = params[:credentialsPath]
|
65
|
+
|
66
|
+
service = Google::Apis::SheetsV4::SheetsService.new
|
67
|
+
service.client_options.application_name = application_name
|
68
|
+
client_id = Google::Auth::ClientId.from_file(client_secret_path_param)
|
69
|
+
token_store = Google::Auth::Stores::FileTokenStore.new(file: credentials_path)
|
70
|
+
authorizer = Google::Auth::UserAuthorizer.new(
|
71
|
+
client_id, SCOPE, token_store)
|
72
|
+
user_id = 'default'
|
73
|
+
credentials = authorizer.get_credentials(user_id)
|
74
|
+
service.authorization = credentials
|
75
|
+
return service
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def self.parseResponseJSON(response)
|
80
|
+
transposed = response.values.transpose
|
81
|
+
size = transposed.size
|
82
|
+
array = Array.new()
|
83
|
+
transposed.each do |row|
|
84
|
+
dictionary = {:language => row[0], :descriptionAndroid => row[1], :descriptioniOS => row[2], :releaseNotesiOS => row[3], :releaseNotesAndroid => row[4]}
|
85
|
+
array << dictionary
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fastlane-plugin-deploy_file_provider
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kamil Krzyk, Przemysław Wośko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-11-
|
11
|
+
date: 2016-11-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: google-api-client
|
@@ -118,7 +118,10 @@ files:
|
|
118
118
|
- README.md
|
119
119
|
- lib/fastlane/plugin/deploy_file_provider.rb
|
120
120
|
- lib/fastlane/plugin/deploy_file_provider/actions/deploy_file_provider_action.rb
|
121
|
-
- lib/fastlane/plugin/deploy_file_provider/helper/
|
121
|
+
- lib/fastlane/plugin/deploy_file_provider/helper/android_file_helper.rb
|
122
|
+
- lib/fastlane/plugin/deploy_file_provider/helper/ios_file_helper.rb
|
123
|
+
- lib/fastlane/plugin/deploy_file_provider/model/metadata.rb
|
124
|
+
- lib/fastlane/plugin/deploy_file_provider/provider/metadata_changes_provider.rb
|
122
125
|
- lib/fastlane/plugin/deploy_file_provider/version.rb
|
123
126
|
homepage:
|
124
127
|
licenses:
|
@@ -1,54 +0,0 @@
|
|
1
|
-
require 'google/apis/sheets_v4'
|
2
|
-
require 'googleauth'
|
3
|
-
require 'googleauth/stores/file_token_store'
|
4
|
-
|
5
|
-
require 'fileutils'
|
6
|
-
|
7
|
-
|
8
|
-
module Fastlane
|
9
|
-
module Helper
|
10
|
-
class DeployFileProviderHelper
|
11
|
-
|
12
|
-
OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'
|
13
|
-
SCOPE = Google::Apis::SheetsV4::AUTH_SPREADSHEETS_READONLY
|
14
|
-
credentials = ''
|
15
|
-
|
16
|
-
def self.authorizedService(params)
|
17
|
-
client_secret_path_param = params[:apiCredentialsPath]
|
18
|
-
application_name = params[:spreadsheetApplicationName]
|
19
|
-
credentials_path = params[:credentialsPath]
|
20
|
-
|
21
|
-
service = Google::Apis::SheetsV4::SheetsService.new
|
22
|
-
service.client_options.application_name = application_name
|
23
|
-
client_id = Google::Auth::ClientId.from_file(client_secret_path_param)
|
24
|
-
token_store = Google::Auth::Stores::FileTokenStore.new(file: credentials_path)
|
25
|
-
authorizer = Google::Auth::UserAuthorizer.new(
|
26
|
-
client_id, SCOPE, token_store)
|
27
|
-
user_id = 'default'
|
28
|
-
credentials = authorizer.get_credentials(user_id)
|
29
|
-
service.authorization = credentials
|
30
|
-
return service
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.get_descriptions_array(params)
|
34
|
-
spreadsheet_id = params[:spreadsheetId]
|
35
|
-
service = self.authorizedService(params)
|
36
|
-
|
37
|
-
range = 'Master!B1:K7'
|
38
|
-
response = service.get_spreadsheet_values(spreadsheet_id, range)
|
39
|
-
puts 'No data found.' if response.values.empty?
|
40
|
-
return self.parse_json(response)
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.parse_json(response)
|
44
|
-
transposed = response.values.transpose
|
45
|
-
size = transposed.size
|
46
|
-
array = Array.new()
|
47
|
-
transposed.each do |row|
|
48
|
-
dictionary = {:language => row[0], :descriptionAndroid => row[1], :descriptioniOS => row[2], :releaseNotesiOS => row[3], :releaseNotesAndroid => row[4]}
|
49
|
-
array << dictionary
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|