class QuartzTorrent::MetainfoPieceState

Constants

BlockSize

Attributes

infoFileName[RW]
metainfoLength[RW]

Public Class Methods

downloaded(baseDirectory, infoHash) click to toggle source

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.

# 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) click to toggle source

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

# File lib/quartz_torrent/metainfopiecestate.rb, line 246
def self.generateInfoFileName(infoHash)
  "#{QuartzTorrent.bytesToHex(infoHash)}.info"
end
new(baseDirectory, infoHash, metainfoSize = nil, info = nil) click to toggle source

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.

# 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

Public Instance Methods

checkResults() click to toggle source

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

# File lib/quartz_torrent/metainfopiecestate.rb, line 160
def checkResults
  results = []
  while true
    result = @pieceManager.nextResult
    break if ! result

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

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

Do we have all the pieces of the metadata?

# File lib/quartz_torrent/metainfopiecestate.rb, line 131
def complete?
  @completePieces.allSet?
end
completedMetainfo() click to toggle source

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

# File lib/quartz_torrent/metainfopiecestate.rb, line 136
def completedMetainfo
  raise "Metadata is not yet complete" if ! complete?
end
findRequestablePeers(classifiedPeers) click to toggle source

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.

# 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
findRequestablePieces() click to toggle source

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

# 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
flush() click to toggle source

Flush all pieces to disk

# File lib/quartz_torrent/metainfopiecestate.rb, line 234
def flush
  id = @pieceManager.flush
  @pieceManagerRequests[id] = PieceManagerRequestMetadata.new(:flush, nil)
  @pieceManager.wait
end
markPeerBad(peer) click to toggle source

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.

# File lib/quartz_torrent/metainfopiecestate.rb, line 229
def markPeerBad(peer)
  @badPeers.add peer
end
metainfoCompletedLength() click to toggle source

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

# 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) click to toggle source

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

# 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) click to toggle source

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

# 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
remove() click to toggle source

Remove the metainfo file

# File lib/quartz_torrent/metainfopiecestate.rb, line 251
def remove
  path = infoFilePath
  FileUtils.rm path
end
savePiece(pieceIndex, data) click to toggle source

Save the specified piece to disk asynchronously.

# 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) click to toggle source

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

# 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
stop() click to toggle source

Stop the underlying PieceManager.

# File lib/quartz_torrent/metainfopiecestate.rb, line 257
def stop
  @pieceManager.stop
end
wait() click to toggle source

Wait for the next a pending request to complete.

# File lib/quartz_torrent/metainfopiecestate.rb, line 241
def wait
  @pieceManager.wait
end

Private Instance Methods

infoFilePath() click to toggle source
# File lib/quartz_torrent/metainfopiecestate.rb, line 275
def infoFilePath
  "#{@baseDirectory}#{File::SEPARATOR}#{@infoFileName}"
end
removeOldRequests() click to toggle source

Remove any pending requests after a timeout.

# File lib/quartz_torrent/metainfopiecestate.rb, line 263
def removeOldRequests
  now = Time.new
  @requestedPieces.length.times do |i|
    if @requestedPieces.set? i
      if now - @pieceRequestTime[i] > @requestTimeout
        @requestedPieces.clear i
        @pieceRequestTime[i] = nil
      end
    end
  end
end