Ruby でファイル転送(改良版)
obelisk.hatenablog.comローカルエリア内に PC が散らばっているので以前ファイル転送のコマンドを作ったのですが、1つのファイルしか転送できないとか、バイナリファイルは転送できないなど使いにくいところがあったので、それらに対応してみました。本当は SSH でやるのがいいのだけれど、一台の PC に Linux をマルチブートしているので厄介なのです。
ネストしたディレクトリもバイナリファイルも転送できるようにしました。
使用例はこんな感じ。受け手(サーバー)側(こちらを先に実行する)。
$ file_transfer IPアドレス: 192.168.11.7 [192.168.11.3:54902] からの接続を了承しました ディレクトリ with_else を作成しました。 [192.168.11.3:54904] からの接続を了承しました with_else-0.0.1.gem を受信中です... with_else-0.0.1.gem の受信が完了しました [192.168.11.3:54906] からの接続を了承しました with_else.gemspec を受信中です... with_else.gemspec の受信が完了しました [192.168.11.3:54908] からの接続を了承しました README を受信中です... README の受信が完了しました [192.168.11.3:54910] からの接続を了承しました ディレクトリ lib を作成しました。 [192.168.11.3:54912] からの接続を了承しました with_else.rb を受信中です... with_else.rb の受信が完了しました [192.168.11.3:54914] からの接続を了承しました [192.168.11.3:54916] からの接続を了承しました すべての受信が終了しました。
送り手(クライアント)側。
$ file_transfer 192.168.11.7 with_else ディレクトリ with_else の処理をしています... with_else-0.0.1.gem を送信中です... with_else.gemspec を送信中です... README を送信中です... with_else-0.0.1.gem の送信が完了しました with_else.gemspec の送信が完了しました README の送信が完了しました ディレクトリ lib の処理をしています... with_else.rb を送信中です... with_else.rb の送信が完了しました すべての送信が終了しました。
ちゃんとディレクトリが送れていますね。ディレクトリ名のところをファイル名に変更すれば、1ファイルの転送ももちろんできます。また、
$ file_transfer 192.168.11.7 oekaki/lib oekaki_sample21.rb ../color_p.sh
などのように、複数ファイル(ディレクトリ)の指定や相対パスでの指定もできます。
コードはこんな具合です。
file_transfer
#!/usr/bin/env ruby require 'socket' require 'thwait' def file_send host = ARGV[0] q = Queue.new 30.times {q.push(:unlock)} #スレッド数の最大値を30にする send_file = ->(name) { q.pop Thread.new(name) do |fname| puts "#{fname} を送信中です..." open(fname, "rb") do |file| size = File.size(fname) TCPSocket.open(host, 7413) do |sock| sock.set_encoding('ASCII-8BIT') sock.puts "File/#{fname}/#{size}" sock.write(file.read(size)) end end puts "#{fname} の送信が完了しました" q.push(:unlock) end } send_directory = ->(dname) { handle_dir = ->(st) { TCPSocket.open(host, 7413) do |sock| sock.set_encoding('ASCII-8BIT') sock.puts st sock.gets.chomp end } Dir.chdir(dname) handle_dir.("Mkdir/#{dname}") puts "ディレクトリ #{dname} の処理をしています..." threads = [] dirs = [] Dir.glob("*").each do |fname| if File.directory?(fname) dirs << fname else threads << send_file.(fname) end end ThreadsWait.all_waits(*threads) dirs.each {|dname| send_directory.(dname)} handle_dir.("Dirup") Dir.chdir("..") } fnames = ARGV[1..-1].map {|fn| File.expand_path(fn)} raise "No files or directorys." if fnames.empty? while (fname = fnames.shift) Dir.chdir(File.dirname(fname)) fname = File.basename(fname) if File.file?(fname) send_file.(fname).join else send_directory.(fname) end end puts "すべての送信が終了しました。" end def receive print "IPアドレス: " ip_ad = Socket.getifaddrs.select {|x| x.addr.ipv4?} puts ip_ad.map {|x| x.addr.ip_address}.select {|x| x.include?("192.168")}[0] s = TCPServer.open(7413) q = Queue.new Thread.new do q.pop () until Thread.list.size <= 2 puts "すべての受信が終了しました。" exit end loop do Thread.new(s.accept) do |sock| puts "[#{sock.peeraddr[3]}:#{sock.peeraddr[1]}] からの接続を了承しました" sock.set_encoding('UTF-8') ar = sock.gets.chomp.split('/') case ar[0] when "File" fname, size = ar.drop(1) puts "#{fname} を受信中です..." open("#{fname}", "wb") do |file| file.write(sock.read(size.to_i)) end puts "#{fname} の受信が完了しました" when "Mkdir" begin Dir.mkdir(ar[1]) rescue puts "ディレクトリが作成できません。" sock.close exit 1 end Dir.chdir(ar[1]) puts "ディレクトリ #{ar[1]} を作成しました。" sock.puts :done when "Dirup" Dir.chdir("..") sock.puts :done when "End" q.push :end sock.puts :done else raise "error: 予期せぬコマンドです。" end sock.close end end end if ARGV.size.zero? receive else file_send end
Thread を使ってみたのですが、同期を取るのに苦労しました。並行プログラミングはむずかしい。
それから、set_encoding()
で 'ASCII-8BIT' で送って 'UTF-8' に復号しているのは、socket でマルチバイト文字がそのまま送れないため。ファイル名やディレクトリ名にマルチバイト文字が使ってある場合に対応しています。
コマンドとして使うなら、
$ chmod 755 file_transfer $ sudo cp file_transfer /usr/local/bin
とでもして下さい(Linux の場合)。
自分では便利に使っています。100MB くらいまでの転送なら意外と使い物になるなという感じ。