# frozen_string_literal: true require "forwardable" require_relative "uploader/bulk_item" require_relative "uploader/bulk" require_relative "uploader/json_delete_handler" require_relative "uploader/json_update_handler" module ShopifyCLI module Theme class Syncer class Uploader extend Forwardable include JsonDeleteHandler include JsonUpdateHandler attr_reader :syncer def_delegators :syncer, # helpers :ctx, :api_client, :theme, :ignore_file?, :overwrite_json?, :bulk_updates_activated?, # enqueue :enqueue_deletes, :enqueue_get, :enqueue_union_merges, :enqueue_updates, # checksums :checksums, :update_checksums, :fetch_checksums!, :wait! def initialize(syncer, delete, delay_low_priority_files, &update_progress_bar_block) @syncer = syncer @delete = delete @delay_low_priority_files = delay_low_priority_files @update_progress_bar_block = update_progress_bar_block @progress_bar_mutex = Mutex.new end def upload! fetch_checksums! delete_files! if bulk_updates_activated? && overwrite_json? bulk_upload! else async_upload! end end def delete_files! return unless delete? files_present_remotely = checksums.keys files_present_locally = theme.theme_files.map(&:relative_path) json_files, other_files = (files_present_remotely - files_present_locally) .map { |file| theme[file] } .reject { |file| ignore_file?(file) } .partition(&:json?) enqueue_deletes(other_files) enqueue_json_deletes(json_files) end private def bulk_upload! update_progress_bar! enqueue_bulk_updates(liquid_files) enqueue_bulk_updates(json_files) enqueue_bulk_updates(config_files) if delay_low_priority_files? # Process lower-priority files (assets) in the background, as they # are served locally enqueue_updates(static_asset_files) else enqueue_bulk_updates(static_asset_files) end wait!(&@update_progress_bar_block) unless delay_low_priority_files? end def async_upload! enqueue_updates(liquid_files) enqueue_json_updates(json_files) enqueue_updates(config_files) # Wait upload of Liquid & JSON files, as they are rendered remotely wait!(&@update_progress_bar_block) if delay_low_priority_files? # Process lower-priority files (assets) in the background, as they # are served locally enqueue_updates(static_asset_files) wait!(&@update_progress_bar_block) unless delay_low_priority_files? end def enqueue_bulk_updates(files) retries = 0 pending_items = files.map { |file| bulk_item(file) } while pending_items.any? && retries < 4 bulk = Bulk.new(ctx, theme, api_client) files .map { |file| bulk_item(file) } .each { |request| bulk.enqueue(request) } bulk.shutdown retries += 1 pending_items = bulk.remaining_items end return unless pending_items.any? # Remaining items are handled in the background when the bulk timeout # is exceeded pending_items.size.times { update_progress_bar! } syncer.enqueue_updates(pending_items.map(&:file)) syncer.wait! end def bulk_item(file) BulkItem.new(file) do |_s, body, response| if response.is_a?(StandardError) report(file, response) else update_checksums(body) end ensure update_progress_bar! end end def delete? @delete end def delay_low_priority_files? @delay_low_priority_files end # Files def number_of_bulk_items @number_of_files ||= [ json_files.size, liquid_files.size, config_files.size, delay_low_priority_files? ? 0 : static_asset_files.size, ].reduce(:+) end def json_files @json_files ||= uploadable(theme.json_files) - config_files end def liquid_files @liquid_files ||= uploadable(theme.liquid_files) end def static_asset_files @static_asset_files ||= uploadable(theme.static_asset_files) end def config_files @config_files ||= uploadable( [ theme["config/settings_schema.json"], theme["config/settings_data.json"], ] ) end def uploadable(files) files.select { |file| uploadable?(file) } end def uploadable?(file) return false unless file.exist? return false if ignore_file?(file) checksums.file_has_changed?(file) end # Handle prorgress bar def update_progress_bar! @pending_files ||= number_of_bulk_items @pending_files -= 1 # Avoid abrupt updates in the progress bar @progress_bar_mutex.synchronize do sleep(0.02) update_progress_bar(@pending_files, number_of_bulk_items) end end def update_progress_bar(size, total) @update_progress_bar_block.call(size, total) end # Handler errors def report(file, _error) error_message = "The asset #{file.relative_path} could not be uploaded.\n#{e.inspect}" syncer.report_file_error(file, error_message) end end end end end