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.
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