Class: FluidCLI::Theme::Syncer

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/fluid_cli/theme/syncer.rb,
lib/fluid_cli/theme/syncer/merger.rb,
lib/fluid_cli/theme/syncer/uploader.rb,
lib/fluid_cli/theme/syncer/checksums.rb,
lib/fluid_cli/theme/syncer/operation.rb,
lib/fluid_cli/theme/syncer/downloader.rb,
lib/fluid_cli/theme/syncer/error_reporter.rb,
lib/fluid_cli/theme/syncer/standard_reporter.rb,
lib/fluid_cli/theme/syncer/unsupported_script_warning.rb,
lib/fluid_cli/theme/syncer/uploader/forms/apply_to_all.rb,
lib/fluid_cli/theme/syncer/uploader/json_delete_handler.rb,
lib/fluid_cli/theme/syncer/uploader/json_update_handler.rb,
lib/fluid_cli/theme/syncer/uploader/forms/apply_to_all_form.rb,
lib/fluid_cli/theme/syncer/uploader/forms/base_strategy_form.rb,
lib/fluid_cli/theme/syncer/uploader/forms/select_delete_strategy.rb,
lib/fluid_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,
  :get_asset,   # - Downloads a binary asset from a given URL
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ctx, theme:, overwrite_json: true, force: false) ⇒ Syncer

Returns a new instance of Syncer.



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

def initialize(ctx, theme:, overwrite_json: true, force: false)
  @ctx = ctx
  @theme = theme
  @overwrite_json = overwrite_json
  @force = force
  @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 = []

  # Initialize `api_client` on main thread
  @api_client = FluidCLI::API.new(ctx)
end

Instance Attribute Details

#api_clientObject (readonly)

Returns the value of attribute api_client.



30
31
32
# File 'lib/fluid_cli/theme/syncer.rb', line 30

def api_client
  @api_client
end

#checksumsObject (readonly)

Returns the value of attribute checksums.



30
31
32
# File 'lib/fluid_cli/theme/syncer.rb', line 30

def checksums
  @checksums
end

#ctxObject (readonly)

Returns the value of attribute ctx.



30
31
32
# File 'lib/fluid_cli/theme/syncer.rb', line 30

def ctx
  @ctx
end

#error_checksumsObject (readonly)

Returns the value of attribute error_checksums.



30
31
32
# File 'lib/fluid_cli/theme/syncer.rb', line 30

def error_checksums
  @error_checksums
end

#pendingObject (readonly)

Returns the value of attribute pending.



30
31
32
# File 'lib/fluid_cli/theme/syncer.rb', line 30

def pending
  @pending
end

#standard_reporterObject (readonly)

Returns the value of attribute standard_reporter.



30
31
32
# File 'lib/fluid_cli/theme/syncer.rb', line 30

def standard_reporter
  @standard_reporter
end

#themeObject (readonly)

Returns the value of attribute theme.



30
31
32
# File 'lib/fluid_cli/theme/syncer.rb', line 30

def theme
  @theme
end

Instance Method Details

#broken_file?(file) ⇒ Boolean

Returns:

  • (Boolean)


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

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

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



174
175
176
177
# File 'lib/fluid_cli/theme/syncer.rb', line 174

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

#empty?Boolean

Returns:

  • (Boolean)


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

def empty?
  @pending.empty?
end

#enqueue_deletes(files) ⇒ Object



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

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

#enqueue_get(files) ⇒ Object



74
75
76
# File 'lib/fluid_cli/theme/syncer.rb', line 74

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

#enqueue_union_merges(files) ⇒ Object



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

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

#enqueue_updates(files) ⇒ Object



70
71
72
# File 'lib/fluid_cli/theme/syncer.rb', line 70

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

#enqueueable?(operation) ⇒ Boolean

Returns:

  • (Boolean)


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

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

  # Already enqueued or ignored
  return false if @pending.include?(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



120
121
122
123
124
125
# File 'lib/fluid_cli/theme/syncer.rb', line 120

def fetch_checksums!
  _status, response = api_client.get(
    path: "application_themes/#{theme.id}/resources"
  )
  update_checksums(response["application_theme_resources"])
end

#get_theme_resources!Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/fluid_cli/theme/syncer.rb', line 128

def get_theme_resources!
  _status, response = api_client.get(
    path: "application_themes/#{theme.id}/resources"
  )
  update_checksums(response["application_theme_resources"])

  response["application_theme_resources"].each do |resource|
    file = theme[resource["key"]]
    next unless file

    if resource["resource_type"] == "FileResource" && !file.text?
      enqueue(:get_asset, file, { url: resource["url"] })
    else
      file.write(resource["content"])
    end
  end
end

#handle_operation_error(operation, error) ⇒ Object



197
198
199
200
# File 'lib/fluid_cli/theme/syncer.rb', line 197

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



62
63
64
# File 'lib/fluid_cli/theme/syncer.rb', line 62

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

#overwrite_json?Boolean

Returns:

  • (Boolean)


202
203
204
# File 'lib/fluid_cli/theme/syncer.rb', line 202

def overwrite_json?
  theme_created_at_runtime? || @overwrite_json
end

#parse_api_errors(file, exception) ⇒ Object



223
224
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
# File 'lib/fluid_cli/theme/syncer.rb', line 223

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
    errors = parsed_body.dig("errors") # either nil or another type
    errors = errors.dig("asset") if errors&.is_a?(Hash)

    ["#{parsed_body['error_message']}\n#{errors}"]
  elsif exception.respond_to?(:message)
    [exception.message]
  else
    ["An unknown error occurred while syncing the asset #{file}"]
  end
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



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

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

#remote_file?(file) ⇒ Boolean

Returns:

  • (Boolean)


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

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

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



216
217
218
219
220
221
# File 'lib/fluid_cli/theme/syncer.rb', line 216

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

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

#shutdownObject



146
147
148
149
150
# File 'lib/fluid_cli/theme/syncer.rb', line 146

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

#sizeObject



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

def size
  @pending.size
end

#start_threads(count = 2) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/fluid_cli/theme/syncer.rb', line 152

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



66
67
68
# File 'lib/fluid_cli/theme/syncer.rb', line 66

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

#update_checksums(api_response) ⇒ Object



206
207
208
209
210
211
212
213
214
# File 'lib/fluid_cli/theme/syncer.rb', line 206

def update_checksums(api_response)
  api_response.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



169
170
171
172
# File 'lib/fluid_cli/theme/syncer.rb', line 169

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)


106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/fluid_cli/theme/syncer.rb', line 106

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