mirror of
https://github.com/chromebrew/chromebrew.git
synced 2026-01-08 23:18:10 -05:00
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
This commit is contained in:
@@ -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 <url>, <sha256sum>, <filename::optional>, <verbose::optional>
|
||||
@@ -49,44 +29,49 @@ def downloader (url, sha256sum, filename = File.basename(url), verbose = false)
|
||||
# <filename>: (Optional) Output path/filename
|
||||
# <verbose>: (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
|
||||
|
||||
104
lib/progress_bar.rb
Normal file
104
lib/progress_bar.rb
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user