Class: ShopifyCLI::Theme::Syncer

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
BackoffHelper, IgnoreHelper
Defined in:
lib/shopify_cli/theme/syncer.rb,
lib/shopify_cli/theme/syncer/merger.rb,
lib/shopify_cli/theme/syncer/uploader.rb,
lib/shopify_cli/theme/syncer/checksums.rb,
lib/shopify_cli/theme/syncer/operation.rb,
lib/shopify_cli/theme/syncer/downloader.rb,
lib/shopify_cli/theme/syncer/uploader/bulk.rb,
lib/shopify_cli/theme/syncer/error_reporter.rb,
lib/shopify_cli/theme/syncer/standard_reporter.rb,
lib/shopify_cli/theme/syncer/uploader/bulk_job.rb,
lib/shopify_cli/theme/syncer/uploader/bulk_item.rb,
lib/shopify_cli/theme/syncer/uploader/bulk_request.rb,
lib/shopify_cli/theme/syncer/unsupported_script_warning.rb,
lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all.rb,
lib/shopify_cli/theme/syncer/uploader/json_delete_handler.rb,
lib/shopify_cli/theme/syncer/uploader/json_update_handler.rb,
lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all_form.rb,
lib/shopify_cli/theme/syncer/uploader/forms/base_strategy_form.rb,
lib/shopify_cli/theme/syncer/uploader/forms/select_delete_strategy.rb,
lib/shopify_cli/theme/syncer/uploader/forms/select_update_strategy.rb

Defined Under Namespace

Classes: Checksums, Downloader, ErrorReporter, Merger, Operation, StandardReporter, UnsupportedScriptWarning, Uploader

Constant Summary collapse

QUEUEABLE_METHODS =
[
  :get,         # - Updates the local file with the remote file content
  :update,      # - Updates the remote file with the local file content
  :delete,      # - Deletes the remote file
  :union_merge, # - Union merges the local file content with the remote file content
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from BackoffHelper

#backingoff?, #backoff!, #backoff_if_near_limit!, #backoff_mutex, #initialize_backoff_helper!, #wait_for_backoff!

Methods included from IgnoreHelper

#ignore_file?, #ignore_operation?, #ignore_path?

Constructor Details

#initialize(ctx, theme:, include_filter: nil, ignore_filter: nil, overwrite_json: true, stable: false) ⇒ Syncer

Returns a new instance of Syncer.



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
# File 'lib/shopify_cli/theme/syncer.rb', line 41

def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil, overwrite_json: true, stable: false)
  @ctx = ctx
  @theme = theme
  @include_filter = include_filter
  @ignore_filter = ignore_filter
  @overwrite_json = overwrite_json
  @error_reporter = ErrorReporter.new(ctx)
  @standard_reporter = StandardReporter.new(ctx)
  @reporters = [@error_reporter, @standard_reporter]

  # Queue of `Operation`s waiting to be picked up from a thread for processing.
  @queue = Queue.new

  # `Operation`s will be removed from this Array completed.
  @pending = []

  # Thread making the API requests.
  @threads = []

  # Latest theme assets checksums. Updated on each upload.
  @checksums = Checksums.new(theme)

  # Checksums of assets with errors.
  @error_checksums = []

  # Do not use the throttler when --stable is passed or a Theme Access password is set
  # (Theme Access API is not compatible yet with bulks)
  @bulk_updates_activated = !stable && !Environment.theme_access_password?

  # Initialize `api_client` on main thread
  @api_client = ThemeAdminAPI.new(ctx, theme.shop)

  # Initialize backoff helper on main thread to pause all threads when the
  # requests are reaching API rate limits.
  initialize_backoff_helper!
end

Instance Attribute Details

#api_clientObject (readonly)

Returns the value of attribute api_client.



36
37
38
# File 'lib/shopify_cli/theme/syncer.rb', line 36

def api_client
  @api_client
end

#checksumsObject (readonly)

Returns the value of attribute checksums.



36
37
38
# File 'lib/shopify_cli/theme/syncer.rb', line 36

def checksums
  @checksums
end

#ctxObject (readonly)

Returns the value of attribute ctx.



36
37
38
# File 'lib/shopify_cli/theme/syncer.rb', line 36

def ctx
  @ctx
end

#error_checksumsObject (readonly)

Returns the value of attribute error_checksums.



36
37
38
# File 'lib/shopify_cli/theme/syncer.rb', line 36

def error_checksums
  @error_checksums
end

#ignore_filterObject

Returns the value of attribute ignore_filter.



37
38
39
# File 'lib/shopify_cli/theme/syncer.rb', line 37

def ignore_filter
  @ignore_filter
end

#include_filterObject

Returns the value of attribute include_filter.



37
38
39
# File 'lib/shopify_cli/theme/syncer.rb', line 37

def include_filter
  @include_filter
end

#pendingObject (readonly)

Returns the value of attribute pending.



36
37
38
# File 'lib/shopify_cli/theme/syncer.rb', line 36

def pending
  @pending
end

#standard_reporterObject (readonly)

Returns the value of attribute standard_reporter.



36
37
38
# File 'lib/shopify_cli/theme/syncer.rb', line 36

def standard_reporter
  @standard_reporter
end

#themeObject (readonly)

Returns the value of attribute theme.



36
37
38
# File 'lib/shopify_cli/theme/syncer.rb', line 36

def theme
  @theme
end

Instance Method Details

#broken_file?(file) ⇒ Boolean

Returns:

  • (Boolean)


118
119
120
# File 'lib/shopify_cli/theme/syncer.rb', line 118

def broken_file?(file)
  error_checksums.include?(checksums[file.relative_path])
end

#bulk_updates_activated?Boolean

Returns:

  • (Boolean)


176
177
178
# File 'lib/shopify_cli/theme/syncer.rb', line 176

def bulk_updates_activated?
  @bulk_updates_activated
end

#download_theme!(delete: true, &block) ⇒ Object



171
172
173
174
# File 'lib/shopify_cli/theme/syncer.rb', line 171

def download_theme!(delete: true, &block)
  downloader = Downloader.new(self, delete, &block)
  downloader.download!
end

#empty?Boolean

Returns:

  • (Boolean)


106
107
108
# File 'lib/shopify_cli/theme/syncer.rb', line 106

def empty?
  @pending.empty?
end

#enqueue_deletes(files) ⇒ Object



94
95
96
# File 'lib/shopify_cli/theme/syncer.rb', line 94

def enqueue_deletes(files)
  files.each { |file| enqueue(:delete, file) }
end

#enqueue_get(files) ⇒ Object



90
91
92
# File 'lib/shopify_cli/theme/syncer.rb', line 90

def enqueue_get(files)
  files.each { |file| enqueue(:get, file) }
end

#enqueue_union_merges(files) ⇒ Object



98
99
100
# File 'lib/shopify_cli/theme/syncer.rb', line 98

def enqueue_union_merges(files)
  files.each { |file| enqueue(:union_merge, file) }
end

#enqueue_updates(files) ⇒ Object



86
87
88
# File 'lib/shopify_cli/theme/syncer.rb', line 86

def enqueue_updates(files)
  files.each { |file| enqueue(:update, file) }
end

#enqueueable?(operation) ⇒ Boolean

Returns:

  • (Boolean)


180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/shopify_cli/theme/syncer.rb', line 180

def enqueueable?(operation)
  file = operation.file
  method = operation.method

  # Already enqueued or ignored
  return false if @pending.include?(operation) || ignore_operation?(operation)

  if [:update, :get].include?(method) && file.exist?
    # File is fixed (and it has been never updated)
    if !!@error_checksums.delete(file.checksum)
      @standard_reporter.report(operation.as_fix_message)
    end

    return checksums.file_has_changed?(file)
  end

  true
end

#fetch_checksums!Object



136
137
138
139
140
141
# File 'lib/shopify_cli/theme/syncer.rb', line 136

def fetch_checksums!
  _status, response = api_client.get(
    path: "themes/#{@theme.id}/assets.json",
  )
  update_checksums(response)
end

#handle_operation_error(operation, error) ⇒ Object



199
200
201
202
# File 'lib/shopify_cli/theme/syncer.rb', line 199

def handle_operation_error(operation, error)
  error_suffix = ":\n  " + parse_api_errors(operation.file, error).join("\n  ")
  report_error(operation, error_suffix)
end

#lock_io!Object



78
79
80
# File 'lib/shopify_cli/theme/syncer.rb', line 78

def lock_io!
  @reporters.each(&:disable!)
end

#overwrite_json?Boolean

Returns:

  • (Boolean)


204
205
206
# File 'lib/shopify_cli/theme/syncer.rb', line 204

def overwrite_json?
  theme_created_at_runtime? || @overwrite_json
end

#parse_api_errors(file, exception) ⇒ Object



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/shopify_cli/theme/syncer.rb', line 225

def parse_api_errors(file, exception)
  parsed_body = {}

  if exception.respond_to?(:response)
    response = exception.response

    parsed_body = if response&.is_a?(Hash)
      response&.[](:body)
    else
      JSON.parse(response&.body)
    end
  end

  errors = parsed_body.dig("errors") # either nil or another type
  errors = errors.dig("asset") if errors&.is_a?(Hash)

  message = errors || parsed_body["message"] || exception.message
  # Truncate to first lines
  [message].flatten.map { |m| m.split("\n", 2).first }
rescue JSON::ParserError
  [exception.message]
rescue StandardError => e
  cause = "(cause: #{e.message})"
  backtrace = e.backtrace.join("\n")
  ["The asset #{file} could not be synced #{cause} #{backtrace}"]
end

#pending_updatesObject



110
111
112
# File 'lib/shopify_cli/theme/syncer.rb', line 110

def pending_updates
  @pending.select { |op| op.method == :update }.map(&:file)
end

#remote_file?(file) ⇒ Boolean

Returns:

  • (Boolean)


114
115
116
# File 'lib/shopify_cli/theme/syncer.rb', line 114

def remote_file?(file)
  checksums.has?(file)
end

#report_file_error(file, error_message = "") ⇒ Object



218
219
220
221
222
223
# File 'lib/shopify_cli/theme/syncer.rb', line 218

def report_file_error(file, error_message = "")
  path = file.relative_path

  @error_checksums << checksums[path]
  @error_reporter.report(error_message)
end

#shutdownObject



143
144
145
146
147
# File 'lib/shopify_cli/theme/syncer.rb', line 143

def shutdown
  @queue.close unless @queue.closed?
ensure
  @threads.each { |thread| thread.join if thread.alive? }
end

#sizeObject



102
103
104
# File 'lib/shopify_cli/theme/syncer.rb', line 102

def size
  @pending.size
end

#start_threads(count = 2) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/shopify_cli/theme/syncer.rb', line 149

def start_threads(count = 2)
  count.times do
    @threads << Thread.new do
      loop do
        operation = @queue.pop
        break if operation.nil? # shutdown was called

        perform(operation)
      rescue Exception => e # rubocop:disable Lint/RescueException
        error_suffix = ": #{e}"
        error_suffix += + "\n\t#{e.backtrace.join("\n\t")}" if @ctx.debug?
        report_error(operation, error_suffix)
      end
    end
  end
end

#unlock_io!Object



82
83
84
# File 'lib/shopify_cli/theme/syncer.rb', line 82

def unlock_io!
  @reporters.each(&:enable!)
end

#update_checksums(api_response) ⇒ Object



208
209
210
211
212
213
214
215
216
# File 'lib/shopify_cli/theme/syncer.rb', line 208

def update_checksums(api_response)
  api_response.values.flatten.each do |asset|
    next unless asset["key"]

    checksums[asset["key"]] = asset["checksum"]
  end

  checksums.reject_duplicated_checksums!
end

#upload_theme!(delay_low_priority_files: false, delete: true, &block) ⇒ Object



166
167
168
169
# File 'lib/shopify_cli/theme/syncer.rb', line 166

def upload_theme!(delay_low_priority_files: false, delete: true, &block)
  uploader = Uploader.new(self, delete, delay_low_priority_files, &block)
  uploader.upload!
end

#wait!Object

Raises:

  • (ThreadError)


122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/shopify_cli/theme/syncer.rb', line 122

def wait!
  raise ThreadError, "No syncer threads" if @threads.empty?

  total = size
  last_size = size
  until empty? || @queue.closed?
    if block_given? && last_size != size
      yield size, total
      last_size = size
    end
    Thread.pass
  end
end