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 くらいまでの転送なら意外と使い物になるなという感じ。