shopify-cli 2.19.0 → 2.20.0
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/.github/ISSUE_TEMPLATE/bug_report.yaml +2 -1
- data/.github/ISSUE_TEMPLATE/config.yml +9 -0
- data/CHANGELOG.md +16 -2
- data/Gemfile.lock +1 -1
- data/README.md +7 -6
- data/docs/users/installation.md +1 -1
- data/lib/project_types/extension/messages/messages.rb +1 -1
- data/lib/project_types/extension/tasks/fetch_specifications.rb +4 -1
- data/lib/project_types/theme/commands/push.rb +3 -1
- data/lib/project_types/theme/commands/serve.rb +1 -0
- data/lib/project_types/theme/messages/messages.rb +38 -1
- data/lib/shopify_cli/assets/post_auth_page/index.html.erb +34 -0
- data/lib/shopify_cli/assets/post_auth_page/style.css +58 -0
- data/lib/shopify_cli/identity_auth/servlet.rb +4 -20
- data/lib/shopify_cli/messages/messages.rb +6 -8
- data/lib/shopify_cli/theme/dev_server/hot-reload-no-script.html +27 -0
- data/lib/shopify_cli/theme/dev_server/hot-reload.js +16 -4
- data/lib/shopify_cli/theme/dev_server/hot_reload.rb +2 -0
- data/lib/shopify_cli/theme/dev_server.rb +3 -2
- data/lib/shopify_cli/theme/file.rb +5 -0
- data/lib/shopify_cli/theme/syncer/json_update_handler.rb +16 -6
- data/lib/shopify_cli/theme/syncer/operation.rb +7 -6
- data/lib/shopify_cli/theme/syncer/unsupported_script_warning.rb +90 -0
- data/lib/shopify_cli/theme/syncer.rb +73 -29
- data/lib/shopify_cli/theme/theme_admin_api.rb +16 -11
- data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk.rb +102 -0
- data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk_job.rb +75 -0
- data/lib/shopify_cli/theme/theme_admin_api_throttler/errors.rb +7 -0
- data/lib/shopify_cli/theme/theme_admin_api_throttler/put_request.rb +52 -0
- data/lib/shopify_cli/theme/theme_admin_api_throttler/request_parser.rb +39 -0
- data/lib/shopify_cli/theme/theme_admin_api_throttler/response_parser.rb +21 -0
- data/lib/shopify_cli/theme/theme_admin_api_throttler.rb +62 -0
- data/lib/shopify_cli/version.rb +1 -1
- metadata +14 -2
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyCLI
|
4
|
+
module Theme
|
5
|
+
class Syncer
|
6
|
+
class UnsupportedScriptWarning
|
7
|
+
attr_reader :ctx
|
8
|
+
|
9
|
+
def initialize(ctx, file)
|
10
|
+
@ctx = ctx
|
11
|
+
@file = file
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"\n\n#{occurrences} #{long_text}"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def occurrences
|
21
|
+
warnings.map { |w| occurrence(w) }.join("\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def occurrence(warning)
|
25
|
+
line_number = "{{blue: #{warning.line} |}}"
|
26
|
+
pointer = pointer_message(warning)
|
27
|
+
|
28
|
+
<<~OCCURRENCE
|
29
|
+
#{line_number} #{warning.line_content}
|
30
|
+
#{pointer}
|
31
|
+
OCCURRENCE
|
32
|
+
end
|
33
|
+
|
34
|
+
def long_text
|
35
|
+
lines_and_columns = warnings.map do |warning|
|
36
|
+
message("line_and_column", warning.line, warning.column)
|
37
|
+
end
|
38
|
+
|
39
|
+
message("unsupported_script_text", lines_and_columns.join)
|
40
|
+
.split("\n")
|
41
|
+
.reduce("") do |text, line|
|
42
|
+
# Add indentation in the long text to improve readability
|
43
|
+
line = " #{line}"
|
44
|
+
|
45
|
+
# Inline yellow (otherwise `CLI::UI::Frame` breaks multiline formatting)
|
46
|
+
line = "{{yellow:#{line}}}"
|
47
|
+
|
48
|
+
"#{text}#{line}\n"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def pointer_message(warning)
|
53
|
+
padding = warning.column + warning.line.to_s.size + 2
|
54
|
+
text = message("unsupported_script")
|
55
|
+
|
56
|
+
"{{yellow:#{" " * padding} ^ {{bold:#{text}}}}}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def message(*args)
|
60
|
+
key = args.shift
|
61
|
+
@ctx.message("theme.serve.syncer.warnings.#{key}", *args)
|
62
|
+
end
|
63
|
+
|
64
|
+
def warnings
|
65
|
+
@warnings ||= @file.warnings.map { |w| Warning.new(@file, w) }
|
66
|
+
end
|
67
|
+
|
68
|
+
class Warning
|
69
|
+
attr_reader :line, :column
|
70
|
+
|
71
|
+
def initialize(file, warning_hash)
|
72
|
+
@file = file
|
73
|
+
@line = warning_hash["line"].to_i
|
74
|
+
@column = warning_hash["column"].to_i
|
75
|
+
end
|
76
|
+
|
77
|
+
def line_content
|
78
|
+
file_lines[line - 1]
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def file_lines
|
84
|
+
@file_lines ||= @file.read.split("\n")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -13,7 +13,9 @@ require_relative "syncer/json_update_handler"
|
|
13
13
|
require_relative "syncer/merger"
|
14
14
|
require_relative "syncer/operation"
|
15
15
|
require_relative "syncer/standard_reporter"
|
16
|
+
require_relative "syncer/unsupported_script_warning"
|
16
17
|
require_relative "theme_admin_api"
|
18
|
+
require_relative "theme_admin_api_throttler"
|
17
19
|
|
18
20
|
module ShopifyCLI
|
19
21
|
module Theme
|
@@ -31,12 +33,12 @@ module ShopifyCLI
|
|
31
33
|
:union_merge, # - Union merges the local file content with the remote file content
|
32
34
|
]
|
33
35
|
|
34
|
-
attr_reader :theme, :checksums, :error_checksums
|
36
|
+
attr_reader :theme, :checksums, :error_checksums, :api_client
|
35
37
|
attr_accessor :include_filter, :ignore_filter
|
36
38
|
|
37
39
|
def_delegators :@error_reporter, :has_any_error?
|
38
40
|
|
39
|
-
def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil, overwrite_json: true)
|
41
|
+
def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil, overwrite_json: true, stable: false)
|
40
42
|
@ctx = ctx
|
41
43
|
@theme = theme
|
42
44
|
@include_filter = include_filter
|
@@ -58,15 +60,21 @@ module ShopifyCLI
|
|
58
60
|
# Mutex used to pause all threads when backing-off when hitting API rate limits
|
59
61
|
@backoff_mutex = Mutex.new
|
60
62
|
|
63
|
+
# Mutex used to coordinate changes in the `pending` list
|
64
|
+
@pending_mutex = Mutex.new
|
65
|
+
|
61
66
|
# Latest theme assets checksums. Updated on each upload.
|
62
67
|
@checksums = Checksums.new(theme)
|
63
68
|
|
64
69
|
# Checksums of assets with errors.
|
65
70
|
@error_checksums = []
|
66
|
-
end
|
67
71
|
|
68
|
-
|
69
|
-
@api_client
|
72
|
+
# Initialize `api_client` on main thread
|
73
|
+
@api_client = ThemeAdminAPIThrottler.new(
|
74
|
+
@ctx,
|
75
|
+
ThemeAdminAPI.new(@ctx, @theme.shop),
|
76
|
+
!stable
|
77
|
+
)
|
70
78
|
end
|
71
79
|
|
72
80
|
def lock_io!
|
@@ -134,6 +142,7 @@ module ShopifyCLI
|
|
134
142
|
end
|
135
143
|
|
136
144
|
def shutdown
|
145
|
+
api_client.shutdown
|
137
146
|
@queue.close unless @queue.closed?
|
138
147
|
ensure
|
139
148
|
@threads.each { |thread| thread.join if thread.alive? }
|
@@ -185,6 +194,9 @@ module ShopifyCLI
|
|
185
194
|
unless delay_low_priority_files
|
186
195
|
wait!(&block)
|
187
196
|
end
|
197
|
+
|
198
|
+
api_client.deactivate_throttler!
|
199
|
+
enqueue_delayed_files_updates
|
188
200
|
end
|
189
201
|
|
190
202
|
def download_theme!(delete: true, &block)
|
@@ -242,38 +254,57 @@ module ShopifyCLI
|
|
242
254
|
wait_for_backoff!
|
243
255
|
@ctx.debug(operation.to_s)
|
244
256
|
|
245
|
-
|
257
|
+
send(operation.method, operation.file) do |response|
|
258
|
+
raise response if response.is_a?(StandardError)
|
259
|
+
|
260
|
+
file = operation.file
|
246
261
|
|
247
|
-
|
262
|
+
if file.warnings.any?
|
263
|
+
warning_message =
|
264
|
+
operation.as_synced_message(color: :yellow) +
|
265
|
+
UnsupportedScriptWarning.new(@ctx, file).to_s
|
266
|
+
@error_reporter.report(warning_message)
|
267
|
+
else
|
268
|
+
@standard_reporter.report(operation.as_synced_message)
|
269
|
+
end
|
248
270
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
271
|
+
# Check if the API told us we're near the rate limit
|
272
|
+
if !backingoff? && (limit = response["x-shopify-shop-api-call-limit"])
|
273
|
+
used, total = limit.split("/").map(&:to_i)
|
274
|
+
backoff_if_near_limit!(used, total)
|
275
|
+
end
|
276
|
+
rescue StandardError => error
|
277
|
+
handle_operation_error(operation, error)
|
278
|
+
ensure
|
279
|
+
@pending_mutex.synchronize do
|
280
|
+
# Avoid abrupt jumps in the progress bar
|
281
|
+
wait(0.05)
|
282
|
+
@pending.delete(operation)
|
283
|
+
end
|
253
284
|
end
|
254
|
-
rescue
|
255
|
-
|
256
|
-
report_error(operation, error_suffix)
|
257
|
-
ensure
|
258
|
-
@pending.delete(operation)
|
285
|
+
rescue StandardError => error
|
286
|
+
handle_operation_error(operation, error)
|
259
287
|
end
|
260
288
|
|
261
289
|
def update(file)
|
262
290
|
asset = { key: file.relative_path }
|
291
|
+
|
263
292
|
if file.text?
|
264
293
|
asset[:value] = file.read
|
265
294
|
else
|
266
295
|
asset[:attachment] = Base64.encode64(file.read)
|
267
296
|
end
|
268
297
|
|
269
|
-
|
270
|
-
|
271
|
-
body: JSON.generate(asset: asset)
|
272
|
-
)
|
298
|
+
path = "themes/#{@theme.id}/assets.json"
|
299
|
+
req_body = JSON.generate(asset: asset)
|
273
300
|
|
274
|
-
|
301
|
+
api_client.put(path: path, body: req_body) do |_status, resp_body, response|
|
302
|
+
update_checksums(resp_body)
|
303
|
+
|
304
|
+
file.warnings = resp_body.dig("asset", "warnings")
|
275
305
|
|
276
|
-
|
306
|
+
yield(response) if block_given?
|
307
|
+
end
|
277
308
|
end
|
278
309
|
|
279
310
|
def get(file)
|
@@ -291,7 +322,7 @@ module ShopifyCLI
|
|
291
322
|
file.write(body.dig("asset", "value"))
|
292
323
|
end
|
293
324
|
|
294
|
-
response
|
325
|
+
yield(response)
|
295
326
|
end
|
296
327
|
|
297
328
|
def delete(file)
|
@@ -302,7 +333,7 @@ module ShopifyCLI
|
|
302
333
|
})
|
303
334
|
)
|
304
335
|
|
305
|
-
response
|
336
|
+
yield(response)
|
306
337
|
end
|
307
338
|
|
308
339
|
def union_merge(file)
|
@@ -311,11 +342,11 @@ module ShopifyCLI
|
|
311
342
|
query: URI.encode_www_form("asset[key]" => file.relative_path),
|
312
343
|
)
|
313
344
|
|
314
|
-
return response unless file.text?
|
345
|
+
return yield(response) unless file.text?
|
315
346
|
|
316
347
|
remote_content = body.dig("asset", "value")
|
317
348
|
|
318
|
-
return response if remote_content.nil?
|
349
|
+
return yield(response) if remote_content.nil?
|
319
350
|
|
320
351
|
content = Merger.union_merge(file, remote_content)
|
321
352
|
|
@@ -323,7 +354,7 @@ module ShopifyCLI
|
|
323
354
|
|
324
355
|
enqueue(:update, file)
|
325
356
|
|
326
|
-
response
|
357
|
+
yield(response)
|
327
358
|
end
|
328
359
|
|
329
360
|
def update_checksums(api_response)
|
@@ -336,7 +367,11 @@ module ShopifyCLI
|
|
336
367
|
end
|
337
368
|
|
338
369
|
def parse_api_errors(exception)
|
339
|
-
parsed_body =
|
370
|
+
parsed_body = if exception&.response&.is_a?(Hash)
|
371
|
+
exception&.response&.[](:body)
|
372
|
+
else
|
373
|
+
JSON.parse(exception&.response&.body)
|
374
|
+
end
|
340
375
|
message = parsed_body.dig("errors", "asset") || parsed_body["message"] || exception.message
|
341
376
|
# Truncate to first lines
|
342
377
|
[message].flatten.map { |m| m.split("\n", 2).first }
|
@@ -347,7 +382,7 @@ module ShopifyCLI
|
|
347
382
|
def backoff_if_near_limit!(used, limit)
|
348
383
|
if used > limit - @threads.size
|
349
384
|
@ctx.debug("Near API call limit, waiting 2 sec…")
|
350
|
-
@backoff_mutex.synchronize {
|
385
|
+
@backoff_mutex.synchronize { wait(2) }
|
351
386
|
end
|
352
387
|
end
|
353
388
|
|
@@ -363,6 +398,15 @@ module ShopifyCLI
|
|
363
398
|
# Sleeping in the mutex in another thread. Wait for unlock
|
364
399
|
@backoff_mutex.synchronize {} if backingoff?
|
365
400
|
end
|
401
|
+
|
402
|
+
def handle_operation_error(operation, error)
|
403
|
+
error_suffix = ":\n " + parse_api_errors(error).join("\n ")
|
404
|
+
report_error(operation, error_suffix)
|
405
|
+
end
|
406
|
+
|
407
|
+
def wait(duration)
|
408
|
+
sleep(duration)
|
409
|
+
end
|
366
410
|
end
|
367
411
|
end
|
368
412
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ShopifyCLI
|
2
4
|
module Theme
|
3
5
|
class ThemeAdminAPI
|
@@ -10,35 +12,36 @@ module ShopifyCLI
|
|
10
12
|
@shop = shop || get_shop_or_abort
|
11
13
|
end
|
12
14
|
|
13
|
-
def get(path:, **args)
|
14
|
-
rest_request(method: "GET", path: path, **args)
|
15
|
+
def get(path:, **args, &block)
|
16
|
+
rest_request(method: "GET", path: path, **args, &block)
|
15
17
|
end
|
16
18
|
|
17
|
-
def put(path:, **args)
|
18
|
-
rest_request(method: "PUT", path: path, **args)
|
19
|
+
def put(path:, **args, &block)
|
20
|
+
rest_request(method: "PUT", path: path, **args, &block)
|
19
21
|
end
|
20
22
|
|
21
|
-
def post(path:, **args)
|
22
|
-
rest_request(method: "POST", path: path, **args)
|
23
|
+
def post(path:, **args, &block)
|
24
|
+
rest_request(method: "POST", path: path, **args, &block)
|
23
25
|
end
|
24
26
|
|
25
|
-
def delete(path:, **args)
|
26
|
-
rest_request(method: "DELETE", path: path, **args)
|
27
|
+
def delete(path:, **args, &block)
|
28
|
+
rest_request(method: "DELETE", path: path, **args, &block)
|
27
29
|
end
|
28
30
|
|
29
31
|
def get_shop_or_abort # rubocop:disable Naming/AccessorMethodName
|
30
32
|
ShopifyCLI::AdminAPI.get_shop_or_abort(@ctx)
|
31
33
|
end
|
32
34
|
|
33
|
-
private
|
34
|
-
|
35
35
|
def rest_request(**args)
|
36
|
-
ShopifyCLI::AdminAPI.rest_request(
|
36
|
+
status, body, response = ShopifyCLI::AdminAPI.rest_request(
|
37
37
|
@ctx,
|
38
38
|
shop: @shop,
|
39
39
|
api_version: API_VERSION,
|
40
40
|
**args.compact
|
41
41
|
)
|
42
|
+
return yield(status, body, response) if block_given?
|
43
|
+
|
44
|
+
[status, body, response]
|
42
45
|
rescue ShopifyCLI::API::APIRequestForbiddenError,
|
43
46
|
ShopifyCLI::API::APIRequestUnauthorizedError => error
|
44
47
|
|
@@ -62,6 +65,8 @@ module ShopifyCLI
|
|
62
65
|
raise error
|
63
66
|
end
|
64
67
|
|
68
|
+
private
|
69
|
+
|
65
70
|
def permission_error
|
66
71
|
ensure_user_error = @ctx.message("theme.ensure_user_error", shop)
|
67
72
|
ensure_user_try_this = @ctx.message("theme.ensure_user_try_this")
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "bulk_job"
|
4
|
+
require "shopify_cli/thread_pool"
|
5
|
+
|
6
|
+
module ShopifyCLI
|
7
|
+
module Theme
|
8
|
+
class ThemeAdminAPIThrottler
|
9
|
+
class Bulk
|
10
|
+
MAX_BULK_BYTESIZE = 10_485_760 # 10MB
|
11
|
+
MAX_BULK_FILES = 20 # files
|
12
|
+
QUEUE_TIMEOUT = 0.2 # 200ms
|
13
|
+
|
14
|
+
attr_accessor :admin_api
|
15
|
+
|
16
|
+
def initialize(ctx, admin_api, pool_size: 20)
|
17
|
+
@ctx = ctx
|
18
|
+
@admin_api = admin_api
|
19
|
+
@latest_enqueued_at = now
|
20
|
+
|
21
|
+
@thread_pool = ShopifyCLI::ThreadPool.new(pool_size: pool_size)
|
22
|
+
|
23
|
+
pool_size.times do
|
24
|
+
@thread_pool.schedule(
|
25
|
+
BulkJob.new(ctx, self)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
@put_requests = []
|
30
|
+
@mut = Mutex.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def enqueue(put_request)
|
34
|
+
@mut.synchronize do
|
35
|
+
@latest_enqueued_at = now
|
36
|
+
@put_requests << put_request
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def shutdown
|
41
|
+
wait_put_requests
|
42
|
+
@thread_pool.shutdown
|
43
|
+
end
|
44
|
+
|
45
|
+
def consume_put_requests
|
46
|
+
to_batch = []
|
47
|
+
to_batch_size_bytes = 0
|
48
|
+
@mut.synchronize do
|
49
|
+
# sort requests to perform less retries at the `bulk_job` level
|
50
|
+
@put_requests.sort_by! { |r| r.liquid? ? 0 : 1 }
|
51
|
+
|
52
|
+
is_ready = false
|
53
|
+
until is_ready || @put_requests.empty?
|
54
|
+
request = @put_requests.first
|
55
|
+
if to_batch.empty? && request.size > MAX_BULK_BYTESIZE
|
56
|
+
is_ready = true
|
57
|
+
to_batch << request
|
58
|
+
to_batch_size_bytes += request.size
|
59
|
+
@put_requests.shift
|
60
|
+
elsif to_batch.size + 1 > MAX_BULK_FILES || to_batch_size_bytes + request.size > MAX_BULK_BYTESIZE
|
61
|
+
is_ready = true
|
62
|
+
else
|
63
|
+
to_batch << request
|
64
|
+
to_batch_size_bytes += request.size
|
65
|
+
@put_requests.shift
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
[to_batch, to_batch_size_bytes]
|
70
|
+
end
|
71
|
+
|
72
|
+
def ready?
|
73
|
+
queue_timeout? || bulk_size >= MAX_BULK_FILES || bulk_bytesize >= MAX_BULK_BYTESIZE
|
74
|
+
end
|
75
|
+
|
76
|
+
def bulk_bytesize
|
77
|
+
@put_requests.map(&:size).reduce(:+).to_i
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def bulk_size
|
83
|
+
@put_requests.size
|
84
|
+
end
|
85
|
+
|
86
|
+
def queue_timeout?
|
87
|
+
return false if bulk_size.zero?
|
88
|
+
elapsed_time = now - @latest_enqueued_at
|
89
|
+
elapsed_time > QUEUE_TIMEOUT
|
90
|
+
end
|
91
|
+
|
92
|
+
def wait_put_requests
|
93
|
+
sleep(0.2) until @put_requests.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
def now
|
97
|
+
Time.now.to_f
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shopify_cli/thread_pool/job"
|
4
|
+
require_relative "request_parser"
|
5
|
+
require_relative "response_parser"
|
6
|
+
require_relative "errors"
|
7
|
+
|
8
|
+
module ShopifyCLI
|
9
|
+
module Theme
|
10
|
+
class ThemeAdminAPIThrottler
|
11
|
+
class BulkJob < ShopifyCLI::ThreadPool::Job
|
12
|
+
JOB_TIMEOUT = 0.2 # 200ms
|
13
|
+
MAX_RETRIES = 5
|
14
|
+
|
15
|
+
attr_reader :bulk
|
16
|
+
|
17
|
+
def initialize(ctx, bulk)
|
18
|
+
super(JOB_TIMEOUT)
|
19
|
+
@ctx = ctx
|
20
|
+
@bulk = bulk
|
21
|
+
|
22
|
+
# Mutex used to coordinate changes performed by the bulk item block
|
23
|
+
@block_mutex = Mutex.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform!
|
27
|
+
return unless bulk.ready?
|
28
|
+
put_requests, bulk_size = bulk.consume_put_requests
|
29
|
+
return if put_requests.empty?
|
30
|
+
|
31
|
+
@ctx.debug("[BulkJob] size: #{put_requests.size}, bytesize: #{bulk_size}")
|
32
|
+
bulk_status, bulk_body, response = rest_request(put_requests)
|
33
|
+
|
34
|
+
if bulk_status == 207
|
35
|
+
responses(bulk_body).each_with_index do |tuple, index|
|
36
|
+
status, body = tuple
|
37
|
+
put_request = put_requests[index]
|
38
|
+
if status == 200 || put_request.retries >= MAX_RETRIES
|
39
|
+
@block_mutex.synchronize do
|
40
|
+
if status == 200
|
41
|
+
@ctx.debug("[BulkJob] asset saved: #{put_request.key}")
|
42
|
+
put_request.block.call(status, body, response)
|
43
|
+
else
|
44
|
+
@ctx.debug("[BulkJob] asset continuing with error: #{put_request.key}")
|
45
|
+
err = AssetUploadError.new(body, response: { body: body })
|
46
|
+
put_request.block.call(status, {}, err)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
else
|
50
|
+
@ctx.debug("[BulkJob] asset error: #{put_request.key}")
|
51
|
+
@block_mutex.synchronize do
|
52
|
+
put_request.retries += 1
|
53
|
+
bulk.enqueue(put_request)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
else
|
58
|
+
@ctx.puts(@ctx.message("theme.stable_flag_suggestion"))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def rest_request(put_requests)
|
65
|
+
request = RequestParser.new(put_requests).parse
|
66
|
+
bulk.admin_api.rest_request(**request)
|
67
|
+
end
|
68
|
+
|
69
|
+
def responses(response_body)
|
70
|
+
ResponseParser.new(response_body).parse
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shopify_cli/thread_pool/job"
|
4
|
+
require_relative "request_parser"
|
5
|
+
require_relative "response_parser"
|
6
|
+
|
7
|
+
module ShopifyCLI
|
8
|
+
module Theme
|
9
|
+
class ThemeAdminAPIThrottler
|
10
|
+
class PutRequest
|
11
|
+
attr_reader :method, :body, :path, :block
|
12
|
+
attr_accessor :retries
|
13
|
+
|
14
|
+
def initialize(path, body, &block)
|
15
|
+
@method = "PUT"
|
16
|
+
@path = path
|
17
|
+
@body = body
|
18
|
+
@block = block
|
19
|
+
@retries = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
method: method,
|
25
|
+
path: path,
|
26
|
+
body: body,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"#{key}, retries: #{retries}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def liquid?
|
35
|
+
key.end_with?(".liquid")
|
36
|
+
end
|
37
|
+
|
38
|
+
def key
|
39
|
+
@key ||= JSON.parse(body)["asset"]["key"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def bulk_path
|
43
|
+
path.gsub(/.json$/, "/bulk.json")
|
44
|
+
end
|
45
|
+
|
46
|
+
def size
|
47
|
+
@size ||= body.bytesize
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyCLI
|
4
|
+
module Theme
|
5
|
+
class ThemeAdminAPIThrottler
|
6
|
+
class RequestParser
|
7
|
+
def initialize(requests)
|
8
|
+
@requests = requests
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse
|
12
|
+
{
|
13
|
+
path: path,
|
14
|
+
method: method,
|
15
|
+
body: JSON.generate({ assets: assets }),
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def method
|
22
|
+
@requests.sample.method
|
23
|
+
end
|
24
|
+
|
25
|
+
def path
|
26
|
+
@requests.sample.bulk_path
|
27
|
+
end
|
28
|
+
|
29
|
+
def assets
|
30
|
+
@requests.map do |request|
|
31
|
+
body = JSON.parse(request.body)
|
32
|
+
body = body.is_a?(Hash) ? body : JSON.parse(body)
|
33
|
+
body["asset"]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyCLI
|
4
|
+
module Theme
|
5
|
+
class ThemeAdminAPIThrottler
|
6
|
+
class ResponseParser
|
7
|
+
def initialize(response_body)
|
8
|
+
@response_body = response_body
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse
|
12
|
+
result = []
|
13
|
+
@response_body["results"]&.each do |resp|
|
14
|
+
result << [resp["code"], resp["body"]]
|
15
|
+
end
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|