fargo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ module Fargo
2
+ module Connection
3
+ class Search < Base
4
+
5
+ # maybe do something special here at some point?
6
+ # this is currently just receiving the search result packets over UDP
7
+ # and fowarding them to the client who will handle them. This doesn't
8
+ # explicitly disconnect because I'm not sure if multiple results
9
+ # are sent. This connection will close itself because there will
10
+ # be a read error I think...
11
+ def receive data
12
+ message = parse_message data
13
+
14
+ @client.publish message[:type], message
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,85 @@
1
+ # TODO: actually get this class to work in some fashion
2
+ module Fargo
3
+ module Connection
4
+ class Upload < Base
5
+
6
+ include Fargo::Utils
7
+ include Fargo::Parser
8
+
9
+ def post_listen
10
+ @lock, @pk = generate_lock
11
+ write "$MyNick #{self[:nick]}|$Lock #{@lock} Pk=#{@pk}"
12
+ @handshake_step = 0
13
+ end
14
+
15
+ def supports
16
+ "$Supports BZList TTHL TTHF" # ???
17
+ end
18
+
19
+ def receive data
20
+ message = parse_message data
21
+ publish message[:type], message
22
+ case message[:type]
23
+ when :mynick
24
+ if @handshake_step == 0
25
+ @remote_nick = message[:nick]
26
+ @handshake_step = 1
27
+ else
28
+ disconnect
29
+ end
30
+ when :lock
31
+ if @handshake_step == 1
32
+ @remote_lock = message[:lock]
33
+ @handshake_step = 2
34
+ else
35
+ disconnect
36
+ end
37
+ when :supports
38
+ if @handshake_step == 2
39
+ @remote_extensions = message[:extensions]
40
+ @handshake_step = 3
41
+ else
42
+ disconnect
43
+ end
44
+ when :direction
45
+ if @handshake_step == 3 && message[:direction] == 'download'
46
+ @handshake_step = 4
47
+ @client_num = message[:number]
48
+ else
49
+ disconnect
50
+ end
51
+ when :key
52
+ if @handshake_step == 4 && generate_key(@lock) == message[:key]
53
+ write supports
54
+ write "$Direction Download #{@my_num = rand 10000}"
55
+ write "$Key #{generate_key @remote_lock}"
56
+ @handshake_step = 5
57
+ else
58
+ disconnect
59
+ end
60
+ when :get
61
+ if @handshake_step == 5
62
+ @filepath = message[:path]
63
+ @offset = message[:offset]
64
+ write "$FileLength #{file_length}"
65
+ @handshake_step = 5
66
+ else
67
+ disconnect
68
+ end
69
+ when :send
70
+ write_chunk if @handshake_step == 5
71
+
72
+ else
73
+ # Fargo.logger.warn "Ignoring `#{data}'\n"
74
+ end
75
+ end
76
+
77
+ def write_chunk
78
+ end
79
+
80
+ def file_length
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,121 @@
1
+ module Fargo
2
+ module Parser
3
+
4
+ #
5
+ # See <http://www.teamfair.info/DC-Protocol.htm> for more information
6
+ #
7
+ @@commandmatch = /\$(.*)$/
8
+ @@messagematch = /^<(.*?)> (.*)$/
9
+
10
+ # TODO: Supports, UserIP, ops command
11
+ # Client - hub commands
12
+ @@validatedenied = /^ValidateDenide/
13
+ @@getpass = /^GetPass$/
14
+ @@badpass = /^BadPass$/
15
+ @@lock = /^Lock (.*) Pk=.*?$/
16
+ @@userip = /^UserIP (.*)$/
17
+ @@hubname = /^HubName (.*)$/
18
+ @@hubfull = /^HubIsFull$/
19
+ @@hubtopic = /^HubTopic (.*)$/
20
+ @@hello = /^Hello (.*)$/
21
+ @@myinfo = /^MyINFO \$ALL (.*?) (.*?)\$ \$(.*?).\$(.*?)\$(.*?)\$/
22
+ @@myinfo2 = /^MyINFO \$ALL (.*?) (.*?)\$$/
23
+ @@to = /^To: (.*?) From: (.*?) \$<.*?> (.*)$/
24
+ @@hubto = /^To: (.*?) From: Hub \$(.*)$/
25
+ @@ctm = /^ConnectToMe (.*?) (.*?):(.*?)$/
26
+ @@nicklist = /^NickList (.*?)$/
27
+ @@psr = /^SR (.*?) (.*?)\005(.*?) (.*?)\/(.*?)\005(.*?) \((.*?):(.*?)\)$/
28
+ @@psearch = /^Search Hub:(.*) (.)\?(.)\?(.*)\?(.)\?(.*)$/
29
+ @@search = /^Search (.*):(.*) (.)\?(.)\?(.*)\?(.)\?(.*)$/
30
+ @@oplist = /^OpList (.*?)$/
31
+ @@botlist = /^BotList (.*?)$/
32
+ @@quit = /^Quit (.*)$/
33
+ @@sr = /^SR (.*?) (.*?)\005(.*?) (.*?)\/(.*?)\005(.*?) (.*?):(.*?)$/
34
+ @@rctm = /^RevConnectToMe (.*?) (.*?)$/
35
+
36
+ # Client to client commands
37
+ @@mynick = /^MyNick (.*)$/
38
+ @@key = /^Key (.*)$/
39
+ @@direction = /^Direction (Download|Upload) (\d+)$/
40
+ @@get = /^Get (.*)\$(\d+)$/
41
+ @@send = /^Send$/
42
+ @@filelength = /^FileLength (.*?)$/
43
+ @@getlistlen = /^GetListLen$/
44
+ @@maxedout = /^MaxedOut$/
45
+ @@supports = /^Supports (.*)$/
46
+ @@error = /^Error (.*)$/
47
+ @@ugetblock = /^UGetBlock (.*?) (.*?) (.*)$/
48
+ @@adcsnd = /^ADCSND (.*?) (.*?) (.*?) (.*?)$/
49
+ @@adcsnd_zl1 = /^ADCSND (.*?) (.*?) (.*?) (.*?) ZL1$/
50
+
51
+ def parse_message text
52
+ case text
53
+ when @@commandmatch then parse_command_message $1
54
+ when @@messagematch then {:type => :chat, :from => $1, :text => $2}
55
+ when '' then {:type => :garbage}
56
+ else {:type => :mystery, :text => text}
57
+ end
58
+ end
59
+
60
+ def parse_command_message text
61
+ case text
62
+ when @@validatedenied then {:type => :denide}
63
+ when @@getpass then {:type => :getpass}
64
+ when @@badpass then {:type => :badpass}
65
+ when @@lock then {:type => :lock, :lock => $1}
66
+ when @@hubname then {:type => :hubname, :name => $1}
67
+ when @@hubname then {:type => :hubfull}
68
+ when @@hubtopic then {:type => :hubtopic, :topic => $1}
69
+ when @@hello then {:type => :hello, :who => $1}
70
+ when @@myinfo then {:type => :myinfo, :nick => $1, :interest => $2, :speed => $3,
71
+ :email => $4, :sharesize => $5.to_i}
72
+ when @@myinfo2 then {:type => :myinfo, :nick=> $1, :interest => $2, :sharesize=> 0}
73
+ when @@to then {:type => :privmsg, :to => $1, :from => $2, :text => $3}
74
+ when @@hubto then {:type => :privmsg, :to => $1, :from => "Hub", :text => $2}
75
+ when @@ctm then {:type => :connect_to_me, :nick => $1, :address => $2,
76
+ :port => $3.to_i}
77
+ when @@nicklist then {:type => :nick_list, :nicks => $1.split(/\$\$/)}
78
+ when @@psr then {:type => :search_result, :nick => $1, :file => $2,
79
+ :size => $3.to_i, :open_slots => $4.to_i, :slots => $5.to_i,
80
+ :hub => $6, :address => $7,
81
+ :port => $8.to_i}
82
+ when @@psearch then {:type => :search, :searcher => $1, :restrict_size => $2,
83
+ :min_size => $3.to_i, :size => $4.to_i, :filetype => $5,
84
+ :pattern => $6}
85
+ when @@search then {:type => :search, :address => $1, :port => $2.to_i,
86
+ :restrict_size => $3,
87
+ :min_size => $4.to_i, :size => $5.to_i, :filetype => $6,
88
+ :pattern => $7}
89
+ when @@oplist then {:type => :op_list, :nicks => $1.split(/\$\$/)}
90
+ when @@oplist then {:type => :bot_list, :nicks => $1.split(/\$\$/)}
91
+ when @@quit then {:type => :quit, :who => $1}
92
+ when @@sr then {:type => :search_result, :nick => $2, :file => $3,
93
+ :size => $4.to_i, :open_slots => $5.to_i, :slots => $6.to_i,
94
+ :hubname => $7}
95
+ when @@rctm then {:type => :revconnect, :who => $1}
96
+
97
+ when @@mynick then {:type => :mynick, :nick => $1}
98
+ when @@key then {:type => :key, :key => $1}
99
+ when @@direction then {:type => :direction, :direction => $1.downcase, :number => $3.to_i}
100
+ when @@get then {:type => :get, :path => $1, :offset => $2.to_i - 1}
101
+ when @@send then {:type => :send}
102
+ when @@filelength then {:type => :file_length, :size => $1.to_i}
103
+ when @@getlistlen then {:type => :getlistlen}
104
+ when @@maxedout then {:type => :noslots}
105
+ when @@supports then {:type => :supports, :extensions => $1.split(' ')}
106
+ when @@error then {:type => :error, :message => $1}
107
+ when @@adcsnd_zl1 then {:type => :adcsnd, :kind => $1, :tth => $2, :offset => $3.to_i,
108
+ :size => $4.to_i, :zlib => true}
109
+ when @@adcsnd then {:type => :adcsnd, :kind => $1, :tth => $2, :offset => $3.to_i,
110
+ :size => $4.to_i}
111
+ when @@ugetblock then {:type => :ugetblock, :start => $1.to_i, :finish => $2.to_i,
112
+ :path => $3}
113
+ when @@userip then
114
+ h = {:type => :userip, :users => {}}
115
+ $1.split("$$").map{ |s| h[:users][s.split(' ')[0]] = s.split(' ')[1]}
116
+ h
117
+ else {:type => :mystery, :text => text}
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,26 @@
1
+ module Fargo
2
+ module Publisher
3
+
4
+ attr_reader :subscribers
5
+
6
+ def subscribe &subscriber
7
+ raise RuntimeError.new("Need a subscription block!") if subscriber.nil?
8
+ Fargo.logger.debug "#{self}: subscribing #{subscriber}"
9
+ (@subscribers ||= []) << subscriber
10
+ end
11
+
12
+ def subscribed_to?
13
+ @subscribers && @subscribers.size > 0
14
+ end
15
+
16
+ def unsubscribe &subscriber
17
+ raise RuntimeError.new("Need a subscription block!") if subscriber.nil?
18
+ Fargo.logger.debug "#{self}: unsubscribing #{subscriber}"
19
+ (@subscribers ||= []).delete subscriber
20
+ end
21
+
22
+ def publish message_type, hash = {}
23
+ @subscribers.each { |subscriber| subscriber.call message_type, hash } if @subscribers
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,61 @@
1
+ module Fargo
2
+ class Search
3
+
4
+ ANY = 1
5
+ AUDIO = 2
6
+ COMPRESSED = 3
7
+ DOCUMENT = 4
8
+ EXECUTABLE = 5
9
+ VIDEO = 7
10
+ FOLDER = 8
11
+
12
+ attr_accessor :size_restricted, :is_minimum_size, :size, :filetype, :pattern
13
+
14
+ def initialize opts = {}
15
+ self.size_restricted = opts[:size_restricted]
16
+ self.is_minimum_size = opts[:is_minimum_size]
17
+ self.size = opts[:size]
18
+ self.filetype = opts[:filetype] || ANY
19
+ if opts[:pattern]
20
+ self.pattern = opts[:pattern]
21
+ elsif opts[:query]
22
+ self.query = opts[:query]
23
+ end
24
+ end
25
+
26
+ def query= query
27
+ @pattern = query.split(' ').join('$')
28
+ end
29
+
30
+ def queries
31
+ pattern.split("$")
32
+ end
33
+
34
+ def query
35
+ pattern.gsub('$', ' ')
36
+ end
37
+
38
+ def matches_result? map
39
+ file = map[:file].downcase
40
+ matches_query = queries.inject(true) { |last, word| last && file.index(word.downcase) }
41
+ if size_restricted == 'T'
42
+ if is_minimum_size
43
+ matches_query && map[:size] > size
44
+ else
45
+ matches_query && map[:size] < size
46
+ end
47
+ else
48
+ matches_query
49
+ end
50
+ end
51
+
52
+ def to_s
53
+ if size_restricted
54
+ "#{size_restricted ? 'T' : 'F' }?#{!size_restricted || is_minimum_size ? 'T' : 'F'}?#{size || 0}?#{filetype}?#{pattern}"
55
+ else
56
+ "F?T?#{size || 0}?#{filetype}?#{pattern}"
57
+ end
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ module Fargo
2
+ class SearchResult
3
+
4
+ # Needs :file, :filesize, :client, :target (if passive)
5
+ def initialize file, filesize, client, target = nil
6
+ @file, @filesize, @client, @target = file, filesize, client, target
7
+ end
8
+
9
+ def to_s
10
+ file = @file.gusb '/', "\\"
11
+ if File.directory? @file
12
+ s = file
13
+ else
14
+ s = "#{file}\005#{@filesize}"
15
+ end
16
+
17
+ s << sprintf(" %d/%d\005%s (%s:%d)", @client.open_slots,
18
+ @client.slots,
19
+ @client.hub.hubname,
20
+ @client.hub.config.ip,
21
+ @client.hub.config.port)
22
+ s << "\005#{@target}" if @client.config.passive
23
+ end
24
+
25
+ def active_send nick, ip, port
26
+ socket = UDPSocket.new
27
+ socket.send "$SR #{nick} #{to_s}", 0, ip, port
28
+ socket.close
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ module Fargo
2
+ class Server
3
+
4
+ include Fargo::Publisher
5
+
6
+ def initialize options = {}
7
+ @options = options
8
+ @options[:address] = '0.0.0.0'
9
+ @peers = []
10
+ end
11
+
12
+ def connected?
13
+ !@server.nil?
14
+ end
15
+
16
+ def connect
17
+ return if connected?
18
+
19
+ Fargo.logger.info "#{self}: Starting server on #{@options[:address]}:#{@options[:port]}"
20
+
21
+ @server = TCPServer.new @options[:address], @options[:port]
22
+
23
+ @active_thread = Thread.start { loop {
24
+
25
+ connection = @options[:connection].new @options.merge(:first => false)
26
+
27
+ connection_type = self.class.name.split("::").last.downcase
28
+ disconnect_symbol = :"#{connection_type}_disconnected"
29
+
30
+ connection.subscribe{ |type, hash|
31
+ @peers.delete connection if type == disconnect_symbol
32
+ }
33
+
34
+ connection.socket = @server.accept
35
+ connection.listen
36
+ @peers << connection
37
+ } }
38
+ end
39
+
40
+ def disconnect
41
+ Fargo.logger.info "#{self}: disconnecting..."
42
+ @active_thread.exit if @active_thread
43
+
44
+ @server.close if @server rescue nil
45
+ @server = nil
46
+
47
+ @peers.each{ |p| p.disconnect }
48
+ @peers.clear
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,35 @@
1
+ module Fargo
2
+ module Supports
3
+ module Chat
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ set_callback :setup, :after, :subscribe_to_chats
8
+ end
9
+
10
+ def messages
11
+ @public_chats
12
+ end
13
+
14
+ def messages_with nick
15
+ @chats[nick] if @chats
16
+ end
17
+
18
+ def subscribe_to_chats
19
+ @public_chats = []
20
+ @chats = Hash.new{ |h, k| h[k] = [] }
21
+
22
+ subscribe do |type, map|
23
+ if type == :chat
24
+ @public_chats << map
25
+ elsif type == :privmsg
26
+ @chats[map[:from]] << map
27
+ elsif type == :hub_disconnected
28
+ @chats.clear
29
+ @public_chats.clear
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,261 @@
1
+ module Fargo
2
+ module Supports
3
+ module Downloads
4
+ extend ActiveSupport::Concern
5
+
6
+ class Download < Struct.new(:nick, :file, :tth, :size, :offset)
7
+ attr_accessor :percent, :status
8
+
9
+ def file_list?
10
+ file == 'files.xml.bz2'
11
+ end
12
+ end
13
+
14
+ attr_reader :current_downloads, :finished_downloads, :queued_downloads,
15
+ :failed_downloads, :open_download_slots, :trying, :timed_out,
16
+ :download_slots
17
+
18
+ included do
19
+ set_callback :setup, :after, :initialize_queues
20
+ end
21
+
22
+ def clear_failed_downloads
23
+ failed_downloads.clear
24
+ end
25
+
26
+ def clear_finished_downloads
27
+ finished_downloads.clear
28
+ end
29
+
30
+ def download nick, file, tth=nil, size=-1, offset=0
31
+ raise ConnectionException.new 'Not connected yet!' unless hub
32
+ raise 'File cannot be nil!' if file.nil?
33
+
34
+ unless nicks.include? nick
35
+ raise ConnectionException.new "User #{nick} does not exist!"
36
+ end
37
+
38
+ download = Download.new nick, file, tth, size, offset
39
+ download.percent = 0
40
+ download.status = 'idle'
41
+
42
+ # Append it to the queue of things to download. This will be processed
43
+ # elsewhere
44
+ @to_download << download
45
+ true
46
+ end
47
+
48
+ def retry_download nick, file
49
+ dl = (@failed_downloads[nick] ||= []).detect{ |h| h.file == file }
50
+
51
+ if dl.nil?
52
+ Fargo.logger.warn "#{file} isn't a failed download for: #{nick}!"
53
+ return
54
+ end
55
+
56
+ @failed_downloads[nick].delete dl
57
+ download dl.nick, dl.file, dl.tth, dl.size
58
+ end
59
+
60
+ def remove_download nick, file
61
+ # We need to synchronize this access, so append these arguments to a
62
+ # queue to be processed later
63
+ @to_remove << [nick, file]
64
+ true
65
+ end
66
+
67
+ def lock_next_download! user, connection
68
+ @downloading_lock.synchronize {
69
+ return get_next_download_with_lock! user, connection
70
+ }
71
+ end
72
+
73
+ # If a connection timed out, retry all queued downloads for that user
74
+ def try_again nick
75
+ return false unless @timed_out.include? nick
76
+
77
+ @timed_out.delete nick
78
+ downloads = @failed_downloads[nick].dup
79
+ @failed_downloads[nick].clear
80
+ downloads.each{ |d| download nick, d.file, d.tth, d.size }
81
+
82
+ true
83
+ end
84
+
85
+ private
86
+
87
+ # Finds the next queued up download and begins downloading it.
88
+ def start_download
89
+ return false if open_download_slots == 0 || @current_downloads.size + @trying.size > download_slots
90
+
91
+ arr = nil
92
+
93
+ @downloading_lock.synchronize {
94
+ # Find the first nick and download list
95
+ arr = @queued_downloads.to_a.detect{ |nick, downloads|
96
+ downloads.size > 0 &&
97
+ !@current_downloads.has_key?(nick) &&
98
+ !@trying.include?(nick) &&
99
+ !@timed_out.include?(nick) &&
100
+ has_slot?(nick)
101
+ }
102
+
103
+ return false if arr.nil? || arr.size == 0
104
+ dl_nick = arr[0]
105
+ connection = connection_for dl_nick
106
+
107
+ # If we already have an open connection to this user, tell that
108
+ # connection to download the file. Otherwise, request a connection
109
+ # which will handle downloading when the connection is complete.
110
+ if connection
111
+ Fargo.logger.debug "Requesting previous connection downloads: #{arr[1].first}"
112
+ download = get_next_download_with_lock! dl_nick, connection
113
+ connection.download = download
114
+ connection.begin_download!
115
+ else
116
+ Fargo.logger.debug "Requesting connection with: #{dl_nick} for downloading"
117
+ @trying << dl_nick
118
+ connect_with dl_nick
119
+ end
120
+ }
121
+
122
+ arr
123
+ end
124
+
125
+ # This method should only be called when synchronized by the mutex
126
+ def get_next_download_with_lock! user, connection
127
+ raise 'No open slots!' if @open_download_slots <= 0
128
+ raise "Already downloading from #{user}!" if @current_downloads[user]
129
+
130
+ if @queued_downloads[user].nil? || @queued_downloads[user].size == 0
131
+ return nil
132
+ end
133
+
134
+ download = @queued_downloads[user].shift
135
+ @current_downloads[user] = download
136
+ @trying.delete user
137
+
138
+ Fargo.logger.debug "#{self}: Locking download: #{download}"
139
+
140
+ block = Proc.new{ |type, map|
141
+ Fargo.logger.debug "#{connection}: received: #{type.inspect} - #{map.inspect}"
142
+
143
+ if type == :download_progress
144
+ download.percent = map[:percent]
145
+ elsif type == :download_started
146
+ download.status = 'downloading'
147
+ elsif type == :download_finished
148
+ connection.unsubscribe &block
149
+ download.percent = 1
150
+ download.status = 'finished'
151
+ download_finished! user, false
152
+ elsif type == :download_failed || type == :download_disconnected
153
+ connection.unsubscribe &block
154
+ download.status = 'failed'
155
+ download_finished! user, true
156
+ end
157
+ }
158
+
159
+ connection.subscribe &block
160
+
161
+ download
162
+ end
163
+
164
+ def download_finished! user, failed
165
+ download = nil
166
+ @downloading_lock.synchronize{
167
+ download = @current_downloads.delete user
168
+ @open_download_slots += 1
169
+ }
170
+
171
+ if failed
172
+ (@failed_downloads[user] ||= []) << download
173
+ else
174
+ (@finished_downloads[user] ||= []) << download
175
+ end
176
+
177
+ start_download # Start another download if possible
178
+ end
179
+
180
+ def connection_failed_with! nick
181
+ @trying.delete nick
182
+ @timed_out << nick
183
+
184
+ @downloading_lock.synchronize {
185
+ @queued_downloads[nick].each{ |d| d.status = 'timeout' }
186
+ @failed_downloads[nick] ||= []
187
+ @failed_downloads[nick] = @failed_downloads[nick] | @queued_downloads[nick]
188
+ @queued_downloads[nick].clear
189
+ }
190
+
191
+ start_download # This one failed, try the next one
192
+ end
193
+
194
+ def initialize_queues
195
+ @download_slots ||= 4
196
+
197
+ FileUtils.mkdir_p config.download_dir, :mode => 0755
198
+
199
+ @downloading_lock = Mutex.new
200
+
201
+ # Don't use Hash.new{} because this can't be dumped by Marshal
202
+ @queued_downloads = {}
203
+ @current_downloads = {}
204
+ @failed_downloads = {}
205
+ @finished_downloads = {}
206
+ @trying = []
207
+ @timed_out = []
208
+
209
+ @open_download_slots = download_slots
210
+
211
+ subscribe { |type, hash|
212
+ if type == :connection_timeout
213
+ connection_failed_with! hash[:nick] if @trying.include?(hash[:nick])
214
+ elsif type == :hub_disconnected
215
+ exit_download_queue_threads
216
+ elsif type == :hub_connection_opened
217
+ start_download_queue_threads
218
+ end
219
+ }
220
+ end
221
+
222
+ def exit_download_queue_threads
223
+ @download_starter_thread.exit
224
+ @download_removal_thread.exit
225
+ end
226
+
227
+ # Both of these need access to the synchronization lock, so we use
228
+ # separate threads to do these processes.
229
+ def start_download_queue_threads
230
+ @to_download = Queue.new
231
+ @download_starter_thread = Thread.start {
232
+ loop {
233
+ download = @to_download.pop
234
+
235
+ if @timed_out.include? download.nick
236
+ download.status = 'timeout'
237
+ (@failed_downloads[download.nick] ||= []) << download
238
+ else
239
+ (@queued_downloads[download.nick] ||= []) << download
240
+ start_download
241
+ end
242
+ }
243
+ }
244
+
245
+ @to_remove = Queue.new
246
+ @download_removal_thread = Thread.start {
247
+ loop {
248
+ user, file = @to_remove.pop
249
+
250
+ @downloading_lock.synchronize {
251
+ @queued_downloads[user] ||= []
252
+ download = @queued_downloads[user].detect{ |h| h.file == file }
253
+ @queued_downloads[user].delete download unless download.nil?
254
+ }
255
+ }
256
+ }
257
+ end
258
+
259
+ end # Downloads
260
+ end # Supports
261
+ end # Fargo