shopify-cli 2.19.0 → 2.20.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|