From 60a1b0d4b4410c80c146fa5cd68603745128592e Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Fri, 1 Nov 2013 15:35:06 +0100 Subject: [PATCH] Basic torrent persistence --- lib/client.js | 27 +++++++++++++ lib/metadata.js | 20 +++++++++- lib/piece.js | 22 ++++++++++- lib/torrent/createpieces.js | 8 ++-- lib/torrent/torrent.js | 78 ++++++++++++++++++++++++++++--------- lib/tracker/tracker.js | 4 ++ 6 files changed, 133 insertions(+), 26 deletions(-) diff --git a/lib/client.js b/lib/client.js index 96a5f09..4452e07 100644 --- a/lib/client.js +++ b/lib/client.js @@ -71,6 +71,33 @@ Client.prototype.removeTorrent = function(torrent) { } }; +Client.prototype.getPersistData = function() { + var client = this; + + return Object.keys(client.torrents).reduce(function(data, infoHash) { + data[infoHash] = client.torrents[infoHash].getPersistData(); + return data; + }, {}); +}; + +Client.prototype.restorePersistData = function(data) { + var client = this; + var torrents = []; + + Object.keys(data).forEach(function(infoHash) { + if (client.torrents[infoHash]) { + LOGGER.warn('Torrent %s not overwritten while restoring data', infoHash); + } else { + var torrent = Torrent.fromPersistData(client.id, client.port, client._extensions, data[infoHash]); + + client.torrents[infoHash] = torrent; + torrents.push(torrent); + } + }); + + return torrents; +}; + Client.prototype._handleConnection = function(stream) { var peer = new Peer(stream), client = this; diff --git a/lib/metadata.js b/lib/metadata.js index af4c171..c986a3b 100644 --- a/lib/metadata.js +++ b/lib/metadata.js @@ -8,10 +8,10 @@ var bencode = require('./util/bencode'), var LOGGER = require('log4js').getLogger('metadata.js'); -function Metadata(infoHash, metadata) { +function Metadata(infoHash, metadata, persistBitfield) { EventEmitter.call(this); this.infoHash = infoHash; - this.bitfield = null; + this.bitfield = persistBitfield ? new Bitfield(persistBitfield.buffer, persistBitfield.length) : null; this._encodedMetadata = null; this._length = 0; this.setMetadata(metadata); @@ -93,6 +93,22 @@ Metadata.prototype.setPiece = function(index, data) { } }; +Metadata.prototype.getPersistData = function() { + return { + infoHash: this.infoHash, + encoded: this._encodedMetadata, + bitfield: this.bitfield ? { buffer: this.bitfield.toBuffer(), length: this.bitfield.length } : null + }; +}; + +Metadata.fromPersistData = function(persistData) { + return new Metadata( + persistData.infoHash, + bencode.decode(persistData.toString('binary')), + persistData.bitfield + ); +}; + Metadata.COMPLETE = 'metadata:complete'; Metadata.INVALID = 'metadata:invalid'; diff --git a/lib/piece.js b/lib/piece.js index f26f471..b500d4d 100644 --- a/lib/piece.js +++ b/lib/piece.js @@ -9,10 +9,15 @@ var File = require('./file'); var LOGGER = require('log4js').getLogger('piece.js'); -var Piece = function(index, offset, length, hash, files, callback) { +var Piece = function(index, offset, length, hash, files, persistBitfield, callback) { EventEmitter.call(this); - this.complete = new BitField(Math.ceil(length / Piece.CHUNK_LENGTH)); + if (persistBitfield) { + this.complete = new BitField(persistBitfield.buffer, persistBitfield.length); + } else { + this.complete = new BitField(Math.ceil(length / Piece.CHUNK_LENGTH)); + } + this.files = []; this.hash = hash; this.index = index; @@ -40,6 +45,12 @@ var Piece = function(index, offset, length, hash, files, callback) { if (valid) { setState(self, Piece.COMPLETE); } else { + if (persistBitfield && self.complete.cardinality === self.complete.length) { + LOGGER.debug('Invalid persisted piece, clearing'); + self.complete = new BitField(self.complete.length); + self.requested = new BitField(self.complete.length); + } + setState(self, Piece.INCOMPLETE); } callback(); @@ -176,6 +187,13 @@ function setState(self, state) { self.emit(state, self); } +Piece.prototype.getPersistData = function() { + return { + buffer: this.complete.toBuffer(), + length: this.complete.length + }; +}; + Piece.CHUNK_LENGTH = 16384; Piece.COMPLETE = 'complete'; diff --git a/lib/torrent/createpieces.js b/lib/torrent/createpieces.js index 144b1f5..93769b6 100644 --- a/lib/torrent/createpieces.js +++ b/lib/torrent/createpieces.js @@ -2,17 +2,17 @@ var Piece = require('../piece'); var LOGGER = require('log4js').getLogger('createpieces.js'); -function createPieces(hashes, files, pieceLength, sizeOfDownload, callback) { +function createPieces(hashes, files, pieceLength, sizeOfDownload, persistPieces, callback) { var pieces = [], numberOfPieces = hashes.length / 20, currentIndex = 0; createPiece(pieces, hashes, files, currentIndex, numberOfPieces, pieceLength, - sizeOfDownload, callback); + sizeOfDownload, persistPieces, callback); } function createPiece(pieces, hashes, files, currentIndex, numberOfPieces, pieceLength, - sizeOfDownload, callback) { + sizeOfDownload, persistPieces, callback) { if (currentIndex === numberOfPieces) { callback(null, pieces); } else { @@ -23,7 +23,7 @@ function createPiece(pieces, hashes, files, currentIndex, numberOfPieces, pieceL lengthOfNextPiece = sizeOfDownload % pieceLength; } var piece = new Piece(currentIndex, currentIndex * pieceLength, lengthOfNextPiece, hash, - files, function() { + files, persistPieces[currentIndex], function() { createPiece(pieces, hashes, files, currentIndex + 1, numberOfPieces, pieceLength, sizeOfDownload, callback); }); diff --git a/lib/torrent/torrent.js b/lib/torrent/torrent.js index 710a581..c582d84 100644 --- a/lib/torrent/torrent.js +++ b/lib/torrent/torrent.js @@ -19,7 +19,7 @@ var BitField = require('../util/bitfield'), var LOGGER = require('log4js').getLogger('torrent.js'); -function Torrent(clientId, clientPort, downloadPath, dataUrl, extensions) { +function Torrent(clientId, clientPort, downloadPath, dataUrl, extensions, persistData) { EventEmitter.call(this); this.clientId = clientId; @@ -45,25 +45,39 @@ function Torrent(clientId, clientPort, downloadPath, dataUrl, extensions) { this._requestManager = new RequestManager(this); this._files = []; this._pieces = []; - this._downloadPath = downloadPath; this._extensions = extensions; this._extensionMap = null; var torrent = this; + // load torrent data - TorrentData.load(dataUrl, function(error, metadata, trackers) { - if (error) { - LOGGER.warn('Error loading torrent data. error = %j', error); - torrent._setStatus(Torrent.ERROR, error); - } else { - LOGGER.debug('Torrent data loaded.'); - torrent._metadata = metadata; - trackers.forEach(function(tracker) { - torrent.addTracker(tracker); - }); - torrent._initialise(); - } - }); + if (persistData) { + LOGGER.debug('Loading persisted torrent data'); + + this._downloadPath = persistData.downloadPath; + this._metadata = Metadata.fromPersistData(persistData.metadata); + + Tracker.createTrackers(null, persistData.trackers).forEach(function(tracker) { + torrent.addTracker(tracker); + }); + torrent._initialise(persistData.bitfield, persistData.pieces); + } else { + this._downloadPath = downloadPath; + + TorrentData.load(dataUrl, function(error, metadata, trackers) { + if (error) { + LOGGER.warn('Error loading torrent data. error = %j', error); + torrent._setStatus(Torrent.ERROR, error); + } else { + LOGGER.debug('Torrent data loaded.'); + torrent._metadata = metadata; + trackers.forEach(function(tracker) { + torrent.addTracker(tracker); + }); + torrent._initialise(); + } + }); + } } util.inherits(Torrent, EventEmitter); @@ -187,7 +201,7 @@ Torrent.prototype.setMetadata = function(metadata) { this._initialise(); }; -Torrent.prototype._initialise = function() { +Torrent.prototype._initialise = function(persistBitfield, persistPieces) { LOGGER.debug('Initialising torrent.'); if (this.status === Torrent.READY) { @@ -229,12 +243,18 @@ Torrent.prototype._initialise = function() { torrent.size = _size; createPieces(torrent._metadata.pieces, _files, torrent._metadata['piece length'], - _size, function(error, _pieces) { + _size, persistPieces, function(error, _pieces) { if (error) { torrent._setStatus(Torrent.ERROR, error); } else { torrent._pieces = _pieces; - torrent.bitfield = new BitField(_pieces.length); + + if (persistBitfield) { + torrent.bitfield = new BitField(persistBitfield.buffer, persistBitfield.length); + } else { + torrent.bitfield = new BitField(_pieces.length); + } + var completeHandler = torrent._pieceComplete.bind(torrent); _pieces.forEach(function(piece) { if (piece.isComplete()) { @@ -282,6 +302,28 @@ Torrent.prototype._setStatus = function(status, data) { } }; +Torrent.prototype.getPersistData = function() { + if (!this._metadata || this.trackers.length === 0) { + throw new Error('Cannot persist torrent without metadata or trackers'); + } + + return { + downloadPath: this._downloadPath, + metadata: this._metadata.getPersistData() : null, + trackers: this.trackers.map(function(tracker) { + return tracker.getPersistData(); + }, + bitfield: this.bitfield ? { buffer: this.bitfield.toBuffer(), length: this.bitfield.length } : null, + pieces: this.pieces.map(function(piece) { + return piece.getPersistData(); + }) + }; +}; + +Torrent.fromPersistData = function(clientId, clientPort, extensions, data) { + return new Torrent(clientId, clientPort, null, null, extensions, data); +}; + Torrent.COMPLETE = 'torrent:complete'; Torrent.ERROR = 'torrent:error'; Torrent.INFO_HASH = 'torrent:info_hash'; diff --git a/lib/tracker/tracker.js b/lib/tracker/tracker.js index 4ae8495..e7db354 100644 --- a/lib/tracker/tracker.js +++ b/lib/tracker/tracker.js @@ -110,6 +110,10 @@ Tracker.prototype._updateInfo = function(data) { } }; +Tracker.prototype.getPersistData = function() { + return this.url; +}; + Tracker.createTrackers = function(announce, announceList) { var trackers = []; if (announceList) {