Class: QuartzTorrent::MetainfoPieceState

Inherits:
Object
  • Object
show all
Defined in:
lib/quartz_torrent/metainfopiecestate.rb

Constant Summary collapse

BlockSize =
16384

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(baseDirectory, infoHash, metainfoSize = nil, info = nil) ⇒ MetainfoPieceState

Create a new MetainfoPieceState that can be used to manage downloading the metainfo for a torrent. The metainfo is stored in a file under baseDirectory named <infohash>.info, where <infohash> is infoHash hex-encoded. The parameter metainfoSize should be the size of the metainfo, and info can be used to pass in the complete metainfo Info object if it is available. This is needed for when other peers request the metainfo from us.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 24

def initialize(baseDirectory, infoHash, metainfoSize = nil, info = nil)
 
  @logger = LogManager.getLogger("metainfo_piece_state")

  @requestTimeout = 5
  @baseDirectory = baseDirectory
  @infoFileName = MetainfoPieceState.generateInfoFileName(infoHash)

  path = infoFilePath

  completed = MetainfoPieceState.downloaded(baseDirectory, infoHash)
  metainfoSize = File.size(path) if ! metainfoSize && completed

  if !completed && info 
    File.open(path, "wb") do |file|
      bencoded = info.bencode
      metainfoSize = bencoded.length
      file.write bencoded
      # Sanity check
      testInfoHash = Digest::SHA1.digest( bencoded )
      raise "The computed infoHash #{QuartzTorrent.bytesToHex(testInfoHash)} doesn't match the original infoHash #{QuartzTorrent.bytesToHex(infoHash)}" if testInfoHash != infoHash
    end
  end

  raise "Unless the metainfo has already been successfully downloaded or the torrent file is available, the metainfoSize is needed" if ! metainfoSize

  # We use the PieceManager to manage the pieces of the metainfo file. The PieceManager is designed
  # for the pieces and blocks of actual torrent data, so we need to build a fake metainfo object that
  # describes our one metainfo file itself so that we can store the pieces if it on disk.
  # In this case we map metainfo pieces to 'torrent' pieces, and our blocks are the full length of the 
  # metainfo piece.
  torrinfo = Metainfo::Info.new
  torrinfo.pieceLen = BlockSize
  torrinfo.files = []
  torrinfo.files.push Metainfo::FileInfo.new(metainfoSize, @infoFileName)


  @pieceManager = PieceManager.new(baseDirectory, torrinfo)
  @pieceManagerRequests = {}

  @numPieces = metainfoSize/BlockSize
  @numPieces += 1 if (metainfoSize%BlockSize) != 0
  @completePieces = Bitfield.new(@numPieces)
  @completePieces.setAll if info || completed

  @lastPieceLength = metainfoSize - (@numPieces-1)*BlockSize
  
  @badPeers = PeerHolder.new
  @requestedPieces = Bitfield.new(@numPieces)
  @requestedPieces.clearAll

  @metainfoLength = metainfoSize

  # Time at which the piece in requestedPiece was requested. Used for timeouts.
  @pieceRequestTime = []
end

Instance Attribute Details

#infoFileNameObject

Returns the value of attribute infoFileName.



106
107
108
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 106

def infoFileName
  @infoFileName
end

#metainfoLengthObject

Returns the value of attribute metainfoLength.



107
108
109
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 107

def metainfoLength
  @metainfoLength
end

Class Method Details

.downloaded(baseDirectory, infoHash) ⇒ Object

Check if the metainfo has already been downloaded successfully during a previous session. Returns the completed, Metainfo::Info object if it is complete, and nil otherwise.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 83

def self.downloaded(baseDirectory, infoHash)
  logger = LogManager.getLogger("metainfo_piece_state")
  infoFileName = generateInfoFileName(infoHash)
  path = "#{baseDirectory}#{File::SEPARATOR}#{infoFileName}"

  result = nil
  if File.exists?(path)
    File.open(path, "rb") do |file|
      bencoded = file.read
      # Sanity check
      testInfoHash = Digest::SHA1.digest( bencoded )
      if testInfoHash == infoHash
        result = Metainfo::Info.createFromBdecode(BEncode.load(bencoded, {:ignore_trailing_junk => 1})) 
      else
        logger.info "the computed SHA1 hash doesn't match the specified infoHash in #{path}"
      end
    end
  else
    logger.info "the metainfo file #{path} doesn't exist"
  end
  result
end

.generateInfoFileName(infoHash) ⇒ Object

Return the name of the file where this class will store the Torrent Info struct.



246
247
248
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 246

def self.generateInfoFileName(infoHash)
  "#{QuartzTorrent.bytesToHex(infoHash)}.info"
end

Instance Method Details

#checkResultsObject

Check the results of savePiece and readPiece. This method returns a list of the PieceManager results.



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 160

def checkResults
  results = []
  while true
    result = @pieceManager.nextResult
    break if ! result

    results.push result
      
     = @pieceManagerRequests.delete(result.requestId)
    if ! 
      @logger.error "Can't find metadata for PieceManager request #{result.requestId}"
      next
    end 

    if .type == :write
      if result.successful?
        @completePieces.set(.data)
      else
        @requestedPieces.clear(.data)
        @pieceRequestTime[.data] = nil
        @logger.error "Writing metainfo piece failed: #{result.error}"
      end
    elsif .type == :read
      if ! result.successful?
        @logger.error "Reading metainfo piece failed: #{result.error}"
      end
    end
  end
  results
end

#complete?Boolean

Do we have all the pieces of the metadata?

Returns:

  • (Boolean)


131
132
133
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 131

def complete?
  @completePieces.allSet?
end

#completedMetainfoObject

Get the completed metainfo. Raises an exception if it’s not yet complete.



136
137
138
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 136

def completedMetainfo
  raise "Metadata is not yet complete" if ! complete?
end

#findRequestablePeers(classifiedPeers) ⇒ Object

Return a list of peers from whom we can request pieces. These are peers for whom we have an established connection, and are not marked as bad. See markPeerBad.



206
207
208
209
210
211
212
213
214
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 206

def findRequestablePeers(classifiedPeers)
  result = []

  classifiedPeers.establishedPeers.each do |peer|
    result.push peer if ! @badPeers.findByAddr(peer.trackerPeer.ip, peer.trackerPeer.port)
  end

  result
end

#findRequestablePiecesObject

Return a list of torrent pieces that can still be requested. These are pieces that are not completed and are not requested.



192
193
194
195
196
197
198
199
200
201
202
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 192

def findRequestablePieces
  piecesRequired = []

  removeOldRequests

  @numPieces.times do |pieceIndex|
    piecesRequired.push pieceIndex if ! @completePieces.set?(pieceIndex) && ! @requestedPieces.set?(pieceIndex)
  end

  piecesRequired
end

#flushObject

Flush all pieces to disk



234
235
236
237
238
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 234

def flush
  id = @pieceManager.flush
  @pieceManagerRequests[id] = PieceManagerRequestMetadata.new(:flush, nil)
  @pieceManager.wait
end

#markPeerBad(peer) ⇒ Object

Mark the specified peer as ‘bad’. We won’t try requesting pieces from this peer. Used, for example, when a peer rejects our request for a metadata piece.



229
230
231
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 229

def markPeerBad(peer)
  @badPeers.add peer
end

#metainfoCompletedLengthObject

Return the number of bytes of the metainfo that we have downloaded so far.



110
111
112
113
114
115
116
117
118
119
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 110

def metainfoCompletedLength
  num = @completePieces.countSet
  # Last block may be smaller
  extra = 0
  if @completePieces.set?(@completePieces.length-1)
    num -= 1
    extra = @lastPieceLength
  end
  num*BlockSize + extra
end

#pieceCompleted?(pieceIndex) ⇒ Boolean

Return true if the specified piece is completed. The piece is specified by index.

Returns:

  • (Boolean)


122
123
124
125
126
127
128
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 122

def pieceCompleted?(pieceIndex)
  if pieceIndex >= 0 && pieceIndex < @completePieces.length
    @completePieces.set? pieceIndex
  else
    false
  end
end

#readPiece(pieceIndex) ⇒ Object

Read a piece from disk. This method is asynchronous; it returns a handle that can be later used to retreive the result.



149
150
151
152
153
154
155
156
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 149

def readPiece(pieceIndex)
  length = BlockSize
  length = @lastPieceLength if pieceIndex == @numPieces - 1
  id = @pieceManager.readBlock pieceIndex, 0, length
  #result = manager.nextResult
  @pieceManagerRequests[id] = PieceManagerRequestMetadata.new(:read, pieceIndex)
  id
end

#removeObject

Remove the metainfo file



251
252
253
254
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 251

def remove
  path = infoFilePath
  FileUtils.rm path
end

#savePiece(pieceIndex, data) ⇒ Object

Save the specified piece to disk asynchronously.



141
142
143
144
145
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 141

def savePiece(pieceIndex, data)
  id = @pieceManager.writeBlock pieceIndex, 0, data
  @pieceManagerRequests[id] = PieceManagerRequestMetadata.new(:write, pieceIndex)
  id
end

#setPieceRequested(pieceIndex, bool) ⇒ Object

Set whether the piece with the passed pieceIndex is requested or not.



217
218
219
220
221
222
223
224
225
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 217

def setPieceRequested(pieceIndex, bool)
  if bool
    @requestedPieces.set pieceIndex
    @pieceRequestTime[pieceIndex] = Time.new
  else
    @requestedPieces.clear pieceIndex
    @pieceRequestTime[pieceIndex] = nil
  end
end

#stopObject

Stop the underlying PieceManager.



257
258
259
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 257

def stop
  @pieceManager.stop
end

#waitObject

Wait for the next a pending request to complete.



241
242
243
# File 'lib/quartz_torrent/metainfopiecestate.rb', line 241

def wait
  @pieceManager.wait
end