I’m assuming you just want to save the file to the file system? If so it would be something like:
require "http"
HTTP::Client.get "https://download.savannah.gnu.org/releases/acl/acl-2.3.1.tar.xz" do |response|
File.open("out.tar.xz", "w") do |file|
IO.copy response.body_io, file
end
end
However, HTTP::Client doesn’t support redirects at the moment, see HTTP::Client: follow redirects · Issue #2721 · crystal-lang/crystal · GitHub, so you will probably have to add in some extra logic to handle that case. I.e. check for response.status.redirection? and if true, look for the location header and make another request before copying the contents to the file.
So I followed your example, and I tried with your explanation to have the expected result.
require "http"
HTTP::Client.get("https://www.kernel.org/pub/linux/kernel/v5.x/linux-5.13.12.tar.xz") do |response|
if response.status.redirection?
HTTP::Client.get(response.headers["location"]) do |response|
File.open("linux-5.13.12.tar.xz", "w") do |file|
IO.copy response.body_io, file
end
end
else
File.open("linux-5.13.12.tar.xz", "w") do |file|
IO.copy response.body_io, file
end
end
end
I am not very good actually with networking
I am definitely sure this can be improve a lot, but I think I didn’t understand everything.
If I use this code, is it okay or it’s a bit slow ?
Like that can I handle multiple redirections ?
How can I mesure received and sended data amount ? Because I would like to provide a nice cli output
A simple way is to just use an intermediate buffer, where you read X bytes from the server, then write X bytes to the destination.
Here’s a whole example client that lets you download something over HTTP(S) to a file and handles redirects.
require "http/client"
abort "No URL" unless ARGV.size > 0
abort "No output file" unless ARGV.size == 2
# Don't clobber files.
abort "File exists" if File.exists?(ARGV[1])
url = ARGV[0]
downloaded = false
# Loop until we've downloaded something (or raise an exception and die spectacularly).
until downloaded
# Do a GET to the url
HTTP::Client.get(url) do |resp|
# Check for a redirect
if resp.status.redirection?
# Get a new URL
if location = resp.headers["location"]
url = location
puts "Redirected to #{url}"
else
abort "Got status #{resp.status_code}, but no location was sent"
end
break # Get out of the .get
end
# Check for status 200
if resp.status_code == 200
# We'll use a buffer so that we can print a percentage.
buf = Bytes.new(65536)
totalRead = 0
len = 0
# If the headers have a Content-Length, then we can provide a
# percentage.
if resp.headers["Content-Length"]?
len = resp.headers["Content-Length"].to_i32
end
# Open the output file
File.open(ARGV[1], "wb") do |file|
# Start reading the data from the HTTP client response.
while (pos = resp.body_io.read(buf)) > 0
file.write(buf[0...pos])
# Print the progress
if len > 0
totalRead += pos
puts "Downloaded: #{totalRead} / #{len}"
else
totalRead += pos
puts "Downloaded: #{totalRead} / ???"
end
end
end
downloaded = true
else
# Don't handle any others.
abort "Received HTTP status code #{resp.status_code}"
end
end
end
I added this to one of my programs just last night. Taking the code from before…
require "http/client"
abort "No URL" unless ARGV.size > 0
abort "No output file" unless ARGV.size == 2
abort "File exists" if File.exists?(ARGV[1]) # Don't clobber files.
url = ARGV[0]
downloaded = false
# Loop until we've downloaded something (or raise an exception and die spectacularly).
until downloaded
# Do a GET to the url
HTTP::Client.get(url) do |resp|
# Check for a redirect
if resp.status.redirection?
# Get a new URL
url = resp.headers["location"]? || abort "Got status #{resp.status_code}, but no location was sent"
puts "Redirected to #{url}"
break # Get out of the .get
end
lastSpeedUpdate = Time.monotonic
# Used to hold the average speed
avg = 0
# The number of bytes we've read between updates.
bytesLastPeriod = 0
# Check for status 200
if resp.status_code == 200
# We'll use a buffer so that we can print some progress.
buf = Bytes.new(65536)
totalRead = 0
len = 0
# If the headers have a Content-Length, then we can provide a
# percentage.
len = resp.headers["Content-Length"].to_i32 if resp.headers["Content-Length"]?
# Open the output file
File.open(ARGV[1], "wb") do |file|
# Start reading the data from the HTTP client response.
while (pos = resp.body_io.read(buf)) > 0
# Calculate average speed, one per second.
lapsed = Time.monotonic - lastSpeedUpdate
if lapsed.total_seconds >= 1
div = lapsed.total_nanoseconds / 1_000_000_000
avg = (bytesLastPeriod / div).to_i32!
bytesLastPeriod = 0
lastSpeedUpdate = Time.monotonic
end
# Copy from the buffer to the output file, up to pos bytes.
file.write(buf[0...pos])
bytesLastPeriod += pos
# Print the progress
if len > 0
totalRead += pos
puts "Downloaded: #{totalRead} / #{len} #{avg.humanize_bytes}/s"
else
totalRead += pos
puts "Downloaded: #{totalRead} / ??? #{avg.humanize_bytes}/s"
end
end
end
# Done downloading
downloaded = true
else
# Don't handle any others.
abort "Received HTTP status code #{resp.status_code}"
end
end
end