From 64ba6d7ab52116c8254bfb8dca5b97d1bc907f3f Mon Sep 17 00:00:00 2001 From: supechicken Date: Sun, 14 Aug 2022 22:53:36 +0800 Subject: [PATCH] lib/downloader: New progress bar style, disappear after download complete (#7270) * lib/downloader: New progress bar design, with more info * Reduce progress bar update time * Clear progress bar when success only * Update progress_bar.rb * Update downloader.rb --- lib/downloader.rb | 101 ++++++++++++++++++++---------------------- lib/progress_bar.rb | 104 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 54 deletions(-) create mode 100644 lib/progress_bar.rb diff --git a/lib/downloader.rb b/lib/downloader.rb index 164aca92d..79b0e213a 100644 --- a/lib/downloader.rb +++ b/lib/downloader.rb @@ -2,13 +2,12 @@ require 'io/console' require 'digest/sha2' require_relative 'const' require_relative 'color' -require_relative 'convert_size' +require_relative 'progress_bar' begin - require 'net/http' require 'securerandom' - require 'uri' require 'resolv-replace' + require 'net/http' rescue RuntimeError => e # hide the error message and fallback to curl if securerandom raise an error if e.message == 'failed to get urandom' @@ -21,25 +20,6 @@ end require 'uri' -def setTermSize - # setTermSize: set progress bar size based on terminal width - # get terminal window size - begin - @termH, @termW = IO.console.winsize - rescue NoMethodError => e - unless @warned - STDERR.puts 'Non-interactive terminals may not be able to be queried for size.' - @warned = true - end - - @termH, @termW = [ 25, 80 ] - end - # space for progress bar after minus the reserved space for showing - # the file size and progress percentage - @progBarW = @termW - 17 - return true -end - def downloader (url, sha256sum, filename = File.basename(url), verbose = false) # downloader: wrapper for all Chromebrew downloaders (`net/http`,`curl`...) # Usage: downloader , , , @@ -49,44 +29,49 @@ def downloader (url, sha256sum, filename = File.basename(url), verbose = false) # : (Optional) Output path/filename # : (Optional) Verbose output # - setTermSize - # reset width settings after terminal resized - trap('WINCH') { setTermSize } - uri = URI(url) unless CREW_USE_CURL or !ENV['CREW_DOWNLOADER'].to_s.empty? case uri.scheme when 'http', 'https' # use net/http if the url protocol is http(s):// - http_downloader(url, filename, verbose) + http_downloader(uri, filename, verbose) when 'file' # use FileUtils to copy if it is a local file (the url protocol is file://) if File.exist?(uri.path) - return FileUtils.cp uri.path, filename + return FileUtils.cp(uri.path, filename) else abort "#{uri.path}: File not found :/".lightred end else # use external downloader (curl by default) if the url protocol is not http(s):// or file:// - external_downloader(url, filename, verbose) + external_downloader(uri, filename, verbose) end else # force using external downloader if either CREW_USE_CURL or ENV['CREW_DOWNLOADER'] is set - external_downloader(url, filename, verbose) + external_downloader(uri, filename, verbose) end # verify with given checksum - unless sha256sum =~ /^SKIP$/i or Digest::SHA256.hexdigest( File.read(filename) ) == sha256sum - abort 'Checksum mismatch :/ Try again?'.lightred + calc_sha256sum = Digest::SHA256.hexdigest( File.read(filename) ) + + unless sha256sum =~ /^SKIP$/i or calc_sha256sum == sha256sum + FileUtils.rm_f filename + + warn 'Checksum mismatch :/ Try again?'.lightred, <<~EOT + #{''} + Filename: #{filename.lightblue} + Expected checksum (SHA256): #{sha256sum.green} + Calculated checksum (SHA256): #{calc_sha256sum.red} + EOT + + exit 2 end end -def http_downloader (url, filename = File.basename(url), verbose = false) +def http_downloader (uri, filename = File.basename(url), verbose = false) # http_downloader: Downloader based on net/http library - uri = URI(url) - # open http connection Net::HTTP.start(uri.host, uri.port, { max_retries: CREW_DOWNLOADER_RETRY, @@ -109,7 +94,7 @@ def http_downloader (url, filename = File.basename(url), verbose = false) redirect_uri.scheme ||= uri.scheme redirect_uri.host ||= uri.host - return send(__method__, redirect_uri.to_s, filename, verbose) + return send(__method__, redirect_uri, filename, verbose) else abort "Download failed with error #{response.code}: #{response.msg}".lightred end @@ -118,43 +103,42 @@ def http_downloader (url, filename = File.basename(url), verbose = false) file_size = response['Content-Length'].to_f downloaded_size = 0.0 + # initialize progress bar + progress_bar = ProgressBar.new(file_size) + if verbose - puts <<~EOT + warn <<~EOT * Connected to #{uri.host} port #{uri.port} * HTTPS: #{uri.scheme.eql?('https')} * EOT # parse response's header to readable format - response.to_hash.each_pair {|k, v| puts "> #{k}: #{v}" } + response.to_hash.each_pair {|k, v| warn "> #{k}: #{v}" } - puts + warn "\n" end # read file chunks from server, write it to filesystem File.open(filename, 'wb') do |io| + progress_bar_thread = progress_bar.show # print progress bar + response.read_body do |chunk| - unless CREW_HIDE_PROGBAR - downloaded_size += chunk.size # record downloaded size, used for showing progress bar - if file_size.positive? - # calculate downloading progress percentage with the given file size - percentage = (downloaded_size / file_size) * 100 - # show progress bar, file size and progress percentage - printf "\r""[%-#{@progBarW}.#{@progBarW}s] %9.9s %3d%%", - '#' * ( @progBarW * (percentage / 100) ), - human_size(file_size), - percentage - end - end + downloaded_size += chunk.size # record downloaded size, used for showing progress bar + progress_bar.set_downloaded_size(downloaded_size) if file_size.positive? + io.write(chunk) # write to file end + ensure + # stop progress bar, wait for it to terminate + progress_bar.progress_bar_showing = false + progress_bar_thread.join end - puts end end end -def external_downloader (url, filename = File.basename(url), verbose = false) +def external_downloader (uri, filename = File.basename(url), verbose = false) # external_downloader: wrapper for external downloaders in CREW_DOWNLOADER (curl by default) # default curl cmdline, CREW_DOWNLOADER should be in this format also @@ -167,5 +151,14 @@ def external_downloader (url, filename = File.basename(url), verbose = false) # use CREW_DOWNLOADER if specified, use curl by default downloader_cmdline = CREW_DOWNLOADER || curl_cmdline - return system (downloader_cmdline % { verbose: verbose ? '--verbose' : '', retry: CREW_DOWNLOADER_RETRY, url: url, output: filename}), exception: true + return system( + format(downloader_cmdline, + { + verbose: verbose ? '--verbose' : '', + retry: CREW_DOWNLOADER_RETRY, + url: uri.to_s, + output: filename + } + ), exception: true + ) end diff --git a/lib/progress_bar.rb b/lib/progress_bar.rb new file mode 100644 index 000000000..eeb3f3d73 --- /dev/null +++ b/lib/progress_bar.rb @@ -0,0 +1,104 @@ +require 'io/console' +require_relative 'color' +require_relative 'convert_size' + +class ProgressBar + attr_accessor :progress_bar_showing + + def initialize (total_size) + # character used to fill the progress bar, one of the box-drawing character in unicode + @bar_char = "\u2501" + + # color scheme of progress bar, can be changed + # see color.rb for more available colors + @bar_front_color = :lightcyan + @bar_bg_color = :gray + + # all info blocks with space taken + @info_before_bar = { downloaded_size_in_str: 20 } + @info_after_bar = { percentage_in_str: 4, elapsed_time_in_str: 8 } + + @percentage = @downloaded = 0 + @total_size = total_size.to_f + + @total_size_in_str = human_size(@total_size) + + trap('WINCH') do + # reset width settings after terminal resized + # get terminal size, calculate the width of progress bar based on it + @terminal_h, @terminal_w = IO.console.winsize + + @bar_width = @terminal_w - + @info_before_bar.merge(@info_after_bar).values.sum - # space that all info blocks takes + ( @info_before_bar.merge(@info_after_bar).length * 2 ) # space for separator (whitespaces) between each info + + rescue NoMethodError => e + # fallback for non-interactive terminals + unless $non_interactive_term_warned + warn 'Non-interactive terminals may not be able to be queried for size.' + $non_interactive_term_warned = true + end + + @terminal_h, @terminal_w = [ 25, 80 ] + end + + Process.kill('WINCH', 0) # trigger the trap above + end + + def set_downloaded_size (downloaded_size) + if @start_time + @elapsed_time = (Time.now - @start_time).to_i + else + # record start time, used for calculating elapsed time + @start_time = Time.now + @elapsed_time = 0 + end + + @elapsed_time_in_str = Time.at(@elapsed_time).utc.strftime('%H:%M:%S') + + # calculate progress percentage, round to nearest 0.1 + @percentage = ( ( downloaded_size / @total_size ) * 100 ).round(1) + @percentage_in_str = "#{@percentage.to_i}%" + + # {downloaded size}/{total size} + @downloaded_size_in_str = "#{human_size(downloaded_size)}/#{@total_size_in_str}" + end + + def show + return Thread.new do + @progress_bar_showing = true + + print "\e[?25l" # hide cursor to prevent cursor flickering + + while @progress_bar_showing + sleep 0.15 # update progress bar after each 0.15 seconds + + completed_length = ( @bar_width * (@percentage / 100) ).to_i + uncompleted_length = @bar_width - completed_length + + # print info and progress bar + @info_before_bar.each_pair do |varName, width| + printf "%*.*s ", width, width, instance_variable_get("@#{varName}") + end + + # print progress bar with color code + print ( @bar_char * completed_length ).send(@bar_front_color), + ( @bar_char * uncompleted_length ).send(@bar_bg_color) + + @info_after_bar.each_pair do |varName, width| + printf " %*.*s", width, width, instance_variable_get("@#{varName}") + end + + # stop when 100% + if @percentage >= 100 + print "\e[2K\r" # clear previous line (progress bar) + break + else + print "\r" + end + end + ensure + print "\e[?25h" # restore cursor mode since we hide it before + end + end +end