fargo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/fargo/client.rb +161 -0
- data/lib/fargo/connection/base.rb +133 -0
- data/lib/fargo/connection/download.rb +302 -0
- data/lib/fargo/connection/hub.rb +120 -0
- data/lib/fargo/connection/search.rb +19 -0
- data/lib/fargo/connection/upload.rb +85 -0
- data/lib/fargo/parser.rb +121 -0
- data/lib/fargo/publisher.rb +26 -0
- data/lib/fargo/search.rb +61 -0
- data/lib/fargo/search_result.rb +31 -0
- data/lib/fargo/server.rb +52 -0
- data/lib/fargo/supports/chat.rb +35 -0
- data/lib/fargo/supports/downloads.rb +261 -0
- data/lib/fargo/supports/file_list.rb +84 -0
- data/lib/fargo/supports/nick_list.rb +71 -0
- data/lib/fargo/supports/persistence.rb +52 -0
- data/lib/fargo/supports/searches.rb +64 -0
- data/lib/fargo/supports/timeout.rb +19 -0
- data/lib/fargo/supports/uploads.rb +16 -0
- data/lib/fargo/utils.rb +35 -0
- data/lib/fargo/version.rb +3 -0
- data/lib/fargo.rb +48 -0
- metadata +127 -0
@@ -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
|
data/lib/fargo/parser.rb
ADDED
@@ -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
|
data/lib/fargo/search.rb
ADDED
@@ -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
|
data/lib/fargo/server.rb
ADDED
@@ -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
|