class VpsAdmin::CLI::StreamDownloader

Public Class Methods

download(*args) click to toggle source
# File lib/vpsadmin/cli/stream_downloader.rb, line 10
def self.download(*args)
  new(*args)
end
new(api, dl, io, progress: STDOUT, position: 0, max_rate: nil, checksum: true) click to toggle source
# File lib/vpsadmin/cli/stream_downloader.rb, line 14
def initialize(api, dl, io, progress: STDOUT, position: 0, max_rate: nil,
              checksum: true)
  downloaded = position
  uri = URI(dl.url)
  digest = Digest::SHA256.new
  dl_check = nil

  if position > 0 && checksum
    if progress
      pb = ProgressBar.create(
          title: 'Calculating checksum',
          total: position,
          format: '%E %t: [%B] %p%% %r MB/s',
          rate_scale: ->(rate) { (rate / 1024.0 / 1024.0).round(2) },
          throttle_rate: 0.2,
          output: progress,
      )
    end

    read = 0
    step = 1*1024*1024
    io.seek(0)

    while read < position
      data = io.read((read + step) > position ? position - read : step)
      read += data.size

      digest << data
      pb.progress = read if pb
    end

    pb.finish if pb
  end

  if progress
    self.format = '%t: [%B] %r kB/s'

    @pb = ProgressBar.create(
        title: 'Downloading',
        total: nil,
        format: @format,
        rate_scale: ->(rate) { (rate / 1024.0).round(2) },
        throttle_rate: 0.2,
        starting_at: downloaded,
        autofinish: false,
        output: progress,
    )
  end

  args = [uri.host] + Array.new(5, nil) + [{use_ssl: uri.scheme == 'https'}]

  Net::HTTP.start(*args) do |http|
    loop do
      begin
        dl_check = api.snapshot_download.show(dl.id)

        if @pb && (dl_check.ready || (dl_check.size && dl_check.size > 0))
          total = dl_check.size * 1024 * 1024
          @pb.total = @pb.progress > total ? @pb.progress : total

          @download_size = (dl_check.size / 1024.0).round(2)

          if dl_check.ready
            @download_ready = true
            self.format = "%E %t #{@download_size} GB: [%B] %p%% %r kB/s"

          else
            self.format = "%E %t ~#{@download_size} GB: [%B] %p%% %r kB/s"
          end
        end

      rescue HaveAPI::Client::ActionFailed => e
        # The SnapshotDownload object no longer exists, the transaction
        # responsible for its creation must have failed.
        stop
        raise DownloadError, 'The download has failed due to transaction failure'
      end

      headers = {}
      headers['Range'] = "bytes=#{downloaded}-" if downloaded > 0

      http.request_get(uri.path, headers) do |res|
        case res.code
        when '404'  # Not Found
          if downloaded > 0
            # This means that the transaction used for preparing the download
            # has failed, the file to download does not exist anymore, so fail.
            raise DownloadError, 'The download has failed, most likely transaction failure'

          else
            # The file is not available yet, this is normal, the transaction
            # may be queued and it can take some time before it is processed.
            pause(10)
            next
          end

        when '416'  # Range Not Satisfiable
          if downloaded > position
            # We have already managed to download something (at this run, if the trasfer
            # was resumed) and the server cannot provide more data yet. This can be
            # because the server is busy. Wait and retry.
            pause(20)
            next

          else
            # The file is not ready yet - we ask for range that cannot be provided
            # This happens when we're resuming a download and the file on the
            # server was deleted meanwhile. The file might not be exactly the same
            # as the one before, sha256sum would most likely fail.
            raise DownloadError, 'Range not satisfiable'
          end

        when '200', '206'  # OK and Partial Content
          resume

        else
          raise DownloadError, "Unexpected HTTP status code '#{res.code}'"
        end
       
        t1 = Time.now
        data_counter = 0

        res.read_body do |fragment|
          size = fragment.size

          data_counter += size
          downloaded += size

          begin
            if @pb && (@pb.total.nil? || @pb.progress < @pb.total)
              @pb.progress += size
            end

          rescue ProgressBar::InvalidProgressError
            # The total value is in MB, it is not precise, so the actual
            # size may be a little bit bigger.
            @pb.progress = @pb.total
          end

          digest.update(fragment) if checksum

          if max_rate && max_rate > 0
            t2 = Time.now
            diff = t2 - t1

            if diff > 0.005
              # Current and expected rates in kB per interval +diff+
              current_rate = data_counter / 1024
              expected_rate = max_rate * diff

              if current_rate > expected_rate
                delay = diff / (expected_rate / (current_rate - expected_rate))
                sleep(delay)
              end

              data_counter = 0
              t1 = Time.now
            end
          end
        
          io.write(fragment)
        end
      end

      # This was the last download, the transfer is complete.
      break if dl_check.ready

      # Give the server time to prepare additional data
      pause(15)
    end
  end

  @pb.finish if @pb

  # Verify the checksum
  if checksum && digest.hexdigest != dl_check.sha256sum
    raise DownloadError, 'The sha256sum does not match, retry the download'
  end
end

Protected Instance Methods

format=(fmt) click to toggle source
# File lib/vpsadmin/cli/stream_downloader.rb, line 230
def format=(fmt)
  @format = fmt
  @pb.format(@format) if @pb
end
pause(secs) click to toggle source
# File lib/vpsadmin/cli/stream_downloader.rb, line 195
def pause(secs)
  @paused = true
  
  if @pb
    secs.times do |i|
      if @download_size
        if @download_ready
          @pb.format("%t #{@download_size} GB: [%B] waiting #{secs - i}")

        else
          @pb.format("%t ~#{@download_size} GB: [%B] waiting #{secs - i}")
        end

      else
        @pb.format("%t: [%B] waiting #{secs - i}")
      end

      @pb.refresh(force: true)
      sleep(1)
    end

  else
    sleep(secs)
  end
end
resume() click to toggle source
# File lib/vpsadmin/cli/stream_downloader.rb, line 221
def resume
  @pb.format(@format) if @pb && @paused
  @paused = false
end
stop() click to toggle source
# File lib/vpsadmin/cli/stream_downloader.rb, line 226
def stop
  @pb.stop if @pb
end