quartz_torrent 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Jeffrey A. Williams
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  QuartzTorrent -- A Ruby Bittorrent Library
2
2
  ==========================================
3
3
 
4
+ [![Gem Version](https://badge.fury.io/rb/quartz_torrent.png)](http://badge.fury.io/rb/quartz\_torrent)
5
+
4
6
  Like the title says, a bittorrent library implemented in pure ruby. Currently
5
7
  the library works, but is still alpha.
6
8
 
@@ -52,6 +52,17 @@ puts "]"
52
52
  # Length of the Bitfield in bits.
53
53
  attr_reader :length
54
54
 
55
+ # Adjust the length of the bitfield. This might be necessary if we had to unserialize the bitfield from
56
+ # a serialized array of bytes.
57
+ def length=(l)
58
+ byteLen = 0
59
+ byteLen = (l-1)/8+1 if l > 0
60
+
61
+ raise "Length adjustment would change size of underlying array" if byteLen != byteLength
62
+
63
+ @length = l
64
+ end
65
+
55
66
 
56
67
  # Length of the Bitfield in bytes.
57
68
  def byteLength
@@ -1,13 +1,15 @@
1
1
  require 'quartz_torrent/peermsg'
2
2
  module QuartzTorrent
3
- # This class contains constants that represent our numbering of the Bittorrent extensions we support.
3
+ # This class contains constants that represent our numbering of the Bittorrent peer-protocol extensions we support.
4
4
  # It also has some utility methods related to extensions.
5
5
  class Extension
6
6
 
7
+ # The metadata extension (BEP 9)
7
8
  MetadataExtensionId = 1
8
9
 
9
- # Parameter info should be the metadata info struct. It is used to determine the size to send
10
- # when negotiating the metadata extension.
10
+ # Create an ExtendedHandshake object based on the passed Torrent metadata info struct.
11
+ # @param info The torrent metadata info struct. It is used to determine the size to send
12
+ # when negotiating the metadata extension.
11
13
  def self.createExtendedHandshake(info)
12
14
  msg = ExtendedHandshake.new
13
15
 
@@ -26,6 +28,8 @@ module QuartzTorrent
26
28
  msg
27
29
  end
28
30
 
31
+ # Get the class to use to serialize and unserialize the specified Bittorent extension. Returns nil if we don't support that extension.
32
+ # @param info The name of a bittorrent extension as specified in the BEP, for example 'ut_metadata'.
29
33
  def self.peerMsgClassForExtensionName(info)
30
34
  if info == 'ut_metadata'
31
35
  ExtendedMetaInfo
@@ -2,8 +2,11 @@ module QuartzTorrent
2
2
  # Class that can be used to format different quantities into
3
3
  # human readable strings.
4
4
  class Formatter
5
+ # Number of bytes in a Kilobyte.
5
6
  Kb = 1024
7
+ # Number of bytes in a Megabyte.
6
8
  Meg = 1024*Kb
9
+ # Number of bytes in a Gigabyte.
7
10
  Gig = 1024*Meg
8
11
 
9
12
  # Format a size in bytes.
@@ -20,6 +20,7 @@ module QuartzTorrent
20
20
 
21
21
  attr_reader :raw
22
22
 
23
+ # Returns true if the passed string is a magnet URI, false otherwise.
23
24
  def self.magnetURI?(str)
24
25
  str =~ @@regex
25
26
  end
@@ -118,6 +118,7 @@ module QuartzTorrent
118
118
  num*BlockSize + extra
119
119
  end
120
120
 
121
+ # Return true if the specified piece is completed. The piece is specified by index.
121
122
  def pieceCompleted?(pieceIndex)
122
123
  @completePieces.set? pieceIndex
123
124
  end
@@ -132,12 +133,15 @@ module QuartzTorrent
132
133
  raise "Metadata is not yet complete" if ! complete?
133
134
  end
134
135
 
136
+ # Save the specified piece to disk asynchronously.
135
137
  def savePiece(pieceIndex, data)
136
138
  id = @pieceManager.writeBlock pieceIndex, 0, data
137
139
  @pieceManagerRequests[id] = PieceManagerRequestMetadata.new(:write, pieceIndex)
138
140
  id
139
141
  end
140
142
 
143
+ # Read a piece from disk. This method is asynchronous; it returns a handle that can be later
144
+ # used to retreive the result.
141
145
  def readPiece(pieceIndex)
142
146
  length = BlockSize
143
147
  length = @lastPieceLength if pieceIndex == @numPieces - 1
@@ -180,6 +184,7 @@ module QuartzTorrent
180
184
  results
181
185
  end
182
186
 
187
+ # Return a list of torrent pieces that can still be requested. These are pieces that are not completed and are not requested.
183
188
  def findRequestablePieces
184
189
  piecesRequired = []
185
190
 
@@ -192,6 +197,8 @@ module QuartzTorrent
192
197
  piecesRequired
193
198
  end
194
199
 
200
+ # Return a list of peers from whom we can request pieces. These are peers for whom we have an established connection, and
201
+ # are not marked as bad. See markPeerBad.
195
202
  def findRequestablePeers(classifiedPeers)
196
203
  result = []
197
204
 
@@ -213,6 +220,8 @@ module QuartzTorrent
213
220
  end
214
221
  end
215
222
 
223
+ # Mark the specified peer as 'bad'. We won't try requesting pieces from this peer. Used, for example, when
224
+ # a peer rejects our request for a metadata piece.
216
225
  def markPeerBad(peer)
217
226
  @badPeers.add peer
218
227
  end
@@ -229,6 +238,7 @@ module QuartzTorrent
229
238
  @pieceManager.wait
230
239
  end
231
240
 
241
+ # Return the name of the file where this class will store the Torrent Info struct.
232
242
  def self.generateInfoFileName(infoHash)
233
243
  "#{QuartzTorrent.bytesToHex(infoHash)}.info"
234
244
  end
@@ -2,9 +2,11 @@ require 'quartz_torrent/rate'
2
2
  require 'quartz_torrent/peermsgserialization'
3
3
 
4
4
  module QuartzTorrent
5
+ # This class represents a torrent peer.
5
6
  class Peer
6
7
  @@stateChangeListeners = []
7
8
 
9
+ # Create a new Peer using the information from the passed TrackerPeer object.
8
10
  def initialize(trackerPeer)
9
11
  @trackerPeer = trackerPeer
10
12
  @amChoked = true
@@ -86,6 +88,7 @@ module QuartzTorrent
86
88
  # A PeerWireMessageSerializer that can unserialize and serialize messages to and from this peer.
87
89
  attr_accessor :peerMsgSerializer
88
90
 
91
+ # Return a string representation of the peer.
89
92
  def to_s
90
93
  @trackerPeer.to_s
91
94
  end
@@ -95,6 +98,7 @@ module QuartzTorrent
95
98
  @@stateChangeListeners.push l
96
99
  end
97
100
 
101
+ # Equate peers.
98
102
  def eql?(o)
99
103
  o.is_a?(Peer) && trackerPeer.eql?(o.trackerPeer)
100
104
  end
@@ -4,7 +4,6 @@ require "quartz_torrent/peermsg.rb"
4
4
  require "quartz_torrent/reactor.rb"
5
5
  require "quartz_torrent/util.rb"
6
6
  require "quartz_torrent/classifiedpeers.rb"
7
- require "quartz_torrent/classifiedpeers.rb"
8
7
  require "quartz_torrent/peerholder.rb"
9
8
  require "quartz_torrent/peermanager.rb"
10
9
  require "quartz_torrent/blockstate.rb"
@@ -38,6 +37,8 @@ module QuartzTorrent
38
37
  @peerManager = PeerManager.new
39
38
  @pieceManagerRequestMetadata = {}
40
39
  @pieceManagerMetainfoRequestMetadata = {}
40
+ @bytesDownloadedDataOnly = 0
41
+ @bytesUploadedDataOnly = 0
41
42
  @bytesDownloaded = 0
42
43
  @bytesUploaded = 0
43
44
  @magnet = nil
@@ -72,6 +73,8 @@ module QuartzTorrent
72
73
  # Metadata associated with outstanding requests to the PieceManager responsible for the pieces of the torrent metainfo.
73
74
  attr_accessor :pieceManagerMetainfoRequestMetadata
74
75
  attr_accessor :peerChangeListener
76
+ attr_accessor :bytesDownloadedDataOnly
77
+ attr_accessor :bytesUploadedDataOnly
75
78
  attr_accessor :bytesDownloaded
76
79
  attr_accessor :bytesUploaded
77
80
  attr_accessor :state
@@ -143,6 +146,10 @@ module QuartzTorrent
143
146
  # After we have completed downloading a torrent, we will continue to upload until we have
144
147
  # uploaded ratio * torrent_size bytes. If nil, no limit on upload.
145
148
  attr_accessor :ratio
149
+ attr_accessor :bytesUploadedDataOnly
150
+ attr_accessor :bytesDownloadedDataOnly
151
+ attr_accessor :bytesUploaded
152
+ attr_accessor :bytesDownloaded
146
153
 
147
154
  # Update the data in this TorrentDataDelegate from the torrentData
148
155
  # object that it was created from. TODO: What if that torrentData is now gone?
@@ -160,6 +167,8 @@ module QuartzTorrent
160
167
  def fillFrom(torrentData)
161
168
  @infoHash = torrentData.infoHash
162
169
  @info = torrentData.info
170
+ @bytesUploadedDataOnly = torrentData.bytesUploadedDataOnly
171
+ @bytesDownloadedDataOnly = torrentData.bytesDownloadedDataOnly
163
172
  @bytesUploaded = torrentData.bytesUploaded
164
173
  @bytesDownloaded = torrentData.bytesDownloaded
165
174
 
@@ -296,7 +305,7 @@ module QuartzTorrent
296
305
  @logger.warn "Asked to set download rate limit for a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
297
306
  return
298
307
  end
299
-
308
+
300
309
  if bytesPerSecond
301
310
  if ! torrentData.downRateLimit
302
311
  torrentData.downRateLimit = RateLimit.new(bytesPerSecond, 2*bytesPerSecond, 0)
@@ -306,6 +315,13 @@ module QuartzTorrent
306
315
  else
307
316
  torrentData.downRateLimit = nil
308
317
  end
318
+
319
+ torrentData.peers.all.each do |peer|
320
+ withPeersIo(peer, "setting download rate limit") do |io|
321
+ io.readRateLimit = torrentData.downRateLimit
322
+ end
323
+ end
324
+
309
325
  end
310
326
 
311
327
  # Set the upload rate limit. Pass nil as the bytesPerSecond to disable the limit.
@@ -325,6 +341,12 @@ module QuartzTorrent
325
341
  else
326
342
  torrentData.upRateLimit = nil
327
343
  end
344
+
345
+ torrentData.peers.all.each do |peer|
346
+ withPeersIo(peer, "setting upload rate limit") do |io|
347
+ io.writeRateLimit = torrentData.upRateLimit
348
+ end
349
+ end
328
350
  end
329
351
 
330
352
  # Set the upload ratio. Pass nil to disable
@@ -503,7 +525,10 @@ module QuartzTorrent
503
525
  close
504
526
  return
505
527
  end
528
+
506
529
  peer.updateUploadRate msg
530
+ torrentData = @torrentData[peer.infoHash]
531
+ torrentData.bytesDownloaded += msg.length if torrentData
507
532
  @logger.debug "Peer #{peer} upload rate: #{peer.uploadRate.value} data only: #{peer.uploadRateDataOnly.value}"
508
533
  end
509
534
 
@@ -801,7 +826,7 @@ module QuartzTorrent
801
826
  end
802
827
 
803
828
  if torrentData.state == :uploading && (torrentData.state != :paused) && torrentData.ratio
804
- if torrentData.bytesUploaded >= torrentData.ratio*torrentData.blockState.totalLength
829
+ if torrentData.bytesUploadedDataOnly >= torrentData.ratio*torrentData.blockState.totalLength
805
830
  @logger.info "Pausing torrent due to upload ratio limit." if torrentData.metainfoPieceState.complete?
806
831
  setPaused(infoHash, true)
807
832
  return
@@ -978,7 +1003,7 @@ module QuartzTorrent
978
1003
  peer.requestedBlocks.delete blockInfo.blockIndex
979
1004
  # Block is marked as not requested when hash is confirmed
980
1005
 
981
- torrentData.bytesDownloaded += msg.data.length
1006
+ torrentData.bytesDownloadedDataOnly += msg.data.length
982
1007
  id = torrentData.pieceManager.writeBlock(msg.pieceIndex, msg.blockOffset, msg.data)
983
1008
  torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:write, msg)
984
1009
  end
@@ -1011,6 +1036,7 @@ module QuartzTorrent
1011
1036
  end
1012
1037
 
1013
1038
  peer.bitfield = msg.bitfield
1039
+ peer.bitfield.length = torrentData.info.pieces.length
1014
1040
 
1015
1041
  if ! torrentData.blockState
1016
1042
  @logger.warn "Bitfield: no blockstate yet."
@@ -1100,8 +1126,9 @@ module QuartzTorrent
1100
1126
  msg.pieceIndex = readRequestMetadata.requestMsg.pieceIndex
1101
1127
  msg.blockOffset = readRequestMetadata.requestMsg.blockOffset
1102
1128
  msg.data = result.data
1129
+ @logger.debug "Sending block to #{peer}: piece #{msg.pieceIndex} offset #{msg.blockOffset} length #{msg.data.length}"
1103
1130
  sendMessageToPeer msg, io, peer
1104
- torrentData.bytesUploaded += msg.data.length
1131
+ torrentData.bytesUploadedDataOnly += msg.data.length
1105
1132
  @logger.debug "Sending piece to peer"
1106
1133
  end
1107
1134
  else
@@ -1313,12 +1340,14 @@ module QuartzTorrent
1313
1340
 
1314
1341
  def sendMessageToPeer(msg, io, peer)
1315
1342
  peer.updateDownloadRate(msg)
1343
+ torrentData = @torrentData[peer.infoHash]
1344
+ torrentData.bytesUploaded += msg.length if torrentData
1345
+
1316
1346
  begin
1317
1347
  peer.peerMsgSerializer.serializeTo(msg, io)
1318
1348
  rescue
1319
1349
  @logger.warn "Sending message to peer #{peer} failed: #{$!.message}"
1320
1350
  end
1321
- msg.serializeTo io
1322
1351
  end
1323
1352
 
1324
1353
  # Update our internal peer list for this torrent from the tracker client
@@ -1602,8 +1631,8 @@ module QuartzTorrent
1602
1631
  result = TrackerDynamicRequestParams.new(dataLength)
1603
1632
  if torrentData && torrentData.blockState
1604
1633
  result.left = torrentData.blockState.totalLength - torrentData.blockState.completedLength
1605
- result.downloaded = torrentData.bytesDownloaded
1606
- result.uploaded = torrentData.bytesUploaded
1634
+ result.downloaded = torrentData.bytesDownloadedDataOnly
1635
+ result.uploaded = torrentData.bytesUploadedDataOnly
1607
1636
  end
1608
1637
  result
1609
1638
  end
@@ -2,6 +2,7 @@ require 'quartz_torrent/peer.rb'
2
2
  require 'quartz_torrent/util'
3
3
 
4
4
  module QuartzTorrent
5
+ # A container class for holding torrent peers. Allows lookup by different properties.
5
6
  class PeerHolder
6
7
  def initialize
7
8
  @peersById = {}
@@ -10,20 +11,24 @@ module QuartzTorrent
10
11
  @log = LogManager.getLogger("peerholder")
11
12
  end
12
13
 
14
+ # Find a peer by its trackerpeer's peerid. This is the id returned by the tracker, and may be nil.
13
15
  def findById(peerId)
14
16
  @peersById[peerId]
15
17
  end
16
18
 
19
+ # Find a peer by its IP address and port.
17
20
  def findByAddr(ip, port)
18
21
  @peersByAddr[ip + port.to_s]
19
22
  end
20
23
 
24
+ # Find all peers related to the torrent with the passed infoHash.
21
25
  def findByInfoHash(infoHash)
22
26
  l = @peersByInfoHash[infoHash]
23
27
  l = [] if ! l
24
28
  l
25
29
  end
26
30
 
31
+ # Add a peer to the PeerHolder.
27
32
  def add(peer)
28
33
  raise "Peer must have it's infoHash set." if ! peer.infoHash
29
34
 
@@ -44,7 +49,7 @@ module QuartzTorrent
44
49
  @peersByInfoHash.pushToList(peer.infoHash, peer)
45
50
  end
46
51
 
47
- # This peer, which previously had no id, has finished handshaking and now has an ID.
52
+ # Set the id for a peer. This peer, which previously had no id, has finished handshaking and now has an ID.
48
53
  def idSet(peer)
49
54
  @peersById.each do |e|
50
55
  return if e.eql?(peer)
@@ -52,6 +57,7 @@ module QuartzTorrent
52
57
  @peersById.pushToList(peer.trackerPeer.id, peer)
53
58
  end
54
59
 
60
+ # Delete the specified peer from the PeerHolder.
55
61
  def delete(peer)
56
62
  @peersByAddr.delete byAddrKey(peer)
57
63
 
@@ -82,14 +88,17 @@ module QuartzTorrent
82
88
  end
83
89
  end
84
90
 
91
+ # Return the list of all peers.
85
92
  def all
86
93
  @peersByAddr.values
87
94
  end
88
95
 
96
+ # Return the number of peers in the holder.
89
97
  def size
90
98
  @peersByAddr.size
91
99
  end
92
100
 
101
+ # Output a string representation of the PeerHolder, for debugging purposes.
93
102
  def to_s(infoHash = nil)
94
103
  def makeFlags(peer)
95
104
  s = "["
@@ -87,12 +87,15 @@ module QuartzTorrent
87
87
  MessageCancel = 8
88
88
  MessageExtended = 20
89
89
 
90
+ # Create a new PeerWireMessage with the specified message id.
90
91
  def initialize(messageId)
91
92
  @messageId = messageId
92
93
  end
93
94
 
95
+ # Get the message id of this message (the message type from the peer wire protocol).
94
96
  attr_reader :messageId
95
97
 
98
+ # Serialize this message to the passed io
96
99
  def serializeTo(io)
97
100
  io.write [payloadLength+1].pack("N")
98
101
  io.write [@messageId].pack("C")
@@ -108,6 +111,7 @@ module QuartzTorrent
108
111
  payloadLength + 5
109
112
  end
110
113
 
114
+ # Unserialize the message from the passed string.
111
115
  def unserialize(payload)
112
116
  raise "Subclasses of PeerWireMessage must implement unserialize but #{self.class} didn't"
113
117
  end
@@ -21,7 +21,7 @@ module QuartzTorrent
21
21
  def serverInit(metainfo, addr, port)
22
22
  end
23
23
 
24
- # Event handler. The current io is ready for reading
24
+ # Event handler: The current io is ready for reading.
25
25
  # If you will write to the same io from both this handler and the timerExpired handler,
26
26
  # you must make sure to perform all writing at once in this handler. If not then
27
27
  # the writes from the timer handler may be interleaved.
@@ -43,19 +43,22 @@ module QuartzTorrent
43
43
  def recvData(metainfo)
44
44
  end
45
45
 
46
- # Event handler. A timer has expired
46
+ # Event handler: a timer has expired.
47
+ # @param metainfo The metainfo associated with the timer, that was passed to scheduleTimer.
47
48
  def timerExpired(metainfo)
48
49
  end
49
50
 
50
- # Event handler. Error during read, or write. Connection errors are reported separately in connectError
51
+ # Event handler: an error occurred during read or write. Connection errors are reported separately in connectError
52
+ # @param metainfo The metainfo associated with the io.
51
53
  def error(metainfo, details)
52
54
  end
53
55
 
54
- # Event handler. Error during connection, or connection timed out.
56
+ # Event handler: an error occurred during connection, or connection timed out.
57
+ # @param metainfo The metainfo associated with the io, as passed to the connect method.
55
58
  def connectError(metainfo, details)
56
59
  end
57
60
 
58
- # Event handler. This is called for events added using addUserEvent to the reactor.
61
+ # Event handler: this is called for events added using addUserEvent to the reactor.
59
62
  def userEvent(event)
60
63
  end
61
64
 
@@ -63,6 +66,12 @@ module QuartzTorrent
63
66
  attr_accessor :reactor
64
67
 
65
68
  # Schedule a timer.
69
+ # @param duration The duration of the timer in seconds
70
+ # @param metainfo The metainfo to associate with the timer
71
+ # @param recurring If true when the timer duration expires, the timer will be rescheduled. If false the timer
72
+ # will not be rescheduled.
73
+ # @param immed If true then the timer will expire immediately (the next pass through the event loop). If the timer
74
+ # is also recurring it will then be rescheduled according to it's duratoin.
66
75
  def scheduleTimer(duration, metainfo = nil, recurring = true, immed = false)
67
76
  @reactor.scheduleTimer(duration, metainfo, recurring, immed) if @reactor
68
77
  end
@@ -73,48 +82,52 @@ module QuartzTorrent
73
82
  @reactor.cancelTimer(timerInfo) if @reactor
74
83
  end
75
84
 
85
+ # Create a TCP connection to the specified host and port. Associate the passed metainfo with the IO representing the connection.
76
86
  def connect(addr, port, metainfo, timeout = nil)
77
87
  @reactor.connect(addr, port, metainfo, timeout) if @reactor
78
88
  end
79
89
 
80
- # Write data to the current io
90
+ # Write data to the current io.
81
91
  def write(data)
82
92
  @reactor.write(data) if @reactor
83
93
  end
84
94
 
85
- # Read len bytes from the current io
95
+ # Read len bytes from the current io. This is meant to be called from one of the event handler methods.
86
96
  def read(len)
87
97
  result = ''
88
98
  result = @reactor.read(len) if @reactor
89
99
  result
90
100
  end
91
101
 
92
- # Shutdown the reactor
102
+ # Shutdown the reactor.
93
103
  def stopReactor
94
104
  @reactor.stop if @reactor
95
105
  end
96
106
 
97
- # Check if stop has been called on the reactor
107
+ # Check if stop has been called on the reactor.
98
108
  def stopped?
99
109
  @stopped
100
110
  end
101
111
 
102
- # Close the current io
112
+ # Close the current io. This is meant to be called from one of the event handler methods.
103
113
  def close(io = nil)
104
114
  @reactor.close(io) if @reactor
105
115
  end
106
116
 
117
+ # Return the current IO object. This is meant to be called from one of the event handler methods.
118
+ # The returned object is actually an IoFacade, a wrapper around the IO object.
107
119
  def currentIo
108
120
  result = nil
109
121
  result = @reactor.currentIo if @reactor
110
122
  result
111
123
  end
112
124
 
113
- # Find an io by metainfo
125
+ # Find an io by metainfo.
114
126
  def findIoByMetainfo(metainfo)
115
127
  @reactor.findIoByMetainfo metainfo if metainfo && @reactor
116
128
  end
117
-
129
+
130
+ # Set the metainfo for the current io. This is meant to be called from one of the event handler methods.
118
131
  def setMetaInfo(metainfo)
119
132
  @reactor.setMetaInfo metainfo if @reactor
120
133
  end
@@ -130,6 +143,8 @@ module QuartzTorrent
130
143
  end
131
144
  end
132
145
 
146
+ # Simple class used to buffer output for an IO until it's ready for writing. This is not part of the
147
+ # public API; it's used internally by the IOInfo class.
133
148
  class OutputBuffer
134
149
  # Create a new OutputBuffer for the specified IO. The parameter seekable should be
135
150
  # true or false. If true, then this output buffer will support seek
@@ -144,14 +159,17 @@ module QuartzTorrent
144
159
  end
145
160
  end
146
161
 
162
+ # Is the buffer empty?
147
163
  def empty?
148
164
  @buffer.length == 0
149
165
  end
150
166
 
167
+ # Number of bytes in the buffer.
151
168
  def size
152
169
  @buffer.length
153
170
  end
154
171
 
172
+ # Append data to the buffer.
155
173
  def append(data)
156
174
  if ! @seekable
157
175
  @buffer << data
@@ -208,6 +226,7 @@ module QuartzTorrent
208
226
  hash.delete @io
209
227
  end
210
228
 
229
+ # Read `length` bytes.
211
230
  def read(length)
212
231
  data = ''
213
232
  while data.length < length
@@ -249,6 +268,7 @@ module QuartzTorrent
249
268
  data
250
269
  end
251
270
 
271
+ # Write data to the IO.
252
272
  def write(data)
253
273
  # Issue: what about write, read, read on files opened for read/write? Write should happen at offset X, but reads moved to offset N. Since writes
254
274
  # are buffered, write may happen after read which means write will happen at the wrong offset N. Can fix by always seeking (if needed) before writes to the
@@ -259,37 +279,61 @@ module QuartzTorrent
259
279
  data.length
260
280
  end
261
281
 
282
+ # Seek on the io.
283
+ # @param amount amount to seek.
284
+ # @param whence one of the whence constants from IO::seek.
262
285
  def seek(amount, whence)
263
286
  @io.seek amount, whence if @ioInfo.seekable?
264
287
  end
265
288
 
289
+ # Flush data.
266
290
  def flush
267
291
  @io.flush
268
292
  end
269
293
 
294
+ # Close the io.
270
295
  def close
271
296
  @io.close
272
297
  end
273
298
 
299
+ # Check if the io is closed.
274
300
  def closed?
275
301
  @io.closed?
276
302
  end
303
+
304
+ def readRateLimit=(rate)
305
+ raise "The argument must be a RateLimit" if ! rate.nil? && ! rate.is_a?(RateLimit)
306
+ @ioInfo.readRateLimit = rate
307
+ end
308
+
309
+ def writeRateLimit=(rate)
310
+ raise "The argument must be a RateLimit" if ! rate.nil? && !rate.is_a?(RateLimit)
311
+ @ioInfo.writeRateLimit = rate
312
+ end
313
+
314
+ attr_accessor :writeRateLimit
277
315
  end
278
316
 
279
- # An IoFacade that doesn't allow reading.
317
+ # An IoFacade that doesn't allow reading. This is not part of the public API.
280
318
  class WriteOnlyIoFacade < IoFacade
319
+ # Create a new WriteOnlyIoFacade that delegates to the passed IOInfo object.
281
320
  def initialize(ioInfo, logger = nil, readError = "Reading is not allowed for this IO")
282
321
  super(ioInfo, logger)
283
322
  @readError = readError
284
323
  end
285
324
 
325
+ # Raise an exception.
286
326
  def read(length)
287
327
  raise @readError
288
328
  end
289
329
  end
290
330
 
291
- # An IO and associated meta-information used by the Reactor.
331
+ # An IO and associated meta-information used by the Reactor. This is not part of the public API.
292
332
  class IOInfo
333
+ # Create a new IOInfo object that operates on the passed IO object.
334
+ # @param io An IO object
335
+ # @param metainfo The metainfo to associate with the IO.
336
+ # @param seekable Whether the IO is seekable or not.
293
337
  def initialize(io, metainfo, seekable = false)
294
338
  @io = io
295
339
  @metainfo = metainfo
@@ -315,6 +359,8 @@ module QuartzTorrent
315
359
  attr_accessor :useErrorhandler
316
360
  attr_accessor :readRateLimit
317
361
  attr_accessor :writeRateLimit
362
+
363
+ # Is the IO seekable.
318
364
  def seekable?
319
365
  @seekable
320
366
  end
@@ -583,13 +629,15 @@ module QuartzTorrent
583
629
  end
584
630
 
585
631
  # Meant to be called from the handler. Sets the max rate at which the current io can read.
586
- def setReadRateLimit(rateLimit)
587
- @currentIoInfo.readRateLimit = rateLimit
632
+ def setReadRateLimit(rate)
633
+ raise "The argument must be a RateLimit" if ! rate.nil? && !rate.is_a?(RateLimit)
634
+ @currentIoInfo.readRateLimit = rate
588
635
  end
589
636
 
590
637
  # Meant to be called from the handler. Sets the max rate at which the current io can be written to.
591
- def setWriteRateLimit(rateLimit)
592
- @currentIoInfo.writeRateLimit = rateLimit
638
+ def setWriteRateLimit(rate)
639
+ raise "The argument must be a RateLimit" if ! rate.nil? && !rate.is_a?(RateLimit)
640
+ @currentIoInfo.writeRateLimit = rate
593
641
  end
594
642
 
595
643
  # Meant to be called from the handler. Find an IO by metainfo. The == operator is used to
@@ -653,18 +701,6 @@ module QuartzTorrent
653
701
  rescue
654
702
  # Exception occurred. Probably EINTR.
655
703
  @logger.warn "Select raised exception; will retry. Reason: #{$!}" if @logger
656
- @logger.warn "IOs at time of failure:"
657
- @logger.warn "Readset:"
658
- readset.each do |io|
659
- ioInfo = @ioInfo[io]
660
- puts " #{ioInfo.metainfo}: #{(io.closed?) ? "closed" : "not closed"}"
661
- end
662
- @logger.warn "Writeset:"
663
- writeset.each do |io|
664
- ioInfo = @ioInfo[io]
665
- puts " #{ioInfo.metainfo}: #{(io.closed?) ? "closed" : "not closed"}"
666
- end
667
-
668
704
  end
669
705
  end
670
706
 
@@ -1,4 +1,6 @@
1
1
  class Array
2
+ # Perform a binary search for a value in the array between the index low and high. This method expects a block. The
3
+ # block is passed a value v, and should return true if the target value is >= v, and false otherwise.
2
4
  def binsearch(low = nil, high = nil)
3
5
  return nil if length == 0
4
6
  result = binsearch_index(low, high){ |x| yield x if !x.nil?}
@@ -6,6 +8,8 @@ class Array
6
8
  result
7
9
  end
8
10
 
11
+ # Perform a binary search for an index in the array between the index low and high. This method expects a block. The
12
+ # block is passed a value v, and should return true if the target value is >= v, and false otherwise.
9
13
  def binsearch_index(low = nil, high = nil)
10
14
  return nil if length == 0
11
15
  low = 0 if !low
@@ -35,17 +35,21 @@ module QuartzTorrent
35
35
  end
36
36
  end
37
37
 
38
+ # Hash code of this TrackerPeer.
38
39
  def hash
39
40
  @hash
40
41
  end
41
42
 
43
+ # Equate to another TrackerPeer.
42
44
  def eql?(o)
43
45
  o.ip == @ip && o.port == @port
44
46
  end
45
47
 
46
- # Ip address, a string in dotted-quad notation
48
+ # IP address, a string in dotted-quad notation
47
49
  attr_accessor :ip
50
+ # TCP port
48
51
  attr_accessor :port
52
+ # Peer Id. This may be nil.
49
53
  attr_accessor :id
50
54
 
51
55
  def to_s
@@ -64,8 +68,11 @@ module QuartzTorrent
64
68
  @left = 0
65
69
  end
66
70
  end
71
+ # Number of bytes uploaded
67
72
  attr_accessor :uploaded
73
+ # Number of bytes downloaded
68
74
  attr_accessor :downloaded
75
+ # Number of bytes left to download before torrent is completed
69
76
  attr_accessor :left
70
77
  end
71
78
 
@@ -98,6 +105,9 @@ module QuartzTorrent
98
105
  class TrackerClient
99
106
  include QuartzTorrent
100
107
 
108
+ # Create a new TrackerClient
109
+ # @param announceUrl The announce URL of the tracker
110
+ # @param infoHash The infoHash of the torrent we're tracking
101
111
  def initialize(announceUrl, infoHash, dataLength = 0, maxErrors = 20)
102
112
  @peerId = "-QR0001-" # Azureus style
103
113
  @peerId << Process.pid.to_s
@@ -126,10 +136,13 @@ module QuartzTorrent
126
136
  # with up-to-date information.
127
137
  attr_accessor :dynamicRequestParamsBuilder
128
138
 
139
+ # Return true if this TrackerClient is started, false otherwise.
129
140
  def started?
130
141
  @started
131
142
  end
132
143
 
144
+ # Return the list of peers that the TrackerClient knows about. This list grows over time
145
+ # as more peers are reported from the tracker.
133
146
  def peers
134
147
  result = nil
135
148
  @peersMutex.synchronize do
@@ -153,7 +166,8 @@ module QuartzTorrent
153
166
  @errors
154
167
  end
155
168
 
156
- # Create a new TrackerClient using the passed information.
169
+ # Create a new TrackerClient using the passed information. This is a factory method that will return
170
+ # a tracker that talks the protocol specified in the URL.
157
171
  def self.create(announceUrl, infoHash, dataLength = 0, start = true)
158
172
  result = nil
159
173
  if announceUrl =~ /udp:\/\//
@@ -166,6 +180,7 @@ module QuartzTorrent
166
180
  result
167
181
  end
168
182
 
183
+ # Create a new TrackerClient using the passed Metainfo object.
169
184
  def self.createFromMetainfo(metainfo, start = true)
170
185
  create(metainfo.announce, metainfo.infoHash, metainfo.info.dataLength, start)
171
186
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quartz_torrent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -164,6 +164,7 @@ files:
164
164
  - lib/quartz_torrent/magnet.rb
165
165
  - lib/quartz_torrent/udptrackerclient.rb
166
166
  - README.md
167
+ - LICENSE
167
168
  - .yardopts
168
169
  - bin/quartztorrent_download
169
170
  - bin/quartztorrent_download_curses