Edited at

メモ: Ruby の Net::FTP で分割ダウンロード

More than 1 year has passed since last update.

:thinking::thought_balloon: docs.ruby-lang.org か rubygems.org に良い感じの実装が転がっている気がするんだけど、みつけられてない。



  • Net::FTP#retrbinary を使ってオフセットを指定




  • IO.copy_stream


    • 良い感じの file 結合処理を思いつけず IO.copy_stream を使った

    • 並行分割ダウンロード結果を統合するロジックを分かっていない



  • 同時接続数を制限しているサーバ相手には機能しない

require 'net/ftp'

require 'tmpdir'
require 'thwait'

DEBUG = !ENV.fetch('DEBUG', '').empty?
CONCURRENT = ENV.fetch('CONCURRENT').to_i
FTP_HOST = ENV.fetch('FTP_HOST')
FTP_PORT = ENV.fetch('FTP_PORT').to_i
FTP_USER = ENV.fetch('FTP_USER')
FTP_PASS = ENV.fetch('FTP_PASS')
FTP_PATH = ENV.fetch('FTP_PATH')

def establish_connection
Net::FTP.new(FTP_HOST, port: FTP_PORT, username: FTP_USER, password: FTP_PASS, passive: true, debug_mode: DEBUG)
end

content_size = establish_connection.size(FTP_PATH)
page_size = (content_size.to_f / CONCURRENT).ceil

Dir.mktmpdir do |dir|
threads = []
CONCURRENT.times do |i|
offset = page_size * i
next if offset >= content_size

threads << Thread.start(i) do |t|
File.open("#{dir}/#{t}", 'wb') do |page|
ftp = establish_connection
ftp.resume = true

retrieved = 0
ftp.retrbinary("RETR #{FTP_PATH}", Net::FTP::DEFAULT_BLOCKSIZE, offset) do |chunk|
rest = page_size - retrieved
retrievable = [rest, chunk.bytesize].min
break if retrievable <= 0

retrieved += page.write(chunk[0, retrievable])
end
end
end
end
ThreadsWait.all_waits(*threads)

File.open('downloaded', 'wb') do |output|
CONCURRENT.times do |i|
path = "#{dir}/#{i}"
next unless File.exist?(path)
IO.copy_stream(path, output)
end
end
end

__END__

### e.g. 495MB CentOS ISO file

ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]

1: 165.29 real 5.34 user 4.74 sys
3: 99.68 real 5.23 user 6.12 sys
5: 88.19 real 5.56 user 7.43 sys