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,27 @@
1
+ module QuartzTorrent
2
+
3
+ # Utility class used for debugging memory leaks. It can be used to count the number of reachable
4
+ # instances of selected classes.
5
+ class MemProfiler
6
+ def initialize
7
+ @classes = []
8
+ end
9
+
10
+ # Add a class to the list of classes we count the reachable instances of.
11
+ def trackClass(clazz)
12
+ @classes.push clazz
13
+ end
14
+
15
+ # Return a hashtable keyed by class where the value is the number of that class of object still reachable.
16
+ def getCounts
17
+ result = {}
18
+ @classes.each do |c|
19
+ count = 0
20
+ ObjectSpace.each_object(c){ count += 1 }
21
+ result[c] = count
22
+ end
23
+ result
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,221 @@
1
+ require 'quartz_torrent/log'
2
+ require 'bencode'
3
+ require 'digest/sha1'
4
+
5
+ module QuartzTorrent
6
+
7
+ # Torrent metainfo structure. This is what's usually found in .torrent files. This class
8
+ # generally follows the structure of the metadata format.
9
+ class Metainfo
10
+
11
+ # If 'v' is null, throw an exception. Otherwise return 'v'.
12
+ def self.valueOrException(v, msg)
13
+ if ! v
14
+ LogManager.getLogger("metainfo").error msg
15
+ raise "Invalid torrent metainfo"
16
+ end
17
+ v
18
+ end
19
+
20
+ # Information about a file contained in the torrent.
21
+ class FileInfo
22
+ def initialize(length = nil, path = nil)
23
+ @length = length
24
+ @path = path
25
+ end
26
+
27
+ # Relative path to the file. For a single-file torrent this is simply the name of the file. For a multi-file torrent,
28
+ # this is the directory names from the torrent and the filename separated by the file separator.
29
+ attr_accessor :path
30
+ # Length of the file.
31
+ attr_accessor :length
32
+
33
+ # Create a FileInfo object from a bdecoded structure.
34
+ def self.createFromBdecode(bdecode)
35
+ result = FileInfo.new
36
+ result.length = Metainfo.valueOrException(bdecode['length'], "Torrent metainfo listed multiple files, and one is missing the length property.")
37
+ path = Metainfo.valueOrException(bdecode['path'], "Torrent metainfo listed multiple files, and one is missing the path property.")
38
+
39
+ result.path = ""
40
+ path.each do |part|
41
+ result.path << File::SEPARATOR if result.path.length > 0
42
+ result.path << part
43
+ end
44
+
45
+ result
46
+ end
47
+ end
48
+
49
+ # The 'info' property of the torrent metainfo.
50
+ class Info
51
+ def initialize
52
+ @pieceLen = nil
53
+ @pieces = nil
54
+ @private = nil
55
+ # Suggested file or directory name
56
+ @name = nil
57
+ # List of file info for files in the torrent. These include the directory name if this is a
58
+ # multi-file download. For a single-file download the
59
+ @files = []
60
+ @logger = LogManager.getLogger("metainfo")
61
+ end
62
+
63
+ # Array of FileInfo objects
64
+ attr_accessor :files
65
+ # Suggested file or directory name
66
+ attr_accessor :name
67
+ # Length of each piece in bytes. The last piece may be shorter than this.
68
+ attr_accessor :pieceLen
69
+ # Array of SHA1 digests of all peices. These digests are in binary format.
70
+ attr_accessor :pieces
71
+ # True if no external peer source is allowed.
72
+ attr_accessor :private
73
+
74
+ # Total length of the torrent data in bytes.
75
+ def dataLength
76
+ files.reduce(0){ |memo,f| memo + f.length}
77
+ end
78
+
79
+ # Create a FileInfo object from a bdecoded structure.
80
+ def self.createFromBdecode(infoDict)
81
+ result = Info.new
82
+ result.pieceLen = infoDict['piece length']
83
+ result.private = infoDict['private']
84
+ result.pieces = parsePieces(Metainfo.valueOrException(infoDict['pieces'], "Torrent metainfo is missing the pieces property."))
85
+ result.name = Metainfo.valueOrException(infoDict['name'], "Torrent metainfo is missing the name property.")
86
+
87
+ if infoDict.has_key? 'files'
88
+ # This is a multi-file torrent
89
+ infoDict['files'].each do |file|
90
+ result.files.push FileInfo.createFromBdecode(file)
91
+ result.files.last.path = result.name + File::SEPARATOR + result.files.last.path
92
+ end
93
+ else
94
+ # This is a single-file torrent
95
+ length = Metainfo.valueOrException(infoDict['length'], "Torrent metainfo listed a single file, but it is missing the length property.")
96
+ result.files.push FileInfo.new(length, result.name)
97
+ end
98
+
99
+ result
100
+ end
101
+
102
+ # BEncode this info and return the result.
103
+ def bencode
104
+ hash = {}
105
+
106
+ raise "Cannot encode Info object with nil pieceLen" if ! @pieceLen
107
+ raise "Cannot encode Info object with nil name" if ! @name
108
+ raise "Cannot encode Info object with nil pieces" if ! @pieces
109
+ raise "Cannot encode Info object with nil files or empty files" if ! @files || @files.empty?
110
+
111
+ hash['piece length'] = @pieceLen
112
+ hash['private'] = @private if @private
113
+ hash['name'] = @name
114
+ hash['pieces'] = @pieces.join
115
+
116
+ if @files.length > 1
117
+ # This is a multi-file torrent
118
+ # When we loaded the torrent, we prepended the 'name' element of the info hash to the path. We need to remove this
119
+ # name element to end up with the same result.
120
+ hash['files'] = @files.collect{ |file| {'length' => file.length, 'path' => file.path.split(File::SEPARATOR).drop(1) } }
121
+ else
122
+ hash['length'] = @files.first.length
123
+ end
124
+ hash.bencode
125
+ end
126
+
127
+ private
128
+ # Parse the pieces of the torrent out of the metainfo.
129
+ def self.parsePieces(p)
130
+ # Break into 20-byte portions.
131
+ if p.length % 20 != 0
132
+ @logger.error "Torrent metainfo contained a pieces property that was not a multiple of 20 bytes long."
133
+ raise "Invalid torrent metainfo"
134
+ end
135
+
136
+ result = []
137
+ index = 0
138
+ while index < p.length
139
+ result.push p[index,20].unpack("a20")[0]
140
+ index += 20
141
+ end
142
+ result
143
+ end
144
+ end
145
+
146
+ def initialize
147
+ @info = nil
148
+ @announce = nil
149
+ @announceList = nil
150
+ @creationDate = nil
151
+ @comment = nil
152
+ @createdBy = nil
153
+ @encoding = nil
154
+ end
155
+
156
+ # A Metainfo::Info object
157
+ attr_accessor :info
158
+
159
+ # A 20-byte SHA1 hash of the value of the info key from the metainfo. This is neede when connecting
160
+ # to the tracker or to a peer.
161
+ attr_accessor :infoHash
162
+
163
+ # Announce URL of the tracker
164
+ attr_accessor :announce
165
+ attr_accessor :announceList
166
+
167
+ # Creation date as a ruby Time object
168
+ attr_accessor :creationDate
169
+ # Comment
170
+ attr_accessor :comment
171
+ # Created By
172
+ attr_accessor :createdBy
173
+ # The string encoding format used to generate the pieces part of the info dictionary in the .torrent metafile
174
+ attr_accessor :encoding
175
+
176
+ # Create a Metainfo object from the passed bencoded string.
177
+ def self.createFromString(data)
178
+ logger = LogManager.getLogger("metainfo")
179
+
180
+ decoded = data.bdecode
181
+ logger.debug "Decoded torrent metainfo: #{decoded.inspect}"
182
+ result = Metainfo.new
183
+ result.createdBy = decoded['created by']
184
+ result.comment = decoded['comment']
185
+ result.creationDate = decoded['creation date']
186
+ if result.creationDate
187
+ if !result.creationDate.is_a?(Integer)
188
+ if result.creationDate =~ /^\d+$/
189
+ result.creationDate = result.creationDate.to_i
190
+ else
191
+ logger.warn "Torrent metainfo contained invalid date: '#{result.creationDate.class}'"
192
+ result.creationDate = nil
193
+ end
194
+ end
195
+
196
+ result.creationDate = Time.at(result.creationDate) if result.creationDate
197
+ end
198
+ result.encoding = decoded['encoding']
199
+ result.announce = decoded['announce'].strip
200
+ result.announceList = decoded['announce-list']
201
+ result.info = Info.createFromBdecode(decoded['info'])
202
+ result.infoHash = Digest::SHA1.digest( decoded['info'].bencode )
203
+
204
+ result
205
+ end
206
+
207
+ # Create a Metainfo object from the passed IO.
208
+ def self.createFromIO(io)
209
+ self.createFromString(io.read)
210
+ end
211
+
212
+ # Create a Metainfo object from the named file.
213
+ def self.createFromFile(path)
214
+ result =
215
+ File.open(path,"rb") do |io|
216
+ result = self.createFromIO(io)
217
+ end
218
+ result
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,265 @@
1
+ require 'quartz_torrent/util'
2
+ require 'quartz_torrent/filemanager'
3
+ require 'quartz_torrent/metainfo'
4
+ require "quartz_torrent/piecemanagerrequestmetadata.rb"
5
+ require "quartz_torrent/peerholder.rb"
6
+ require 'digest/sha1'
7
+ require 'fileutils'
8
+
9
+ # This class is used when we don't have the info struct for the torrent (no .torrent file) and must
10
+ # download it piece by piece from peers. It keeps track of the pieces we have.
11
+ #
12
+ # When a piece is requested from a peer and that peer responds with a reject saying it doesn't have
13
+ # that metainfo piece, we take a simple approach and mark that peer as bad, and don't request any more
14
+ # pieces from that peer even though they may have other pieces. This simplifies the code.
15
+ module QuartzTorrent
16
+ class MetainfoPieceState
17
+ BlockSize = 16384
18
+
19
+ # Create a new MetainfoPieceState that can be used to manage downloading the metainfo
20
+ # for a torrent. The metainfo is stored in a file under baseDirectory named <infohash>.info,
21
+ # where <infohash> is infoHash hex-encoded. The parameter metainfoSize should be the size of
22
+ # the metainfo, and info can be used to pass in the complete metainfo Info object if it is available. This
23
+ # is needed for when other peers request the metainfo from us.
24
+ def initialize(baseDirectory, infoHash, metainfoSize = nil, info = nil)
25
+
26
+ @logger = LogManager.getLogger("metainfo_piece_state")
27
+
28
+ @requestTimeout = 5
29
+ @baseDirectory = baseDirectory
30
+ @infoFileName = MetainfoPieceState.generateInfoFileName(infoHash)
31
+
32
+ path = infoFilePath
33
+
34
+ completed = MetainfoPieceState.downloaded(baseDirectory, infoHash)
35
+ metainfoSize = File.size(path) if ! metainfoSize && completed
36
+
37
+ if !completed && info
38
+ File.open(path, "wb") do |file|
39
+ bencoded = info.bencode
40
+ metainfoSize = bencoded.length
41
+ file.write bencoded
42
+ # Sanity check
43
+ testInfoHash = Digest::SHA1.digest( bencoded )
44
+ raise "The computed infoHash #{QuartzTorrent.bytesToHex(testInfoHash)} doesn't match the original infoHash #{QuartzTorrent.bytesToHex(infoHash)}" if testInfoHash != infoHash
45
+ end
46
+ end
47
+
48
+ raise "Unless the metainfo has already been successfully downloaded or the torrent file is available, the metainfoSize is needed" if ! metainfoSize
49
+
50
+ # We use the PieceManager to manage the pieces of the metainfo file. The PieceManager is designed
51
+ # for the pieces and blocks of actual torrent data, so we need to build a fake metainfo object that
52
+ # describes our one metainfo file itself so that we can store the pieces if it on disk.
53
+ # In this case we map metainfo pieces to 'torrent' pieces, and our blocks are the full length of the
54
+ # metainfo piece.
55
+ torrinfo = Metainfo::Info.new
56
+ torrinfo.pieceLen = BlockSize
57
+ torrinfo.files = []
58
+ torrinfo.files.push Metainfo::FileInfo.new(metainfoSize, @infoFileName)
59
+
60
+
61
+ @pieceManager = PieceManager.new(baseDirectory, torrinfo)
62
+ @pieceManagerRequests = {}
63
+
64
+ @numPieces = metainfoSize/BlockSize
65
+ @numPieces += 1 if (metainfoSize%BlockSize) != 0
66
+ @completePieces = Bitfield.new(@numPieces)
67
+ @completePieces.setAll if info || completed
68
+
69
+ @lastPieceLength = metainfoSize - (@numPieces-1)*BlockSize
70
+
71
+ @badPeers = PeerHolder.new
72
+ @requestedPieces = Bitfield.new(@numPieces)
73
+ @requestedPieces.clearAll
74
+
75
+ @metainfoLength = metainfoSize
76
+
77
+ # Time at which the piece in requestedPiece was requested. Used for timeouts.
78
+ @pieceRequestTime = []
79
+ end
80
+
81
+ # Check if the metainfo has already been downloaded successfully during a previous session.
82
+ # Returns the completed, Metainfo::Info object if it is complete, and nil otherwise.
83
+ def self.downloaded(baseDirectory, infoHash)
84
+ logger = LogManager.getLogger("metainfo_piece_state")
85
+ infoFileName = generateInfoFileName(infoHash)
86
+ path = "#{baseDirectory}#{File::SEPARATOR}#{infoFileName}"
87
+
88
+ result = nil
89
+ if File.exists?(path)
90
+ File.open(path, "rb") do |file|
91
+ bencoded = file.read
92
+ # Sanity check
93
+ testInfoHash = Digest::SHA1.digest( bencoded )
94
+ if testInfoHash == infoHash
95
+ result = Metainfo::Info.createFromBdecode(bencoded.bdecode)
96
+ else
97
+ logger.info "the computed SHA1 hash doesn't match the specified infoHash in #{path}"
98
+ end
99
+ end
100
+ else
101
+ logger.info "the metainfo file #{path} doesn't exist"
102
+ end
103
+ result
104
+ end
105
+
106
+ attr_accessor :infoFileName
107
+ attr_accessor :metainfoLength
108
+
109
+ # Return the number of bytes of the metainfo that we have downloaded so far.
110
+ def metainfoCompletedLength
111
+ num = @completePieces.countSet
112
+ # Last block may be smaller
113
+ extra = 0
114
+ if @completePieces.set?(@completePieces.length-1)
115
+ num -= 1
116
+ extra = @lastPieceLength
117
+ end
118
+ num*BlockSize + extra
119
+ end
120
+
121
+ def pieceCompleted?(pieceIndex)
122
+ @completePieces.set? pieceIndex
123
+ end
124
+
125
+ # Do we have all the pieces of the metadata?
126
+ def complete?
127
+ @completePieces.allSet?
128
+ end
129
+
130
+ # Get the completed metainfo. Raises an exception if it's not yet complete.
131
+ def completedMetainfo
132
+ raise "Metadata is not yet complete" if ! complete?
133
+ end
134
+
135
+ def savePiece(pieceIndex, data)
136
+ id = @pieceManager.writeBlock pieceIndex, 0, data
137
+ @pieceManagerRequests[id] = PieceManagerRequestMetadata.new(:write, pieceIndex)
138
+ id
139
+ end
140
+
141
+ def readPiece(pieceIndex)
142
+ length = BlockSize
143
+ length = @lastPieceLength if pieceIndex == @numPieces - 1
144
+ id = @pieceManager.readBlock pieceIndex, 0, length
145
+ #result = manager.nextResult
146
+ @pieceManagerRequests[id] = PieceManagerRequestMetadata.new(:read, pieceIndex)
147
+ id
148
+ end
149
+
150
+ # Check the results of savePiece and readPiece. This method returns a list
151
+ # of the PieceManager results.
152
+ def checkResults
153
+ results = []
154
+ while true
155
+ result = @pieceManager.nextResult
156
+ break if ! result
157
+
158
+ results.push result
159
+
160
+ metaData = @pieceManagerRequests.delete(result.requestId)
161
+ if ! metaData
162
+ @logger.error "Can't find metadata for PieceManager request #{result.requestId}"
163
+ next
164
+ end
165
+
166
+ if metaData.type == :write
167
+ if result.successful?
168
+ @completePieces.set(metaData.data)
169
+ else
170
+ @requestedPieces.clear(metaData.data)
171
+ @pieceRequestTime[metaData.data] = nil
172
+ @logger.error "Writing metainfo piece failed: #{result.error}"
173
+ end
174
+ elsif metaData.type == :read
175
+ if ! result.successful?
176
+ @logger.error "Reading metainfo piece failed: #{result.error}"
177
+ end
178
+ end
179
+ end
180
+ results
181
+ end
182
+
183
+ def findRequestablePieces
184
+ piecesRequired = []
185
+
186
+ removeOldRequests
187
+
188
+ @numPieces.times do |pieceIndex|
189
+ piecesRequired.push pieceIndex if ! @completePieces.set?(pieceIndex) && ! @requestedPieces.set?(pieceIndex)
190
+ end
191
+
192
+ piecesRequired
193
+ end
194
+
195
+ def findRequestablePeers(classifiedPeers)
196
+ result = []
197
+
198
+ classifiedPeers.establishedPeers.each do |peer|
199
+ result.push peer if ! @badPeers.findByAddr(peer.trackerPeer.ip, peer.trackerPeer.port)
200
+ end
201
+
202
+ result
203
+ end
204
+
205
+ # Set whether the piece with the passed pieceIndex is requested or not.
206
+ def setPieceRequested(pieceIndex, bool)
207
+ if bool
208
+ @requestedPieces.set pieceIndex
209
+ @pieceRequestTime[pieceIndex] = Time.new
210
+ else
211
+ @requestedPieces.clear pieceIndex
212
+ @pieceRequestTime[pieceIndex] = nil
213
+ end
214
+ end
215
+
216
+ def markPeerBad(peer)
217
+ @badPeers.add peer
218
+ end
219
+
220
+ # Flush all pieces to disk
221
+ def flush
222
+ id = @pieceManager.flush
223
+ @pieceManagerRequests[id] = PieceManagerRequestMetadata.new(:flush, nil)
224
+ @pieceManager.wait
225
+ end
226
+
227
+ # Wait for the next a pending request to complete.
228
+ def wait
229
+ @pieceManager.wait
230
+ end
231
+
232
+ def self.generateInfoFileName(infoHash)
233
+ "#{QuartzTorrent.bytesToHex(infoHash)}.info"
234
+ end
235
+
236
+ # Remove the metainfo file
237
+ def remove
238
+ path = infoFilePath
239
+ FileUtils.rm path
240
+ end
241
+
242
+ # Stop the underlying PieceManager.
243
+ def stop
244
+ @pieceManager.stop
245
+ end
246
+
247
+ private
248
+ # Remove any pending requests after a timeout.
249
+ def removeOldRequests
250
+ now = Time.new
251
+ @requestedPieces.length.times do |i|
252
+ if @requestedPieces.set? i
253
+ if now - @pieceRequestTime[i] > @requestTimeout
254
+ @requestedPieces.clear i
255
+ @pieceRequestTime[i] = nil
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ def infoFilePath
262
+ "#{@baseDirectory}#{File::SEPARATOR}#{@infoFileName}"
263
+ end
264
+ end
265
+ end