quartz_torrent 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/bin/quartztorrent_download +127 -0
  2. data/bin/quartztorrent_download_curses +841 -0
  3. data/bin/quartztorrent_magnet_from_torrent +32 -0
  4. data/bin/quartztorrent_show_info +62 -0
  5. data/lib/quartz_torrent.rb +2 -0
  6. data/lib/quartz_torrent/bitfield.rb +314 -0
  7. data/lib/quartz_torrent/blockstate.rb +354 -0
  8. data/lib/quartz_torrent/classifiedpeers.rb +95 -0
  9. data/lib/quartz_torrent/extension.rb +37 -0
  10. data/lib/quartz_torrent/filemanager.rb +543 -0
  11. data/lib/quartz_torrent/formatter.rb +92 -0
  12. data/lib/quartz_torrent/httptrackerclient.rb +121 -0
  13. data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
  14. data/lib/quartz_torrent/log.rb +132 -0
  15. data/lib/quartz_torrent/magnet.rb +92 -0
  16. data/lib/quartz_torrent/memprofiler.rb +27 -0
  17. data/lib/quartz_torrent/metainfo.rb +221 -0
  18. data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
  19. data/lib/quartz_torrent/peer.rb +145 -0
  20. data/lib/quartz_torrent/peerclient.rb +1627 -0
  21. data/lib/quartz_torrent/peerholder.rb +123 -0
  22. data/lib/quartz_torrent/peermanager.rb +170 -0
  23. data/lib/quartz_torrent/peermsg.rb +502 -0
  24. data/lib/quartz_torrent/peermsgserialization.rb +102 -0
  25. data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
  26. data/lib/quartz_torrent/rate.rb +58 -0
  27. data/lib/quartz_torrent/ratelimit.rb +48 -0
  28. data/lib/quartz_torrent/reactor.rb +949 -0
  29. data/lib/quartz_torrent/regionmap.rb +124 -0
  30. data/lib/quartz_torrent/semaphore.rb +43 -0
  31. data/lib/quartz_torrent/trackerclient.rb +271 -0
  32. data/lib/quartz_torrent/udptrackerclient.rb +70 -0
  33. data/lib/quartz_torrent/udptrackermsg.rb +250 -0
  34. data/lib/quartz_torrent/util.rb +100 -0
  35. 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