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,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'getoptlong'
|
3
|
+
require 'quartz_torrent/magnet'
|
4
|
+
require 'quartz_torrent/metainfo'
|
5
|
+
|
6
|
+
include QuartzTorrent
|
7
|
+
|
8
|
+
def help
|
9
|
+
puts "Usage: #{$0} <torrentfile>"
|
10
|
+
puts "Output a magnet link based on the information found in torrentfile (which should be a .torrent file)"
|
11
|
+
puts
|
12
|
+
end
|
13
|
+
|
14
|
+
opts = GetoptLong.new(
|
15
|
+
[ '--help', '-h', GetoptLong::NO_ARGUMENT],
|
16
|
+
)
|
17
|
+
|
18
|
+
opts.each do |opt, arg|
|
19
|
+
if opt == '--help'
|
20
|
+
help
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
torrent = ARGV[0]
|
25
|
+
if ! torrent
|
26
|
+
help
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
|
30
|
+
metainfo = Metainfo.createFromFile(torrent)
|
31
|
+
puts MagnetURI.encodeFromMetainfo(metainfo)
|
32
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'getoptlong'
|
3
|
+
require 'quartz_torrent'
|
4
|
+
|
5
|
+
include QuartzTorrent
|
6
|
+
|
7
|
+
def help
|
8
|
+
puts "Usage: #{$0} <torrentfile or infofile>"
|
9
|
+
puts "Output information about the specified .torrent or .info file"
|
10
|
+
puts
|
11
|
+
end
|
12
|
+
|
13
|
+
opts = GetoptLong.new(
|
14
|
+
[ '--help', '-h', GetoptLong::NO_ARGUMENT],
|
15
|
+
)
|
16
|
+
|
17
|
+
opts.each do |opt, arg|
|
18
|
+
if opt == '--help'
|
19
|
+
help
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
torrent = ARGV[0]
|
24
|
+
if ! torrent
|
25
|
+
help
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
info = nil
|
30
|
+
begin
|
31
|
+
metainfo = Metainfo.createFromFile(torrent)
|
32
|
+
puts "Info Hash: #{bytesToHex(metainfo.infoHash)}"
|
33
|
+
puts "Announce: #{metainfo.announce}" if metainfo.announce
|
34
|
+
if metainfo.announceList
|
35
|
+
puts "Announce list: "
|
36
|
+
metainfo.announceList.each { |e| puts " #{e}" }
|
37
|
+
end
|
38
|
+
puts "Creation date: #{metainfo.creationDate}" if metainfo.creationDate
|
39
|
+
puts "Comment: #{metainfo.comment}" if metainfo.comment
|
40
|
+
puts "Created by: #{metainfo.createdBy}" if metainfo.createdBy
|
41
|
+
puts "Encoding: #{metainfo.encoding}" if metainfo.encoding
|
42
|
+
|
43
|
+
info = metainfo.info
|
44
|
+
rescue
|
45
|
+
# Doesn't seem to be a complete .torrent file. Maybe it is just the info part.
|
46
|
+
File.open(torrent, "r") do |file|
|
47
|
+
bencoded = file.read
|
48
|
+
info = Metainfo::Info.createFromBdecode(bencoded.bdecode)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if info
|
53
|
+
puts "Info:"
|
54
|
+
puts " Name: #{info.name}" if info.name
|
55
|
+
puts " Piece Length: #{info.pieceLen}" if info.pieceLen
|
56
|
+
puts " Size of pieces array: #{info.pieces.size}" if info.pieces
|
57
|
+
puts " Private: #{info.private}" if info.private
|
58
|
+
puts " Files (length/path): "
|
59
|
+
info.files.each do |file|
|
60
|
+
puts " #{file.length} #{file.path}"
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
module QuartzTorrent
|
2
|
+
|
3
|
+
# A bitfield that allows querying and setting individual bits, as well as serializing and unserializing.
|
4
|
+
class Bitfield
|
5
|
+
=begin
|
6
|
+
# Code used to generate lookup table:
|
7
|
+
print "["
|
8
|
+
256.times do |i|
|
9
|
+
# count bits set
|
10
|
+
o = i
|
11
|
+
c = 0
|
12
|
+
8.times do
|
13
|
+
c += 1 if i & 1 > 0
|
14
|
+
i >>= 1
|
15
|
+
end
|
16
|
+
|
17
|
+
print "," if o > 0
|
18
|
+
puts if o > 0 && o % 20 == 0
|
19
|
+
print "#{c}"
|
20
|
+
end
|
21
|
+
puts "]"
|
22
|
+
|
23
|
+
=end
|
24
|
+
|
25
|
+
# Lookup table. The value at index i is the number of bits on in byte with value i.
|
26
|
+
@@bitsSetInByteLookup =
|
27
|
+
[0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,
|
28
|
+
2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,
|
29
|
+
2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,
|
30
|
+
4,5,5,6,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
|
31
|
+
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,
|
32
|
+
3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,
|
33
|
+
4,5,5,6,5,6,6,7,1,2,2,3,2,3,3,4,2,3,3,4,
|
34
|
+
3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
|
35
|
+
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,
|
36
|
+
4,5,5,6,4,5,5,6,5,6,6,7,2,3,3,4,3,4,4,5,
|
37
|
+
3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,
|
38
|
+
5,6,6,7,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
|
39
|
+
4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8]
|
40
|
+
|
41
|
+
|
42
|
+
# Create a bitfield of length 'length' in bits.
|
43
|
+
def initialize(length)
|
44
|
+
@length = length
|
45
|
+
if @length == 0
|
46
|
+
@data = Array.new(0)
|
47
|
+
else
|
48
|
+
@data = Array.new((length-1)/8+1, 0)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Length of the Bitfield in bits.
|
53
|
+
attr_reader :length
|
54
|
+
|
55
|
+
|
56
|
+
# Length of the Bitfield in bytes.
|
57
|
+
def byteLength
|
58
|
+
@data.length
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set the bit at index 'bit' to 1.
|
62
|
+
def set(bit)
|
63
|
+
quotient = bit >> 3
|
64
|
+
remainder = bit & 0x7
|
65
|
+
mask = 0x80 >> remainder
|
66
|
+
|
67
|
+
raise "Bit #{bit} out of range of bitfield with length #{length}" if quotient >= @data.length
|
68
|
+
@data[quotient] |= mask
|
69
|
+
end
|
70
|
+
|
71
|
+
# Clear the bit at index 'bit' to 0.
|
72
|
+
def clear(bit)
|
73
|
+
quotient = bit >> 3
|
74
|
+
remainder = bit & 0x7
|
75
|
+
mask = ~(0x80 >> remainder)
|
76
|
+
|
77
|
+
raise "Bit #{bit} out of range of bitfield with length #{length}" if quotient >= @data.length
|
78
|
+
@data[quotient] &= mask
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns true if the bit is set, false otherwise.
|
82
|
+
def set?(bit)
|
83
|
+
#raise "Bit #{bit} out of range of bitfield with length #{length}" if quotient >= @data.length
|
84
|
+
(@data[bit >> 3] << (bit & 0x7)) & 0x80 > 0
|
85
|
+
end
|
86
|
+
|
87
|
+
# Are all bits in the Bitfield set?
|
88
|
+
def allSet?
|
89
|
+
# Check all but last byte quickly
|
90
|
+
(@data.length-1).times do |i|
|
91
|
+
return false if @data[i] != 0xff
|
92
|
+
end
|
93
|
+
# Check last byte slowly
|
94
|
+
toCheck = @length % 8
|
95
|
+
toCheck = 8 if toCheck == 0
|
96
|
+
((@length-toCheck)..(@length-1)).each do |i|
|
97
|
+
return false if ! set?(i)
|
98
|
+
end
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
# Are all bits in the Bitfield clear?
|
103
|
+
def allClear?
|
104
|
+
# Check all but last byte quickly
|
105
|
+
(@data.length-1).times do |i|
|
106
|
+
return false if @data[i] != 0
|
107
|
+
end
|
108
|
+
# Check last byte slowly
|
109
|
+
toCheck = @length % 8
|
110
|
+
toCheck = 8 if toCheck == 0
|
111
|
+
((@length-toCheck)..(@length-1)).each do |i|
|
112
|
+
return false if set?(i)
|
113
|
+
end
|
114
|
+
true
|
115
|
+
end
|
116
|
+
|
117
|
+
# Set all bits in the field to 1.
|
118
|
+
def setAll
|
119
|
+
@data.fill(0xff)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Clear all bits in the field to 0.
|
123
|
+
def clearAll
|
124
|
+
@data.fill(0x00)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Calculate the union of this bitfield and the passed bitfield, and
|
128
|
+
# return the result as a new bitfield.
|
129
|
+
def union(bitfield)
|
130
|
+
raise "That's not a bitfield" if ! bitfield.is_a?(Bitfield)
|
131
|
+
raise "bitfield lengths must be equal" if ! bitfield.length == length
|
132
|
+
|
133
|
+
result = Bitfield.new(length)
|
134
|
+
(@data.length).times do |i|
|
135
|
+
result.data[i] = @data[i] | bitfield.data[i]
|
136
|
+
end
|
137
|
+
result
|
138
|
+
end
|
139
|
+
|
140
|
+
# Calculate the intersection of this bitfield and the passed bitfield, and
|
141
|
+
# return the result as a new bitfield.
|
142
|
+
def intersection(bitfield)
|
143
|
+
raise "That's not a bitfield" if ! bitfield.is_a?(Bitfield)
|
144
|
+
raise "bitfield lengths must be equal" if ! bitfield.length == length
|
145
|
+
|
146
|
+
newbitfield = Bitfield.new(length)
|
147
|
+
newbitfield.copyFrom(self)
|
148
|
+
newbitfield.intersection!(bitfield)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Update this bitfield to be the intersection of this bitfield and the passed bitfield.
|
152
|
+
def intersection!(bitfield)
|
153
|
+
raise "That's not a bitfield" if ! bitfield.is_a?(Bitfield)
|
154
|
+
raise "bitfield lengths must be equal" if ! bitfield.length == length
|
155
|
+
|
156
|
+
(@data.length).times do |i|
|
157
|
+
@data[i] = @data[i] & bitfield.data[i]
|
158
|
+
end
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
# Set the contents of this bitfield to be the same as the passed bitfield. An exception is
|
163
|
+
# thrown if the passed bitfield is smaller than this.
|
164
|
+
def copyFrom(bitfield)
|
165
|
+
raise "Source bitfield is too small (#{bitfield.length} < #{length})" if bitfield.length < length
|
166
|
+
(@data.length).times do |i|
|
167
|
+
@data[i] = bitfield.data[i]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Calculate the compliment of this bitfield, and
|
172
|
+
# return the result as a new bitfield.
|
173
|
+
def compliment
|
174
|
+
bitfield = Bitfield.new(length)
|
175
|
+
bitfield.copyFrom(self)
|
176
|
+
bitfield.compliment!
|
177
|
+
end
|
178
|
+
|
179
|
+
# Update this bitfield to be the compliment of itself.
|
180
|
+
def compliment!
|
181
|
+
@data.collect!{ |e| ~e }
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
185
|
+
# Serialize this bitfield as a string.
|
186
|
+
def serialize
|
187
|
+
@data.pack "C*"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Unserialize this bitfield from a string.
|
191
|
+
def unserialize(s)
|
192
|
+
@data = s.unpack "C*"
|
193
|
+
end
|
194
|
+
|
195
|
+
# Return a display string representing the bitfield.
|
196
|
+
def to_s(groupsOf = 8)
|
197
|
+
groupsOf = 8 if groupsOf == 0
|
198
|
+
s = ""
|
199
|
+
length.times do |i|
|
200
|
+
s << (set?(i) ? "1" : "0")
|
201
|
+
s << " " if i % groupsOf == 0
|
202
|
+
end
|
203
|
+
s
|
204
|
+
end
|
205
|
+
|
206
|
+
# Count the number of bits that are set.
|
207
|
+
def countSet
|
208
|
+
@data.reduce(0){ |memo, v| memo + @@bitsSetInByteLookup[v] }
|
209
|
+
end
|
210
|
+
|
211
|
+
protected
|
212
|
+
def data
|
213
|
+
@data
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# A bitfield that is always empty.
|
218
|
+
class EmptyBitfield < Bitfield
|
219
|
+
def initialize
|
220
|
+
@length = 0
|
221
|
+
end
|
222
|
+
|
223
|
+
# Length of the Bitfield in bits.
|
224
|
+
attr_reader :length
|
225
|
+
|
226
|
+
# Length of the Bitfield in bytes.
|
227
|
+
def byteLength
|
228
|
+
0
|
229
|
+
end
|
230
|
+
|
231
|
+
# Set the bit at index 'bit' to 1.
|
232
|
+
def set(bit)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Clear the bit at index 'bit' to 0.
|
236
|
+
def clear(bit)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns true if the bit is set, false otherwise.
|
240
|
+
def set?(bit)
|
241
|
+
false
|
242
|
+
end
|
243
|
+
|
244
|
+
# Are all bits in the Bitfield set?
|
245
|
+
def allSet?
|
246
|
+
false
|
247
|
+
end
|
248
|
+
|
249
|
+
# Are all bits in the Bitfield clear?
|
250
|
+
def allClear?
|
251
|
+
true
|
252
|
+
end
|
253
|
+
|
254
|
+
# Set all bits in the field to 1.
|
255
|
+
def setAll
|
256
|
+
end
|
257
|
+
|
258
|
+
# Clear all bits in the field to 0.
|
259
|
+
def clearAll
|
260
|
+
end
|
261
|
+
|
262
|
+
# Calculate the union of this bitfield and the passed bitfield, and
|
263
|
+
# return the result as a new bitfield.
|
264
|
+
def union(bitfield)
|
265
|
+
self
|
266
|
+
end
|
267
|
+
|
268
|
+
# Calculate the intersection of this bitfield and the passed bitfield, and
|
269
|
+
# return the result as a new bitfield.
|
270
|
+
def intersection(bitfield)
|
271
|
+
self
|
272
|
+
end
|
273
|
+
|
274
|
+
# Update this bitfield to be the intersection of this bitfield and the passed bitfield.
|
275
|
+
def intersection!(bitfield)
|
276
|
+
self
|
277
|
+
end
|
278
|
+
|
279
|
+
# Set the contents of this bitfield to be the same as the passed bitfield. An exception is
|
280
|
+
# thrown if the passed bitfield is smaller than this.
|
281
|
+
def copyFrom(bitfield)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Calculate the compliment of this bitfield, and
|
285
|
+
# return the result as a new bitfield.
|
286
|
+
def compliment
|
287
|
+
self
|
288
|
+
end
|
289
|
+
|
290
|
+
# Update this bitfield to be the compliment of itself.
|
291
|
+
def compliment!
|
292
|
+
self
|
293
|
+
end
|
294
|
+
|
295
|
+
# Serialize this bitfield as a string.
|
296
|
+
def serialize
|
297
|
+
""
|
298
|
+
end
|
299
|
+
|
300
|
+
# Unserialize this bitfield from a string.
|
301
|
+
def unserialize(s)
|
302
|
+
end
|
303
|
+
|
304
|
+
# Return a display string representing the bitfield.
|
305
|
+
def to_s(groupsOf = 8)
|
306
|
+
"empty"
|
307
|
+
end
|
308
|
+
|
309
|
+
# Count the number of bits that are set. Slow: could use lookup table.
|
310
|
+
def countSet
|
311
|
+
0
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
@@ -0,0 +1,354 @@
|
|
1
|
+
require 'quartz_torrent/util'
|
2
|
+
require 'quartz_torrent/bitfield'
|
3
|
+
|
4
|
+
module QuartzTorrent
|
5
|
+
|
6
|
+
# Information representing a single block (a part of a piece)
|
7
|
+
class BlockInfo
|
8
|
+
def initialize(pieceIndex, offset, length, peers, blockIndex)
|
9
|
+
@pieceIndex = pieceIndex
|
10
|
+
@offset = offset
|
11
|
+
@length = length
|
12
|
+
@peers = peers
|
13
|
+
@blockIndex = blockIndex
|
14
|
+
end
|
15
|
+
|
16
|
+
# The index of the piece that this block belongs to.
|
17
|
+
attr_accessor :pieceIndex
|
18
|
+
# Offset (in bytes) within the piece where the block begins.
|
19
|
+
attr_accessor :offset
|
20
|
+
# Length of the block in bytes.
|
21
|
+
attr_accessor :length
|
22
|
+
# A list of peers that have the piece this block belongs to. This block
|
23
|
+
# can be requested from these peers.
|
24
|
+
attr_accessor :peers
|
25
|
+
# Index of the block in units of the blockSize from the BlockState that created
|
26
|
+
# this object.
|
27
|
+
attr_accessor :blockIndex
|
28
|
+
|
29
|
+
# Return a new Bittorrent Request message that requests this block.
|
30
|
+
def getRequest
|
31
|
+
m = Request.new
|
32
|
+
m.pieceIndex = @pieceIndex
|
33
|
+
m.blockOffset = @offset
|
34
|
+
m.blockLength = @length
|
35
|
+
m
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# Any given torrent is broken into pieces. Those pieces are broken into blocks.
|
41
|
+
# This class can be used to keep track of which blocks are currently complete, which
|
42
|
+
# have been requested but aren't available yet, and which are missing.
|
43
|
+
#
|
44
|
+
# This class only supports one block size.
|
45
|
+
class BlockState
|
46
|
+
|
47
|
+
# Create a new BlockState. Parameter 'info' should be the Metainfo.info object
|
48
|
+
# for the torrent, 'initialPieceBitfield' should be the already-existing pieces,
|
49
|
+
# and 'blockSize' is the size of blocks.
|
50
|
+
def initialize(info, initialPieceBitfield, blockSize = 16384)
|
51
|
+
raise "Block size cannot be <= 0" if blockSize <= 0
|
52
|
+
|
53
|
+
@logger = LogManager.getLogger("blockstate")
|
54
|
+
|
55
|
+
@numPieces = initialPieceBitfield.length
|
56
|
+
@pieceSize = info.pieceLen
|
57
|
+
@numPieces = info.pieces.length
|
58
|
+
@blocksPerPiece = (@pieceSize/blockSize + (@pieceSize%blockSize == 0 ? 0 : 1))
|
59
|
+
@blockSize = blockSize
|
60
|
+
# When calculating the number of blocks, the last piece is likely a partial piece...
|
61
|
+
@numBlocks = @blocksPerPiece * (@numPieces-1)
|
62
|
+
lastPieceLen = (info.dataLength - (@numPieces-1)*@pieceSize)
|
63
|
+
@numBlocks += lastPieceLen / @blockSize
|
64
|
+
@numBlocks += 1 if lastPieceLen % @blockSize != 0
|
65
|
+
@lastBlockLength = (info.dataLength - (@numBlocks-1)*@blockSize)
|
66
|
+
@totalLength = info.dataLength
|
67
|
+
|
68
|
+
raise "Initial piece bitfield is the wrong length" if initialPieceBitfield.length != @numPieces
|
69
|
+
raise "Piece size is not divisible by block size" if @pieceSize % blockSize != 0
|
70
|
+
|
71
|
+
@completePieces = initialPieceBitfield
|
72
|
+
@completeBlocks = Bitfield.new(@numBlocks)
|
73
|
+
blockIndex = 0
|
74
|
+
initialPieceBitfield.length.times do |pieceIndex|
|
75
|
+
isSet = initialPieceBitfield.set?(pieceIndex)
|
76
|
+
@blocksPerPiece.times do
|
77
|
+
# The last piece may be a smaller number of blocks.
|
78
|
+
break if blockIndex >= @completeBlocks.length
|
79
|
+
|
80
|
+
if isSet
|
81
|
+
@completeBlocks.set blockIndex
|
82
|
+
else
|
83
|
+
@completeBlocks.clear blockIndex
|
84
|
+
end
|
85
|
+
blockIndex += 1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@requestedBlocks = Bitfield.new(@numBlocks)
|
90
|
+
@requestedBlocks.clearAll
|
91
|
+
@currentPieces = []
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get the block size
|
95
|
+
attr_reader :blockSize
|
96
|
+
|
97
|
+
# Total length of the torrent in bytes.
|
98
|
+
attr_reader :totalLength
|
99
|
+
|
100
|
+
# Return a list of BlockInfo objects representing blocjs that should be requested from peers.
|
101
|
+
def findRequestableBlocks(classifiedPeers, numToReturn = nil)
|
102
|
+
# Have a list of the current pieces we are working on. Each time this method is
|
103
|
+
# called, check the blocks in the pieces in list order to find the blocks to return
|
104
|
+
# for requesting. If a piece is completed, remove it from this list. If we need more blocks
|
105
|
+
# than there are available in the list, add more pieces to the end of the list (in rarest-first
|
106
|
+
# order).
|
107
|
+
result = []
|
108
|
+
|
109
|
+
# Update requestable peers to only be those that we can still request pieces from.
|
110
|
+
peersHavingPiece = computePeersHavingPiece(classifiedPeers)
|
111
|
+
requestable = @completeBlocks.union(@requestedBlocks).compliment!
|
112
|
+
rarityOrder = nil
|
113
|
+
|
114
|
+
currentPiece = 0
|
115
|
+
while true
|
116
|
+
if currentPiece >= @currentPieces.length
|
117
|
+
# Add more pieces in rarest-first order. If there are no more pieces, break.
|
118
|
+
rarityOrder = computeRarity(classifiedPeers) if ! rarityOrder
|
119
|
+
added = false
|
120
|
+
rarityOrder.each do |pair|
|
121
|
+
pieceIndex = pair[1]
|
122
|
+
peersWithPiece = peersHavingPiece[pieceIndex]
|
123
|
+
if peersWithPiece && peersWithPiece.size > 0 && !@currentPieces.index(pieceIndex) && ! pieceCompleted?(pieceIndex)
|
124
|
+
@logger.debug "Adding piece #{pieceIndex} to the current downloading list"
|
125
|
+
@currentPieces.push pieceIndex
|
126
|
+
added = true
|
127
|
+
break
|
128
|
+
end
|
129
|
+
end
|
130
|
+
if ! added
|
131
|
+
@logger.debug "There are no more pieces to add to the current downloading list"
|
132
|
+
break
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
currentPieceIndex = @currentPieces[currentPiece]
|
137
|
+
|
138
|
+
if pieceCompleted?(currentPieceIndex)
|
139
|
+
@logger.debug "Piece #{currentPieceIndex} complete so removing it from the current downloading list"
|
140
|
+
@currentPieces.delete_at(currentPiece)
|
141
|
+
next
|
142
|
+
end
|
143
|
+
|
144
|
+
peersWithPiece = peersHavingPiece[currentPieceIndex]
|
145
|
+
if !peersWithPiece || peersWithPiece.size == 0
|
146
|
+
@logger.debug "No peers have piece #{currentPieceIndex}"
|
147
|
+
currentPiece += 1
|
148
|
+
next
|
149
|
+
end
|
150
|
+
|
151
|
+
eachBlockInPiece(currentPieceIndex) do |blockIndex|
|
152
|
+
if requestable.set?(blockIndex)
|
153
|
+
result.push createBlockinfoByPieceAndBlockIndex(currentPieceIndex, peersWithPiece, blockIndex)
|
154
|
+
break if numToReturn && result.size >= numToReturn
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
break if numToReturn && result.size >= numToReturn
|
159
|
+
currentPiece += 1
|
160
|
+
end
|
161
|
+
|
162
|
+
result
|
163
|
+
end
|
164
|
+
|
165
|
+
# Set whether the block represented by the passed BlockInfo is requested or not.
|
166
|
+
def setBlockRequested(blockInfo, bool)
|
167
|
+
if bool
|
168
|
+
@requestedBlocks.set blockInfo.blockIndex
|
169
|
+
else
|
170
|
+
@requestedBlocks.clear blockInfo.blockIndex
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Mark a block as completed. If clearRequested is :clear_requested, then the block is also marked
|
175
|
+
# as no longer requested. If this block completes the piece and a block is passed, the pieceIndex
|
176
|
+
# is yielded to the block.
|
177
|
+
def setBlockCompleted(pieceIndex, blockOffset, bool, clearRequested = :clear_requested)
|
178
|
+
bi = blockIndexFromPieceAndOffset(pieceIndex, blockOffset)
|
179
|
+
@requestedBlocks.clear bi if clearRequested == :clear_requested
|
180
|
+
if bool
|
181
|
+
@completeBlocks.set bi
|
182
|
+
if allBlocksInPieceCompleted?(pieceIndex)
|
183
|
+
yield pieceIndex if block_given?
|
184
|
+
@completePieces.set pieceIndex
|
185
|
+
end
|
186
|
+
else
|
187
|
+
@completeBlocks.clear bi
|
188
|
+
@completePieces.clear pieceIndex
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Is the specified block completed (downloaded)?
|
193
|
+
def blockCompleted?(blockInfo)
|
194
|
+
@completeBlocks.set? blockInfo.blockIndex
|
195
|
+
end
|
196
|
+
|
197
|
+
# Set whether the piece is completed or not.
|
198
|
+
def setPieceCompleted(pieceIndex, bool)
|
199
|
+
eachBlockInPiece(pieceIndex) do |blockIndex|
|
200
|
+
if bool
|
201
|
+
@completeBlocks.set blockIndex
|
202
|
+
else
|
203
|
+
@completeBlocks.clear blockIndex
|
204
|
+
end
|
205
|
+
end
|
206
|
+
if bool
|
207
|
+
@completePieces.set pieceIndex
|
208
|
+
else
|
209
|
+
@completePieces.clear pieceIndex
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Is the specified piece completed (all blocks are downloaded)?
|
214
|
+
def pieceCompleted?(pieceIndex)
|
215
|
+
@completePieces.set? pieceIndex
|
216
|
+
end
|
217
|
+
|
218
|
+
# Get a bitfield representing the completed pieces.
|
219
|
+
def completePieceBitfield
|
220
|
+
result = Bitfield.new(@numPieces)
|
221
|
+
result.copyFrom(@completePieces)
|
222
|
+
result
|
223
|
+
end
|
224
|
+
|
225
|
+
# Number of bytes we have downloaded and verified.
|
226
|
+
def completedLength
|
227
|
+
num = @completeBlocks.countSet
|
228
|
+
# Last block may be smaller
|
229
|
+
extra = 0
|
230
|
+
if @completeBlocks.set?(@completeBlocks.length-1)
|
231
|
+
num -= 1
|
232
|
+
extra = @lastBlockLength
|
233
|
+
end
|
234
|
+
num*@blockSize + extra
|
235
|
+
end
|
236
|
+
|
237
|
+
# Create a new BlockInfo object using the specified information. The peers list is empty.
|
238
|
+
def createBlockinfoByPieceResponse(pieceIndex, offset, length)
|
239
|
+
blockIndex = pieceIndex*@blocksPerPiece + offset/@blockSize
|
240
|
+
raise "offset in piece is not divisible by block size" if offset % @blockSize != 0
|
241
|
+
BlockInfo.new(pieceIndex, offset, length, [], blockIndex)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Create a new BlockInfo object using the specified information. The peers list is empty.
|
245
|
+
def createBlockinfoByBlockIndex(blockIndex)
|
246
|
+
pieceIndex = blockIndex / @blockSize
|
247
|
+
offset = (blockIndex % @blocksPerPiece)*@blockSize
|
248
|
+
length = @blockSize
|
249
|
+
raise "offset in piece is not divisible by block size" if offset % @blockSize != 0
|
250
|
+
BlockInfo.new(pieceIndex, offset, length, [], blockIndex)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Create a new BlockInfo object using the specified information.
|
254
|
+
def createBlockinfoByPieceAndBlockIndex(pieceIndex, peersWithPiece, blockIndex)
|
255
|
+
# If this is the very last block, then it might be smaller than the rest.
|
256
|
+
blockSize = @blockSize
|
257
|
+
blockSize = @lastBlockLength if blockIndex == @numBlocks-1
|
258
|
+
offsetWithinPiece = (blockIndex % @blocksPerPiece)*@blockSize
|
259
|
+
BlockInfo.new(pieceIndex, offsetWithinPiece, blockSize, peersWithPiece, blockIndex)
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
# Yield to a block each block index in a piece.
|
264
|
+
def eachBlockInPiece(pieceIndex)
|
265
|
+
(pieceIndex*@blocksPerPiece).upto(pieceIndex*@blocksPerPiece+@blocksPerPiece-1) do |blockIndex|
|
266
|
+
break if blockIndex >= @numBlocks
|
267
|
+
yield blockIndex
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def allBlocksInPieceCompleted?(pieceIndex)
|
272
|
+
complete = true
|
273
|
+
eachBlockInPiece(pieceIndex) do |blockIndex|
|
274
|
+
if ! @completeBlocks.set?(blockIndex)
|
275
|
+
complete = false
|
276
|
+
break
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
complete
|
281
|
+
end
|
282
|
+
|
283
|
+
def blockIndexFromPieceAndOffset(pieceIndex, blockOffset)
|
284
|
+
pieceIndex*@blocksPerPiece + blockOffset/@blockSize
|
285
|
+
end
|
286
|
+
|
287
|
+
# Return an array indexed by piece index where each element
|
288
|
+
# is a list of peers with that piece.
|
289
|
+
def computePeersHavingPiece(classifiedPeers)
|
290
|
+
# Make a list of each peer having the specified piece
|
291
|
+
peersHavingPiece = Array.new(@numPieces)
|
292
|
+
# This first list represents rarity by number if peers having that piece. 1 = rarest.
|
293
|
+
classifiedPeers.requestablePeers.each do |peer|
|
294
|
+
@numPieces.times do |i|
|
295
|
+
if peer.bitfield.set?(i)
|
296
|
+
if peersHavingPiece[i]
|
297
|
+
peersHavingPiece[i].push peer
|
298
|
+
else
|
299
|
+
peersHavingPiece[i] = [peer]
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
peersHavingPiece
|
305
|
+
end
|
306
|
+
|
307
|
+
# Compute an array representing the relative rarity of each piece of the torrent.
|
308
|
+
# The returned array has one entry for each piece of the torrent. Each entry is a two-element
|
309
|
+
# array where the first element is the rarity of the piece where lower is more rare (i.e. 0 is rarest
|
310
|
+
# and represents 0 peers with that piece), and the second element in the entry is the piece index.
|
311
|
+
# The returned array is sorted in order of ascending rarity value (rarest is first), but within each
|
312
|
+
# class of the same rarity value the piece indices are randomized. For example, rarity 1 elements are
|
313
|
+
# all before rarity 2 elements, but the piece indices with rarity 1 are in a random order.
|
314
|
+
def computeRarity(classifiedPeers)
|
315
|
+
# 1. Make a list of the rarity of pieces.
|
316
|
+
rarity = Array.new(@numPieces,0)
|
317
|
+
# This first list represents rarity by number if peers having that piece. 1 = rarest.
|
318
|
+
classifiedPeers.requestablePeers.each do |peer|
|
319
|
+
@numPieces.times do |i|
|
320
|
+
rarity[i] += 1 if peer.bitfield.set?(i)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# 2. Make a new list that indexes the first list by order of descending rarity.
|
325
|
+
rarityOrder = Array.new(@numPieces)
|
326
|
+
# The elements of this second list are pairs. The first element in the pair is the rarity, second is the piece index.
|
327
|
+
@numPieces.times do |i|
|
328
|
+
rarityOrder[i] = [rarity[i],i]
|
329
|
+
end
|
330
|
+
rarityOrder.sort!{ |a,b| a[0] <=> b[0] }
|
331
|
+
|
332
|
+
# 3. Randomize the list order within classes of the same rarity.
|
333
|
+
left = 0
|
334
|
+
leftVal = rarityOrder[left][0]
|
335
|
+
@numPieces.times do |i|
|
336
|
+
if rarityOrder[i][0] != leftVal
|
337
|
+
# New range
|
338
|
+
rangeLen = i-left+1
|
339
|
+
|
340
|
+
QuartzTorrent.arrayShuffleRange!(rarityOrder, left, rangeLen)
|
341
|
+
|
342
|
+
left = i+1
|
343
|
+
leftVal = rarityOrder[left][0] if left < @numPieces
|
344
|
+
end
|
345
|
+
end
|
346
|
+
QuartzTorrent.arrayShuffleRange!(rarityOrder, left, @numPieces-left) if left < @numPieces
|
347
|
+
|
348
|
+
rarityOrder
|
349
|
+
end
|
350
|
+
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
354
|
+
|