distribustream 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,120 +8,110 @@
8
8
  # See http://distribustream.rubyforge.org/
9
9
  #++
10
10
 
11
- module PDTP
12
- # Handle a file buffer, which may be written to and read from randomly
13
- class FileBuffer
14
- def initialize(io = nil)
15
- @io = io
16
- @written = 0
17
- @entries = []
18
- end
11
+ require 'rubygems'
12
+ require 'mongrel'
13
+ require 'tempfile'
19
14
 
20
- # Write data starting at start_pos. Overwrites any existing data in that block
21
- def write(start_pos, data)
22
- return if data.size == 0
23
-
24
- # create and entry and attempt to combine it with old entries
25
- new_entry = Entry.new(start_pos,data)
15
+ module PDTP
16
+ class Client
17
+ # Handle a file buffer, which may be written to and read from randomly
18
+ class FileBuffer
19
+ def initialize(output = nil)
20
+ # The output stream. Treated as non-seekable since it may be a pipe
21
+ @output = output
22
+ @cursor = 0
23
+
24
+ # The tempfile backbuffer. This will always be seekable
25
+ @buffer = Tempfile.new 'distribustream'
26
+ @populated = []
27
+ end
26
28
 
27
- intersections = true
28
- while intersections
29
- intersections = false
30
- @entries.each do |e|
31
- if intersects?(new_entry, e)
32
- new_entry = combine(e, new_entry)
33
- @entries.delete(e)
34
- intersections = true
35
- end
29
+ # Write to the backbuffer. Write contiguous data to the output stream
30
+ def write(position, data)
31
+ length = data.size
32
+ return 0 if length.zero?
33
+
34
+ range = position..(position + length - 1)
35
+
36
+ # Insert the chunk into the buffer and return a new range of contiguous data
37
+ writeable_range = insert_chunk range, data
38
+
39
+ # If this chunk begins at the cursor, write it out
40
+ if @output and writeable_range.begin == @cursor
41
+ @cursor += @output.write read(writeable_range)
36
42
  end
43
+
44
+ # Return the length we just wrote
45
+ length
37
46
  end
38
-
39
- # Add entry to the local store
40
- @entries << new_entry
41
47
 
42
- # Write contiguous blocks we receive to our internal IO cursor
43
- if @io and start_pos == @written
44
- data_begin = @written - new_entry.start_pos
45
- bytes_written = @io.write(new_entry.data[data_begin..new_entry.data.length])
46
- @written += bytes_written
47
- else
48
- bytes_written = data.size
48
+ def read(range)
49
+ chunk = containing_chunk range.begin
50
+
51
+ unless chunk and chunk.end >= range.end
52
+ raise RuntimeError, "#{chunk.inspect} cannot satisfy range #{range.inspect}"
53
+ end
54
+
55
+ @buffer.pos = range.begin
56
+ @buffer.read(range.end - range.begin + 1)
49
57
  end
50
58
 
51
- bytes_written
52
- end
53
-
54
- # Returns a string containing the desired data.
55
- # Returns nil if the data is not all there
56
- def read(range)
57
- return nil if range.first>range.last
58
- current_byte=range.first
59
-
60
- buffer = ''
61
-
62
- while current_byte <= range.last do
63
- # find an entry that contains this byte
64
-
65
- found=false
66
- @entries.each do |e|
67
- if e.range.include?(current_byte)
68
- internal_start=current_byte-e.start_pos #start position inside this entry's data
69
- internal_end=(range.last<e.end_pos ? range.last : e.end_pos) - e.start_pos
70
- buffer << e.data[internal_start..internal_end]
71
- current_byte+=internal_end-internal_start+1
72
- found=true
73
- break if current_byte>range.last
59
+ #######
60
+ private
61
+ #######
62
+
63
+ def insert_chunk(range, data)
64
+ i = 0
65
+
66
+ # Find the first slot in the populated array that this range exceeds
67
+ i += 1 while i < @populated.size and range.begin > @populated[i].end
68
+
69
+ if i == 0
70
+ unless @populated.empty? or @populated.first.begin > range.end
71
+ raise RuntimeError, "chunks in output buffer overlap"
74
72
  end
73
+
74
+ @populated = [range] + @populated
75
+ elsif i < @populated.size
76
+ head = @populated[0..(i - 1)]
77
+ tail = @populated[i..(@populated.size - 1)]
78
+
79
+ raise RuntimeError, "chunks in output buffer overlap" if tail.first.begin <= range.end
80
+
81
+ @populated = head + [range] + tail
82
+ else
83
+ @populated << range
75
84
  end
76
- return nil if found==false
77
- end
78
-
79
- buffer
80
- end
81
-
82
- # Returns true if two entries intersect
83
- def intersects?(entry1, entry2)
84
- first, last = entry1.start_pos <= entry2.start_pos ? [entry1, entry2] : [entry2, entry1]
85
- first.end_pos + 1 >= last.start_pos
86
- end
87
-
88
- # Takes two Entries
89
- # Returns nil if there is no intersection
90
- # Returns the union if they intersect
91
- def combine(old_entry, new_entry)
92
- start = old_entry.start_pos < new_entry.start_pos ? old_entry.start_pos: new_entry.start_pos
93
-
94
- stringio = StringIO.new
95
- stringio.seek(old_entry.start_pos - start)
96
- stringio.write(old_entry.data)
97
- stringio.seek(new_entry.start_pos - start)
98
- stringio.write(new_entry.data)
99
- return Entry.new(start, stringio.string)
100
- end
101
-
102
- # Return number of bytes currently in the buffer
103
- def bytes_stored
104
- bytes=0
105
- @entries.each do |e|
106
- bytes=bytes+e.data.size
107
- end
108
- return bytes
109
- end
110
-
111
- # Container for an entry in the buffer
112
- class Entry
113
- def initialize(start_pos,data)
114
- @start_pos,@data=start_pos,data
85
+
86
+ @buffer.pos = range.begin
87
+ @buffer.write data
88
+
89
+ compact_chunks
90
+ position = range.begin
91
+
92
+ # Find the range beginning at the current position and ending at the last contiguous chunk
93
+ position..(containing_chunk(position).end)
115
94
  end
116
-
117
- attr_accessor :start_pos, :data
118
-
119
- def end_pos
120
- @start_pos + data.length - 1
95
+
96
+ def compact_chunks
97
+ output = []
98
+ current_range = @populated.shift
99
+
100
+ @populated.each do |next_range|
101
+ if current_range.end + 1 == next_range.begin
102
+ current_range = current_range.begin..next_range.end
103
+ else
104
+ output << current_range
105
+ current_range = next_range
106
+ end
107
+ end
108
+
109
+ output << current_range
110
+ @populated = output
121
111
  end
122
-
123
- def range
124
- Range.new(@start_pos,end_pos)
112
+
113
+ def containing_chunk(position)
114
+ @populated.detect { |range| range.include? position }
125
115
  end
126
116
  end
127
117
  end
@@ -8,18 +8,21 @@
8
8
  # See http://distribustream.rubyforge.org/
9
9
  #++
10
10
 
11
+ require 'rubygems'
12
+ require 'mongrel'
11
13
  require 'uri'
12
14
  require 'pathname'
15
+
13
16
  require File.dirname(__FILE__) + '/../common/file_service.rb'
14
17
  require File.dirname(__FILE__) + '/file_buffer.rb'
15
18
 
16
19
  module PDTP
17
- class Client < Mongrel::HttpHandler
20
+ class Client
18
21
  # The client specific file utilities. Most importantly, handling
19
22
  # the data buffer.
20
23
  class FileInfo < PDTP::FileInfo
21
- def initialize(filename)
22
- @buffer = FileBuffer.new open(filename, 'w')
24
+ def initialize(filename, io = nil)
25
+ @buffer = FileBuffer.new io || open(filename, 'w')
23
26
  @lock = Mutex.new
24
27
  end
25
28
 
@@ -0,0 +1,77 @@
1
+ #--
2
+ # Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)
3
+ # All rights reserved. See COPYING for permissions.
4
+ #
5
+ # This source file is distributed as part of the
6
+ # DistribuStream file transfer system.
7
+ #
8
+ # See http://distribustream.rubyforge.org/
9
+ #++
10
+
11
+ require 'rubygems'
12
+ require 'eventmachine'
13
+ require 'mongrel'
14
+
15
+ require File.dirname(__FILE__) + '/transfer'
16
+
17
+ module PDTP
18
+ class Client
19
+ # Mongrel::HttpHandler to handle incoming HTTP chunk transfers
20
+ class HttpHandler < Mongrel::HttpHandler
21
+ attr_accessor :client
22
+
23
+ def initialize(client)
24
+ @client = client
25
+ end
26
+
27
+ # This method is called after a connection to the server
28
+ # has been successfully established.
29
+ def connection_created(connection)
30
+ @@log.debug("[mongrel] Opened connection...");
31
+ end
32
+
33
+ # This method is called when the server connection is destroyed
34
+ def connection_destroyed(connection)
35
+ @@log.debug("[mongrel] Closed connection...")
36
+ end
37
+
38
+ # Returns a transfer object if the given connection is a peer associated with
39
+ # that transfer. Otherwise returns nil.
40
+ def get_transfer(connection)
41
+ @client.transfers.each { |t| return t if t.peer == connection }
42
+ nil
43
+ end
44
+
45
+ # This method is called when an HTTP request is received. It is called in
46
+ # a separate thread, one for each request.
47
+ def process(request,response)
48
+ begin
49
+ @@log.debug "Creating Transfer::Listener"
50
+ transfer = Transfer::Listener.new(
51
+ @client.connection,
52
+ request,
53
+ response,
54
+ client.file_service
55
+ )
56
+
57
+ #Needs to be locked because multiple threads could attempt to append a transfer at once
58
+ @client.lock.synchronize { @client.transfers << transfer }
59
+ transfer.handle_header
60
+ rescue Exception => e
61
+ raise e if transfer.nil?
62
+ transfer.write_http_exception(e)
63
+ end
64
+
65
+ transfer.send_completed_message transfer.hash
66
+ end
67
+
68
+ # Returns true if the given message refers to the given transfer
69
+ def transfer_matches?(transfer, message)
70
+ transfer.peer == message["peer"] and
71
+ transfer.url == message["url"] and
72
+ transfer.byte_range == message["range"] and
73
+ transfer.peer_id == message["peer_id"]
74
+ end
75
+ end
76
+ end
77
+ end
@@ -15,7 +15,7 @@ require "uri"
15
15
  require "digest/sha2"
16
16
 
17
17
  module PDTP
18
- class Client < Mongrel::HttpHandler
18
+ class Client
19
19
  module Transfer
20
20
  # Generic HTTP Exception to be used on error
21
21
  class HTTPException < Exception
@@ -29,8 +29,7 @@ module PDTP
29
29
  # The base information and methods needed by client transfers
30
30
  class Base
31
31
  attr_reader :peer, :peer_id, :url, :byte_range
32
- attr_reader :server_connection, :file_service
33
- attr_reader :method, :client, :hash
32
+ attr_reader :file_service, :method, :connection, :hash
34
33
 
35
34
  # Returns true if a server message matches this transfer
36
35
  def matches_message?(message)
@@ -52,7 +51,7 @@ module PDTP
52
51
  # Notify the server of transfer completion.
53
52
  # Hash field is used to denote success or failure
54
53
  def send_completed_message(hash)
55
- @server_connection.send_message(:completed,
54
+ @connection.send_message(:completed,
56
55
  :url => @url,
57
56
  :peer => @peer,
58
57
  :range => @byte_range,
@@ -62,7 +61,7 @@ module PDTP
62
61
  end
63
62
 
64
63
  def send_ask_verify_message
65
- @server_connection.send_message(:ask_verify,
64
+ @connection.send_message(:ask_verify,
66
65
  :url => @url,
67
66
  :peer => @peer,
68
67
  :range => @byte_range,
@@ -76,11 +75,11 @@ module PDTP
76
75
  attr :request, :response
77
76
 
78
77
  # Called with the request and response parameters given by Mongrel
79
- def initialize(request,response,server_connection,file_service,client)
80
- @request,@response=request,response
81
- @server_connection,@file_service=server_connection,file_service
82
- @authorized=false
83
- @client=client
78
+ def initialize(connection, request, response, file_service)
79
+ @request, @response = request,response
80
+ @file_service = file_service
81
+ @authorized = false
82
+ @connection = connection
84
83
  end
85
84
 
86
85
  # Send an HTTP error response to requester
@@ -177,14 +176,14 @@ module PDTP
177
176
 
178
177
  # Implements http transfer between two peers from the connector's (client) perspective
179
178
  class Connector < Base
180
- def initialize(message,server_connection,file_service,client)
181
- @server_connection,@file_service=server_connection,file_service
179
+ def initialize(connection, message, file_service)
180
+ @file_service=file_service
182
181
  @peer,@port=message["host"],message["port"]
183
182
  @method = message["method"]
184
183
  @url=message["url"]
185
184
  @byte_range=message["range"]
186
185
  @peer_id=message["peer_id"]
187
- @client=client
186
+ @connection=connection
188
187
  end
189
188
 
190
189
  # Perform the transfer
@@ -211,7 +210,7 @@ module PDTP
211
210
 
212
211
  req.add_field("Range", "bytes=#{@byte_range.begin}-#{@byte_range.end}")
213
212
  req.add_field("Host",vhost)
214
- req.add_field("X-PDTP-Peer-Id",@client.my_id)
213
+ req.add_field("X-PDTP-Peer-Id", @connection.client.client_id)
215
214
  res = Net::HTTP.start(@peer,@port) {|http| http.request(req,body) }
216
215
 
217
216
  if res.code == '206' and @method == 'get'
data/lib/pdtp/client.rb CHANGED
@@ -10,186 +10,103 @@
10
10
 
11
11
  require 'rubygems'
12
12
  require 'eventmachine'
13
- require 'mongrel'
14
- require 'net/http'
15
13
  require 'thread'
16
14
  require 'digest/md5'
17
15
 
18
- require File.dirname(__FILE__) + '/common/common_init'
19
- require File.dirname(__FILE__) + '/common/protocol'
20
- require File.dirname(__FILE__) + '/client/protocol'
16
+ require File.dirname(__FILE__) + '/client/connection'
17
+ require File.dirname(__FILE__) + '/client/callbacks'
21
18
  require File.dirname(__FILE__) + '/client/file_service'
22
- require File.dirname(__FILE__) + '/client/transfer'
23
- require File.dirname(__FILE__) + '/server/file_service'
19
+ require File.dirname(__FILE__) + '/client/http_handler'
24
20
 
25
21
  module PDTP
26
- # This is the main driver for the client-side implementation
27
- # of PDTP. It maintains a single connection to a server and
28
- # all the necessary connections to peers. It is responsible
29
- # for handling all messages corresponding to these connections.
30
-
31
- # Client inherits from Mongrel::HttpHandler in order to handle
32
- # incoming HTTP connections
33
- class Client < Mongrel::HttpHandler
34
- # Accessor for a client file service instance
35
- attr_accessor :file_service
36
- attr_accessor :server_connection
37
- attr_accessor :my_id
38
-
39
- def self.get(host, path, options = {})
40
- path = '/' + path unless path[0] == ?/
41
-
42
- opts = {
43
- :host => host,
44
- :port => 6086,
45
- :file_root => '.',
46
- :quiet => true,
47
- :listen_port => 8000,
48
- :request_url => "http://#{host}#{path}"
49
- }.merge(options)
50
-
51
- common_init $0, opts
22
+ # PDTP::Client provides an interface for accessing resources on PDTP servers
23
+ class Client
24
+ # None of these should be publically accessible, and will be factored away in time
25
+ attr_reader :connection, :client_id, :listen_port, :file_service, :transfers, :lock
26
+
27
+ # Create a PDTP::Client object for accessing the PDTP server at the given host and port
28
+ def initialize(host, port = 6086, options = {})
29
+ @host, @port = host, port
30
+
31
+ @listen_addr = options[:listen_addr] || '0.0.0.0'
32
+ @listen_port = options[:listen_port] || 60860
52
33
 
53
- # Run the EventMachine reactor loop
54
- EventMachine::run do
55
- connection = EventMachine::connect host, opts[:port], Client::Protocol
56
- @@log.info "connecting with ev=#{EventMachine::VERSION}"
57
- @@log.info "host= #{host} port=#{opts[:port]}"
58
- end
59
- end
60
-
61
- def initialize
34
+ @file_service = PDTP::Client::FileService.new
62
35
  @transfers = []
63
- @mutex = Mutex.new
64
- end
65
-
66
- # This method is called after a connection to the server
67
- # has been successfully established.
68
- def connection_created(connection)
69
- @@log.debug("[mongrel] Opened connection...");
70
- end
71
-
72
- # This method is called when the server connection is destroyed
73
- def connection_destroyed(connection)
74
- @@log.debug("[mongrel] Closed connection...")
75
- end
36
+ @lock = Mutex.new
76
37
 
77
- # Returns a transfer object if the given connection is a peer associated with
78
- # that transfer. Otherwise returns nil.
79
- def get_transfer(connection)
80
- @transfers.each { |t| return t if t.peer == connection }
81
- nil
82
- end
83
-
84
- # This method is called when an HTTP request is received. It is called in
85
- # a separate thread, one for each request.
86
- def process(request,response)
38
+ # Start a Mongrel server on the specified port. If it isnt available, keep trying higher ports
87
39
  begin
88
- @@log.debug "Creating Transfer::Listener"
89
- transfer = Transfer::Listener.new(
90
- request,
91
- response,
92
- @server_connection,
93
- @file_service,
94
- self
95
- )
96
-
97
- #Needs to be locked because multiple threads could attempt to append a transfer at once
98
- @mutex.synchronize { @transfers << transfer }
99
- transfer.handle_header
100
- rescue Exception=>e
101
- transfer.write_http_exception(e)
40
+ @http_server = Mongrel::HttpServer.new @listen_addr, @listen_port
41
+ rescue Exception => e
42
+ @listen_port += 1
43
+ retry
102
44
  end
103
45
 
104
- transfer.send_completed_message transfer.hash
105
- end
46
+ @client_id = Digest::MD5.hexdigest "#{Time.now.to_f}#{$$}"
106
47
 
107
- # Returns true if the given message refers to the given transfer
108
- def transfer_matches?(transfer, message)
109
- transfer.peer == message["peer"] and
110
- transfer.url == message["url"] and
111
- transfer.byte_range == message["range"] and
112
- transfer.peer_id == message["peer_id"]
48
+ #@@log.info "listening on port #{@listen_port}"
49
+ @http_handler = HttpHandler.new(self)
50
+ @http_server.register '/', @http_handler
51
+ @http_server.run
113
52
  end
114
53
 
115
- # Called when any server message is received. This is the brains of
116
- # the client's protocol handling.
117
- def dispatch_message(command, message, connection)
118
- case command
119
- when "tell_info" # Receive and store information for this url
120
- info = FileInfo.new message["url"].split('/').last
121
- info.file_size = message["size"]
122
- info.base_chunk_size = message["chunk_size"]
123
- info.streaming = message["streaming"]
124
- @file_service.set_info(message["url"], info)
125
- when "transfer" # Begin a transfer as a connector
126
- transfer = Transfer::Connector.new(message,@server_connection,@file_service,self)
127
-
128
- @@log.debug "TRANSFER STARTING"
129
-
130
- # Run each transfer in its own thread and notify the server upon completion
131
- Thread.new(transfer) do |t|
132
- begin
133
- t.run
134
- rescue Exception=>e
135
- @@log.info("Exception in dispatch_message: " + e.exception + "\n" + e.backtrace.join("\n"))
136
- end
137
- t.send_completed_message(t.hash)
138
- end
139
- when "tell_verify"
140
- # We are a listener, and asked for verification of a transfer from a server.
141
- # After asking for verification, we stopped running, and must be restarted
142
- # if verification is successful
143
-
144
- found=false
145
- @transfers.each do |t|
146
- if t.matches_message?(message)
147
- finished(t)
148
- t.tell_verify(message["is_authorized"])
149
- found=true
150
- break
151
- end
54
+ # Connect to the PDTP server. This is a blocking call which runs the client event loop
55
+ def connect(callbacks = nil)
56
+ klass = if(callbacks and callbacks.is_a?(Class))
57
+ callbacks
58
+ else
59
+ Class.new(Callbacks) {callbacks and include callbacks}
60
+ end
61
+
62
+ # Run the EventMachine reactor loop
63
+ EventMachine.run do
64
+ EventMachine.connect(@host, @port, Connection) do |c|
65
+ # Store reference to the connection
66
+ @connection = c
67
+
68
+ # Set connection variables and configure callbacks
69
+ c.client = self
70
+ c.callbacks = klass.new(self)
71
+ c.callbacks.client = self
72
+
73
+ # Allow users to populate instance variables in their callback class
74
+ yield c.callbacks if block_given?
152
75
  end
153
76
 
154
- unless found
155
- puts "BUG: Tell verify sent for an unknown transfer"
156
- exit!
157
- end
158
- when "hash_verify"
159
- @@log.debug "Hash verified for url=#{message["url"]} range=#{message["range"]} hash_ok=#{message["hash_ok"]}"
160
- when "protocol_error", "protocol_warn" #ignore
161
- else raise "Server sent an unknown message type: #{command} "
77
+ #@@log.info "connecting with ev=#{EventMachine::VERSION}"
78
+ #@@log.info "host= #{host} port=#{opts[:port]}"
162
79
  end
163
80
  end
164
81
 
165
- #Prints the number of transfers associated with this client
166
- def print_stats
167
- @@log.debug "client: num_transfers=#{@transfers.size}"
82
+ # Are we currently connected to a server?
83
+ def connected?
84
+ !!@connection
168
85
  end
169
86
 
170
- #Provides a threadsafe mechanism for transfers to report themselves finished
171
- def finished(transfer)
172
- @mutex.synchronize do
173
- @transfers.delete(transfer)
174
- end
175
- end
176
-
177
- # Generate and set the client ID for an instance
178
- def generate_client_id(port = 0)
179
- @my_id = Client.generate_client_id port
180
- end
87
+ # Retrieve the resource at the given path and write it to the given IO object
88
+ def get(path, io, options = {})
89
+ raise RuntimeError, "not connected to server yet" unless connected?
90
+
91
+ path = '/' + path unless path[0] == ?/
92
+ url = "http://#{@host}#{path}"
93
+ filename = path.split('/').last
94
+
95
+ # Register the file and its IO object with the local file service
96
+ file_service.set_info url, FileInfo.new(filename, io)
97
+
98
+ # Ask the server for some information on the file we want
99
+ @connection.send_message :ask_info, :url => url
181
100
 
182
- # Client ID generator routine
183
- def self.generate_client_id(port = 0)
184
- md5 = Digest::MD5::new
185
- now = Time::now
186
- md5.update now.to_s
187
- md5.update String(now.usec)
188
- md5.update String(rand(0))
189
- md5.update String($$)
101
+ # Request the file (should probably be done after receiving :tell_info)
102
+ @connection.send_message :request, :url => url
190
103
 
191
- #return md5.hexdigest+":#{port}" # long id
192
- return md5.hexdigest[0..5] # short id
104
+ #@@log.info "This client is requesting"
105
+ end
106
+
107
+ # Stop the client event loop. This only works within callbacks given to the #connect method
108
+ def stop
109
+ EventMachine.stop_event_loop
193
110
  end
194
111
  end
195
112
  end