Ruby com XMPP Filetransfer: enviando e recebendo arquivos

Este é meu segundo post sobre como usar Ruby com o protocolo XMPP. No primeiro descrevi como enviar e receber mensagens com Ruby e XMPP, agora irei mostrar como enviar arquivos de um cliente Jabber para um outro.

Antes de começar gostaria de informar que vamos utilizar os mesmos recursos de servidor Jabber e usuários já configurados no meu primeiro artigo desta série. A novidade aqui fica por conta do código. Iremos começar pelo cliente que ficará encarregado de receber os arquivos.

O script receiver.rb vai instanciar a classe ReceiveFile do arquivo receive_file.rb que faz toda a reponsabilidade do recebimento.

receiver.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env ruby

require 'rubygems'
require 'xmpp4r'
require 'receive_file'

client = Jabber::Client.new(Jabber::JID.new('neo@localhost/Home'))
client.connect('localhost', '5222')
client.auth('123456')
client.send(Jabber::Presence.new.set_type(:available))
client.send(Jabber::Presence.new(:chat, 'Files Transfer!'))

receive_file = ReceiveFile.new
receive_file.wating_for_incoming(client, '/tmp')

receive_file.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
require 'xmpp4r/bytestreams'

class ReceiveFile
  def wating_for_incoming(client = nil, destination_dir = nil)
    @file_transfer = Jabber::FileTransfer::Helper.new(client)
    receive_file_callback(destination_dir)
    puts "Waiting for incoming files..."
    Thread.stop
  end

  private
  def receive_file_callback(destination_dir = nil)
    @file_transfer.add_incoming_callback do |iq, file|
      puts "Incoming file transfer from #{iq.from}: #{file.fname} (#{format_file_size(file.size)})"
      filename = "#{destination_dir}/#{file.fname.split(/\//).last}"
      file_offset = check_incoming_file_offset(iq, file, filename)

      Thread.new {
        begin
          puts "Accepting #{file.fname}"
          @stream = @file_transfer.accept(iq, file_offset)
          if @stream.kind_of?(Jabber::Bytestreams::SOCKS5Bytestreams)
            @stream.connect_timeout = 60
            add_stream_callback
          end
          handle_receiving(filename, file_offset)
        rescue Exception => exception
          puts "#{exception.class}: #{exception}\n#{exception.backtrace.join("\n")}"
        end
      } # Thread
    end
  end

  def check_incoming_file_offset(iq = nil, file = nil, filename = nil)
    file_offset = nil
    if File::exist?(filename)
      puts "#{filename} already exists"
      if (File::size(filename) < file.size) && (file.range)
        file_offset = File::size(filename)
        puts "Peer supports <range/>, will retrieve #{file.fname} starting at #{file_offset}"
      else
        puts "#{file.fname} is already fully retrieved, declining file-transfer"
        @file_transfer.decline(iq)
      end
    end
   
    return file_offset
  end
 
  def add_stream_callback
    @stream.add_streamhost_callback do |streamhost, state, exception|
      case state
        when :connecting
          puts "Connecting to #{streamhost.jid} (#{streamhost.host}:#{streamhost.port})"
        when :success
          puts "Successfully using #{streamhost.jid} (#{streamhost.host}:#{streamhost.port})"
        when :failure
          puts "Error using #{streamhost.jid} (#{streamhost.host}:#{streamhost.port}): #{exception}"
      end
    end  
  end

  def handle_receiving(filename = nil, file_offset = nil)
    puts "Waiting for stream configuration..."
    if @stream.accept
      puts "Stream established"
      outfile = File.new(filename, (file_offset ? 'a' : 'w'))
      while buf = @stream.read
        break if buf.nil? || buf == ''
        outfile.write(buf)
        print '.'
        $stdout.flush
      end
      puts '!'
      outfile.close
      @stream.close
    else
      raise 'Stream failed'
    end
  end
 
  def format_file_size(bytes_size = 0)
    kilo_bytes_size = bytes_size / 1024
    return "#{(kilo_bytes_size.to_i == 0) ? bytes_size.to_s + ' B' : kilo_bytes_size.to_s + ' KB' }"
  end
end

Agora execute o receiver.rb:

 shell> ruby receiver.rb
 Waiting for incoming files. . .

Neste momento ele está esperando que alguém envie um arquivo. Vamos agora aprender como enviar um arquivo, de acordo com o código que se segue.

Aqui utilizei o mesmo principio do receiver: o sender.rb instancia a classe SendFile do arquivo send_file.rb.

sender.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env ruby

require 'rubygems'
require 'xmpp4r'
require 'send_file'

client = Jabber::Client.new(Jabber::JID.new('acc@localhost/Home'))
client.connect('localhost', '5222')
client.auth('123456')
client.send(Jabber::Presence.new.set_type(:available))
client.send(Jabber::Presence.new(:chat, 'Files Transfer!'))

send_file = SendFile.new
status = send_file.send(client, 'neo@localhost/Home', '/home/acc/document.txt')

send_file.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
require 'xmpp4r/bytestreams'

class SendFile
  def send(from = nil, to = nil, filename = nil)
    begin
      file_transfer = Jabber::FileTransfer::Helper.new(from)
      @source = Jabber::FileTransfer::FileSource.new(filename)
      puts "Offering #{@source.filename} to #{to}"
      @stream = file_transfer.offer(Jabber::JID.new(to), @source)
      return handler_stremam
    rescue Exception => exception
      raise exception.to_s
    end
  end
 
  private  
  def handler_stremam
    if @stream
      stream_callback
      @stream.open      
      if @stream.kind_of? Jabber::Bytestreams::SOCKS5BytestreamsInitiator
        puts "Using streamhost #{@stream.streamhost_used.jid} (#{@stream.streamhost_used.host}:#{@stream.streamhost_used.port})"
      end

      puts 'Stream established'
      while buf = @source.read
        print '.'
        $stdout.flush
        @stream.write buf
        @stream.flush
      end
      puts '!'
      @stream.close
      return true
    else
      puts 'Error while hadling stream!'
      return false
    end
  end
 
  def stream_callback
    if @stream.kind_of? Jabber::Bytestreams::SOCKS5BytestreamsInitiator
      @stream.add_streamhost(socket_server)
     
      # TODO: Proxy configuration
      ([]).each { |proxy|
        @stream.add_streamhost proxy
      }

      @stream.add_streamhost_callback { |streamhost, state, exception|
        case state
          when :connecting
            puts "Connecting to #{streamhost.jid} (#{streamhost.host}:#{streamhost.port})"
          when :success
            puts "Successfully using #{streamhost.jid} (#{streamhost.host}:#{streamhost.port})"
          when :failure
            puts "Error using #{streamhost.jid} (#{streamhost.host}:#{streamhost.port}): #{exception}"
        end
      }
    end
  end

  def socket_server
    # TODO: Socket configuration
    puts 'Biding Local server: locahost:#65010'
    begin
      bss = Jabber::Bytestreams::SOCKS5BytestreamsServer.new('65010')
      bss.add_address('localhost')      
      return bss
    rescue Exception => exception
      raise exception.to_s
    end
  end
 
end

Agora basta executar o sender.rb que a transferência de arquivos começará, de acordo com o output abaixo:

 shell> ruby sender.rb
 Offering document.txt to neo@localhost/Home
 Biding Local server: locahost:#65010
 Successfully using acc@localhost/Home (localhost:65010)
 Using streamhost acc@localhost/Home (localhost:65010)
 Stream established
 ……………….!

Voltando ao shell do receiver.rb teremos o seguinte output:

 shell> ruby receiver.rb
 Waiting for incoming files…
 Incoming file transfer from acc@localhost/Home: document.txt (53 kB)
 Accepting document.txt
 Waiting for stream configuration…
 Connecting to acc@localhost/Home (127.0.0.1:65010)
 Successfully using acc@localhost/Home (127.0.0.1:65010)
 Stream established
 .!

Pronto! Veja agora que existe um arquivo transferido da sua pasta de origem até a sua pasta de destino.

Xmpp4r no Windows

O Xmpp4r apresenta problema ao transferir arquivos binários a partir de máquinas Windows. Para resolver este problema abra para edição o arquivo:

 C:\Ruby\lib\ruby\gems\1.8\gems\xmpp4r-0.5\lib\xmpp4r\bytestreams\helper\filetransfer.rb

Procure no código o método initialize da classe FileSource e mude a seguinte linha de:

1
@file = File.new(filename)

para:

1
@file = File.open(filename, 'rb')

Salve, feche o arquivo e teste a transferẽncia.

Bom pessoal, este é o segundo post sobre como utilizar Ruby com XMPP. O terceiro vou mostrar como fazer aplicações web em tempo real usando as mesmas ferramentas. Até lá!