fargo 0.1.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.
@@ -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