quartz_torrent 0.0.1
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.
- data/bin/quartztorrent_download +127 -0
- data/bin/quartztorrent_download_curses +841 -0
- data/bin/quartztorrent_magnet_from_torrent +32 -0
- data/bin/quartztorrent_show_info +62 -0
- data/lib/quartz_torrent.rb +2 -0
- data/lib/quartz_torrent/bitfield.rb +314 -0
- data/lib/quartz_torrent/blockstate.rb +354 -0
- data/lib/quartz_torrent/classifiedpeers.rb +95 -0
- data/lib/quartz_torrent/extension.rb +37 -0
- data/lib/quartz_torrent/filemanager.rb +543 -0
- data/lib/quartz_torrent/formatter.rb +92 -0
- data/lib/quartz_torrent/httptrackerclient.rb +121 -0
- data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
- data/lib/quartz_torrent/log.rb +132 -0
- data/lib/quartz_torrent/magnet.rb +92 -0
- data/lib/quartz_torrent/memprofiler.rb +27 -0
- data/lib/quartz_torrent/metainfo.rb +221 -0
- data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
- data/lib/quartz_torrent/peer.rb +145 -0
- data/lib/quartz_torrent/peerclient.rb +1627 -0
- data/lib/quartz_torrent/peerholder.rb +123 -0
- data/lib/quartz_torrent/peermanager.rb +170 -0
- data/lib/quartz_torrent/peermsg.rb +502 -0
- data/lib/quartz_torrent/peermsgserialization.rb +102 -0
- data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
- data/lib/quartz_torrent/rate.rb +58 -0
- data/lib/quartz_torrent/ratelimit.rb +48 -0
- data/lib/quartz_torrent/reactor.rb +949 -0
- data/lib/quartz_torrent/regionmap.rb +124 -0
- data/lib/quartz_torrent/semaphore.rb +43 -0
- data/lib/quartz_torrent/trackerclient.rb +271 -0
- data/lib/quartz_torrent/udptrackerclient.rb +70 -0
- data/lib/quartz_torrent/udptrackermsg.rb +250 -0
- data/lib/quartz_torrent/util.rb +100 -0
- metadata +195 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
module QuartzTorrent
|
2
|
+
# Class that can be used to format different quantities into
|
3
|
+
# human readable strings.
|
4
|
+
class Formatter
|
5
|
+
Kb = 1024
|
6
|
+
Meg = 1024*Kb
|
7
|
+
Gig = 1024*Meg
|
8
|
+
|
9
|
+
# Format a size in bytes.
|
10
|
+
def self.formatSize(size)
|
11
|
+
s = size.to_f
|
12
|
+
if s >= Gig
|
13
|
+
s = "%.2fGB" % (s / Gig)
|
14
|
+
elsif s >= Meg
|
15
|
+
s = "%.2fMB" % (s / Meg)
|
16
|
+
elsif s >= Kb
|
17
|
+
s = "%.2fKB" % (s / Kb)
|
18
|
+
else
|
19
|
+
s = "%.2fB" % s
|
20
|
+
end
|
21
|
+
s
|
22
|
+
end
|
23
|
+
|
24
|
+
# Format a floating point number as a percentage with one decimal place.
|
25
|
+
def self.formatPercent(frac)
|
26
|
+
s = "%.1f" % (frac.to_f*100)
|
27
|
+
s + "%"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Format a speed in bytes per second.
|
31
|
+
def self.formatSpeed(s)
|
32
|
+
Formatter.formatSize(s) + "/s"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Format a duration of time in seconds.
|
36
|
+
def self.formatTime(secs)
|
37
|
+
s = ""
|
38
|
+
time = secs.to_i
|
39
|
+
arr = []
|
40
|
+
conv = [60,60]
|
41
|
+
unit = ["s","m","h"]
|
42
|
+
conv.each{ |c|
|
43
|
+
v = time % c
|
44
|
+
time = time / c
|
45
|
+
arr.push v
|
46
|
+
}
|
47
|
+
arr.push time
|
48
|
+
i = unit.size-1
|
49
|
+
arr.reverse.each{ |v|
|
50
|
+
if v == 0
|
51
|
+
i -= 1
|
52
|
+
else
|
53
|
+
break
|
54
|
+
end
|
55
|
+
}
|
56
|
+
while i >= 0
|
57
|
+
s << arr[i].to_s + unit[i]
|
58
|
+
i -= 1
|
59
|
+
end
|
60
|
+
|
61
|
+
s = "0s" if s.length == 0
|
62
|
+
|
63
|
+
s
|
64
|
+
end
|
65
|
+
|
66
|
+
# Parse a size in the format '50 KB'
|
67
|
+
def self.parseSize(size)
|
68
|
+
if size =~ /(\d+(?:\.\d+)?)\s*([^\d]+)*/
|
69
|
+
value = $1.to_f
|
70
|
+
suffix = ($2 ? $2.downcase : nil)
|
71
|
+
|
72
|
+
multiplicand = 1
|
73
|
+
if suffix.nil? || suffix[0] == 'b'
|
74
|
+
multiplicand = 1
|
75
|
+
elsif suffix[0,2] == 'kb'
|
76
|
+
multiplicand = Kb
|
77
|
+
elsif suffix[0,2] == 'mb'
|
78
|
+
multiplicand = Meg
|
79
|
+
elsif suffix[0,2] == 'gb'
|
80
|
+
multiplicand = Gig
|
81
|
+
else
|
82
|
+
raise "Unknown suffix '#{suffix}' for size '#{size}'"
|
83
|
+
end
|
84
|
+
|
85
|
+
value*multiplicand
|
86
|
+
else
|
87
|
+
raise "Malformed size '#{size}'"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module QuartzTorrent
|
2
|
+
class TrackerClient
|
3
|
+
end
|
4
|
+
|
5
|
+
# A tracker client that uses the HTTP protocol. This is the classic BitTorrent tracker protocol.
|
6
|
+
class HttpTrackerClient < TrackerClient
|
7
|
+
def initialize(announceUrl, infoHash, dataLength)
|
8
|
+
super(announceUrl, infoHash, dataLength)
|
9
|
+
@startSent = false
|
10
|
+
@logger = LogManager.getLogger("http_tracker_client")
|
11
|
+
end
|
12
|
+
|
13
|
+
# Request a list of peers from the tracker and return it as a TrackerResponse.
|
14
|
+
# Event, if specified, may be set to :started, :stopped, or :completed.
|
15
|
+
# This is used to notify the tracker that this is the first request,
|
16
|
+
# that we are shutting down, or that we have the full torrent respectively.
|
17
|
+
# Not specifying the event just means this is a regular poll.
|
18
|
+
#def getPeers(event = nil)
|
19
|
+
def request(event = nil)
|
20
|
+
|
21
|
+
uri = URI(@announceUrl)
|
22
|
+
|
23
|
+
dynamicParams = @dynamicRequestParamsBuilder.call
|
24
|
+
|
25
|
+
params = {}
|
26
|
+
params['info_hash'] = CGI.escape(@infoHash)
|
27
|
+
params['peer_id'] = @peerId
|
28
|
+
params['port'] = @port
|
29
|
+
params['uploaded'] = dynamicParams.uploaded.to_s
|
30
|
+
params['downloaded'] = dynamicParams.downloaded.to_s
|
31
|
+
params['left'] = dynamicParams.left.to_s
|
32
|
+
params['compact'] = "1"
|
33
|
+
params['no_peer_id'] = "1"
|
34
|
+
if ! @startSent
|
35
|
+
event = :started
|
36
|
+
@startSent = true
|
37
|
+
end
|
38
|
+
params['event'] = event.to_s if event
|
39
|
+
|
40
|
+
|
41
|
+
@logger.debug "Request parameters: "
|
42
|
+
params.each do |k,v|
|
43
|
+
@logger.debug " #{k}: #{v}"
|
44
|
+
end
|
45
|
+
|
46
|
+
query = ""
|
47
|
+
params.each do |k,v|
|
48
|
+
query << "&" if query.length > 0
|
49
|
+
query << "#{k}=#{v}"
|
50
|
+
end
|
51
|
+
uri.query = query
|
52
|
+
|
53
|
+
res = Net::HTTP.get_response(uri)
|
54
|
+
@logger.debug "Tracker response code: #{res.code}"
|
55
|
+
@logger.debug "Tracker response body: #{res.body}"
|
56
|
+
result = buildTrackerResponse(res)
|
57
|
+
@logger.debug "TrackerResponse: #{result.inspect}"
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
def decodePeers(peersProp)
|
63
|
+
peers = []
|
64
|
+
if peersProp.is_a?(String)
|
65
|
+
# Compact format: 4byte IP followed by 2byte port, in network byte order
|
66
|
+
index = 0
|
67
|
+
while index + 6 <= peersProp.length
|
68
|
+
ip = peersProp[index,4].unpack("CCCC").join('.')
|
69
|
+
port = peersProp[index+4,2].unpack("n").first
|
70
|
+
peers.push TrackerPeer.new(ip, port)
|
71
|
+
index += 6
|
72
|
+
end
|
73
|
+
else
|
74
|
+
# Non-compact format
|
75
|
+
peersProp.each do |peer|
|
76
|
+
ip = peer['ip']
|
77
|
+
port = peer['port']
|
78
|
+
if ip && port
|
79
|
+
peers.push TrackerPeer.new(ip, port)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
#raise "Non-compact peer format not implemented"
|
83
|
+
end
|
84
|
+
peers
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def buildTrackerResponse(netHttpResponse)
|
89
|
+
error = nil
|
90
|
+
peers = []
|
91
|
+
interval = nil
|
92
|
+
success = netHttpResponse.code.to_i >= 200 && netHttpResponse.code.to_i < 300
|
93
|
+
if success
|
94
|
+
begin
|
95
|
+
decoded = netHttpResponse.body.bdecode
|
96
|
+
rescue
|
97
|
+
error = "Tracker netHttpResponse body was not a valid bencoded string"
|
98
|
+
success = false
|
99
|
+
return self
|
100
|
+
end
|
101
|
+
|
102
|
+
if decoded.has_key? 'peers'
|
103
|
+
peers = decodePeers(decoded['peers'])
|
104
|
+
else
|
105
|
+
error = "Tracker netHttpResponse didn't contain a peers property"
|
106
|
+
success = false
|
107
|
+
end
|
108
|
+
|
109
|
+
if decoded.has_key? 'interval'
|
110
|
+
interval = decoded['interval'].to_i
|
111
|
+
end
|
112
|
+
else
|
113
|
+
error = netHttpResponse.body
|
114
|
+
end
|
115
|
+
|
116
|
+
result = TrackerResponse.new(success, error, peers)
|
117
|
+
result.interval = interval if interval
|
118
|
+
result
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module QuartzTorrent
|
4
|
+
# Class that implements a sleep for a specified number of seconds that can be interrupted.
|
5
|
+
# When a caller calls sleep, another thread can call wake to wake the sleeper.
|
6
|
+
class InterruptibleSleep
|
7
|
+
def initialize
|
8
|
+
@eventRead, @eventWrite = IO.pipe
|
9
|
+
@eventPending = false
|
10
|
+
@mutex = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
# Sleep.
|
14
|
+
def sleep(seconds)
|
15
|
+
if IO.select([@eventRead], nil, nil, seconds)
|
16
|
+
@eventRead.read(1)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Wake the sleeper.
|
21
|
+
def wake
|
22
|
+
@mutex.synchronize do
|
23
|
+
@eventWrite.print "X" if ! @eventPending
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'log4r'
|
2
|
+
|
3
|
+
# For some reason the Log4r log level constants (ERROR, etc) are not defined until we
|
4
|
+
# create at least one logger. Here I create one unused one so that the constants exist.
|
5
|
+
Log4r::Logger.new '__init'
|
6
|
+
|
7
|
+
module QuartzTorrent
|
8
|
+
# Class used to control logging.
|
9
|
+
class LogManager
|
10
|
+
|
11
|
+
@@outputter = nil
|
12
|
+
@@defaultLevel = Log4r::ERROR
|
13
|
+
@@maxOldLogs = 10
|
14
|
+
@@maxLogSize = 1048576
|
15
|
+
@@dest = nil
|
16
|
+
|
17
|
+
# Initialize logging based on environment variables. The QUARTZ_TORRENT_LOGFILE variable controls where logging is written,
|
18
|
+
# and should be either a file path, 'stdout' or 'stderr'.
|
19
|
+
def self.initializeFromEnv
|
20
|
+
@@dest = ENV['QUARTZ_TORRENT_LOGFILE']
|
21
|
+
@@defaultLevel = parseLogLevel(ENV['QUARTZ_TORRENT_LOGLEVEL'])
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
# Initialize the log manager. This method expects a block, and the block may call the following methods:
|
26
|
+
#
|
27
|
+
# setLogfile(path)
|
28
|
+
# setDefaultLevel(level)
|
29
|
+
# setMaxOldLogs(num)
|
30
|
+
# setMaxLogSize(size)
|
31
|
+
#
|
32
|
+
# In the above methods, `path` defines where logging is written, and should be either a file path, 'stdout' or 'stderr';
|
33
|
+
# `level` is a logging level as per setLevel, `num` is an integer, and `size` is a value in bytes.
|
34
|
+
def self.setup(&block)
|
35
|
+
self.instance_eval &block
|
36
|
+
|
37
|
+
dest = @@dest
|
38
|
+
if dest
|
39
|
+
if dest.downcase == 'stdout'
|
40
|
+
dest = Log4r::Outputter.stdout
|
41
|
+
elsif dest.downcase == 'stderr'
|
42
|
+
dest = Log4r::Outputter.stderr
|
43
|
+
else
|
44
|
+
dest = Log4r::RollingFileOutputter.new('outputter', {filename: dest, maxsize: @@maxLogSize, max_backups: @@maxOldLogs})
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@@outputter = dest
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set log level for the named logger. The level can be one of
|
51
|
+
# fatal, error, warn, info, or debug as a string or symbol.
|
52
|
+
def self.setLevel(name, level)
|
53
|
+
level = parseLogLevel(level)
|
54
|
+
logger = LogManager.getLogger(name)
|
55
|
+
logger.level = level
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get the logger with the specified name. Currently this returns a log4r Logger.
|
59
|
+
def self.getLogger(name)
|
60
|
+
if ! @@outputter
|
61
|
+
Log4r::Logger.root
|
62
|
+
else
|
63
|
+
logger = Log4r::Logger[name]
|
64
|
+
if ! logger
|
65
|
+
logger = Log4r::Logger.new name
|
66
|
+
logger.level = @@defaultLevel
|
67
|
+
logger.outputters = @@outputter
|
68
|
+
end
|
69
|
+
logger
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
# DSL method used by setup.
|
75
|
+
# Set the logfile.
|
76
|
+
def self.setLogfile(dest)
|
77
|
+
@@dest = dest
|
78
|
+
end
|
79
|
+
# DSL method used by setup.
|
80
|
+
# Set the default log level.
|
81
|
+
def self.setDefaultLevel(level)
|
82
|
+
@@defaultLevel = parseLogLevel(level)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Set number of old log files to keep when rotating.
|
86
|
+
def self.setMaxOldLogs(num)
|
87
|
+
@@maxOldLogs = num
|
88
|
+
end
|
89
|
+
|
90
|
+
# Max size of a single logfile in bytes
|
91
|
+
def self.setMaxLogSize(size)
|
92
|
+
@@maxLogSize = size
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.parseLogLevel(level)
|
96
|
+
if level
|
97
|
+
if level.is_a? Symbol
|
98
|
+
if level == :fatal
|
99
|
+
level = Log4r::FATAL
|
100
|
+
elsif level == :error
|
101
|
+
level = Log4r::ERROR
|
102
|
+
elsif level == :warn
|
103
|
+
level = Log4r::WARN
|
104
|
+
elsif level == :info
|
105
|
+
level = Log4r::INFO
|
106
|
+
elsif level == :debug
|
107
|
+
level = Log4r::DEBUG
|
108
|
+
else
|
109
|
+
level = Log4r::ERROR
|
110
|
+
end
|
111
|
+
else
|
112
|
+
if level.downcase == 'fatal'
|
113
|
+
level = Log4r::FATAL
|
114
|
+
elsif level.downcase == 'error'
|
115
|
+
level = Log4r::ERROR
|
116
|
+
elsif level.downcase == 'warn'
|
117
|
+
level = Log4r::WARN
|
118
|
+
elsif level.downcase == 'info'
|
119
|
+
level = Log4r::INFO
|
120
|
+
elsif level.downcase == 'debug'
|
121
|
+
level = Log4r::DEBUG
|
122
|
+
else
|
123
|
+
level = Log4r::ERROR
|
124
|
+
end
|
125
|
+
end
|
126
|
+
else
|
127
|
+
level = Log4r::ERROR
|
128
|
+
end
|
129
|
+
level
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'quartz_torrent/util'
|
2
|
+
require 'uri'
|
3
|
+
require 'base32'
|
4
|
+
|
5
|
+
module QuartzTorrent
|
6
|
+
class MagnetURI
|
7
|
+
@@regex = /magnet:\?(.*)/
|
8
|
+
|
9
|
+
# Create a new MagnetURI object given a magnet URI string.
|
10
|
+
def initialize(str)
|
11
|
+
@params = {}
|
12
|
+
@raw = str
|
13
|
+
|
14
|
+
if str =~ @@regex
|
15
|
+
parseQuery $1
|
16
|
+
else
|
17
|
+
raise "Not a magnet URI"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :raw
|
22
|
+
|
23
|
+
def self.magnetURI?(str)
|
24
|
+
str =~ @@regex
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return the value of the specified key from the magnet URI.
|
28
|
+
def [](key)
|
29
|
+
@params[key]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return the first Bittorrent info hash found in the magnet URI. The returned
|
33
|
+
# info hash is in binary format.
|
34
|
+
def btInfoHash
|
35
|
+
result = nil
|
36
|
+
@params['xt'].each do |topic|
|
37
|
+
if topic =~ /urn:btih:(.*)/
|
38
|
+
hash = $1
|
39
|
+
if hash.length == 40
|
40
|
+
# Hex-encoded info hash. Convert to binary.
|
41
|
+
result = [hash].pack "H*"
|
42
|
+
else
|
43
|
+
# Base32 encoded
|
44
|
+
result = Base32.decode hash
|
45
|
+
end
|
46
|
+
break
|
47
|
+
end
|
48
|
+
end
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
# Return the first tracker URL found in the magnet link. Returns nil if the magnet has no tracker info.
|
53
|
+
def tracker
|
54
|
+
tr = @params['tr']
|
55
|
+
if tr
|
56
|
+
tr.first
|
57
|
+
else
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return the first display name found in the magnet link. Returns nil if the magnet has no display name.
|
63
|
+
def displayName
|
64
|
+
dn = @params['dn']
|
65
|
+
if dn
|
66
|
+
dn.first
|
67
|
+
else
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Create a magnet URI string given the metainfo from a torrent file.
|
73
|
+
def self.encodeFromMetainfo(metainfo)
|
74
|
+
s = "magnet:?xt=urn:btih:"
|
75
|
+
s << metainfo.infoHash.unpack("H*").first
|
76
|
+
s << "&tr="
|
77
|
+
s << metainfo.announce
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def parseQuery(query)
|
82
|
+
query.split('&').each do |part|
|
83
|
+
if part =~ /(.*)=(.*)/
|
84
|
+
name = $1
|
85
|
+
val = $2
|
86
|
+
name = $1 if name =~ /(.*).\d+$/
|
87
|
+
@params.pushToList name, URI.unescape(val).tr('+',' ')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|