# The contents of this file are subject to the BitTorrent Open Source License # Version 1.1 (the License). You may not copy or use this file, in either # source code or executable form, except in compliance with the License. You # may obtain a copy of the License at http://www.bittorrent.com/license/. # # Software distributed under the License is distributed on an AS IS basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # Written by Bram Cohen from __future__ import division from sha import sha from array import array from binascii import b2a_hex from NohGooee.bitfield import Bitfield from NohGooee import BTFailure, INFO, WARNING, ERROR, CRITICAL def toint(s): return int(b2a_hex(s), 16) def tobinary(i): return (chr(i >> 24) + chr((i >> 16) & 0xFF) + chr((i >> 8) & 0xFF) + chr(i & 0xFF)) NO_PLACE = -1 ALLOCATED = -1 UNALLOCATED = -2 FASTRESUME_PARTIAL = -3 class StorageWrapper(object): def __init__(self, storage, config, hashes, piece_size, finished, statusfunc, flag, data_flunked, infohash, errorfunc, resumefile): self.numpieces = len(hashes) self.storage = storage self.config = config check_hashes = config['check_hashes'] self.hashes = hashes self.piece_size = piece_size self.data_flunked = data_flunked self.errorfunc = errorfunc self.total_length = storage.get_total_length() self.amount_left = self.total_length self.partial_mark = "BitTorrent - this part has not been "+\ "downloaded yet."+infohash+\ tobinary(config['download_slice_size']) if self.total_length <= piece_size * (self.numpieces - 1): raise BTFailure, _("bad data in responsefile - total too small") if self.total_length > piece_size * self.numpieces: raise BTFailure, _("bad data in responsefile - total too big") self.finished = finished self.numactive = array('H', [0] * self.numpieces) self.inactive_requests = [1] * self.numpieces self.amount_inactive = self.total_length self.endgame = False self.have = Bitfield(self.numpieces) self.waschecked = Bitfield(self.numpieces) if self.numpieces < 32768: typecode = 'h' else: typecode = 'l' self.places = array(typecode, [NO_PLACE] * self.numpieces) if not check_hashes: self.rplaces = array(typecode, range(self.numpieces)) fastresume = True else: self.rplaces = self._load_fastresume(resumefile, typecode) if self.rplaces is not None: fastresume = True else: self.rplaces = array(typecode, [UNALLOCATED] * self.numpieces) fastresume = False self.holepos = 0 self.stat_numfound = 0 self.stat_numflunked = 0 self.stat_numdownloaded = 0 self.stat_active = {} self.stat_new = {} self.stat_dirty = {} self.download_history = {} self.failed_pieces = {} if self.numpieces == 0: return targets = {} total = 0 if not fastresume: for i in xrange(self.numpieces): if self._waspre(i): self.rplaces[i] = ALLOCATED total += 1 else: targets[hashes[i]] = i if total and check_hashes: statusfunc(_("checking existing file"), 0) def markgot(piece, pos): if self.have[piece]: if piece != pos: return self.rplaces[self.places[pos]] = ALLOCATED self.places[pos] = self.rplaces[pos] = pos return self.places[piece] = pos self.rplaces[pos] = piece self.have[piece] = True self.amount_left -= self._piecelen(piece) self.amount_inactive -= self._piecelen(piece) self.inactive_requests[piece] = None if not fastresume: self.waschecked[piece] = True self.stat_numfound += 1 lastlen = self._piecelen(self.numpieces - 1) partials = {} for i in xrange(self.numpieces): if not self._waspre(i): if self.rplaces[i] != UNALLOCATED: raise BTFailure(_("--check_hashes 0 or fastresume info " "doesn't match file state (missing data)")) continue elif fastresume: t = self.rplaces[i] if t >= 0: markgot(t, i) continue if t == UNALLOCATED: raise BTFailure(_("Bad fastresume info (files contain more " "data)")) if t == ALLOCATED: continue if t!= FASTRESUME_PARTIAL: raise BTFailure(_("Bad fastresume info (illegal value)")) data = self.storage.read(self.piece_size * i, self._piecelen(i)) self._check_partial(i, partials, data) self.rplaces[i] = ALLOCATED else: data = self.storage.read(piece_size * i, self._piecelen(i)) sh = sha(buffer(data, 0, lastlen)) sp = sh.digest() sh.update(buffer(data, lastlen)) s = sh.digest() if s == hashes[i]: markgot(i, i) elif s in targets and self._piecelen(i) == self._piecelen(targets[s]): markgot(targets[s], i) elif not self.have[self.numpieces - 1] and sp == hashes[-1] and (i == self.numpieces - 1 or not self._waspre(self.numpieces - 1)): markgot(self.numpieces - 1, i) else: self._check_partial(i, partials, data) statusfunc(fractionDone = 1 - self.amount_left / self.total_length) if flag.isSet(): return self.amount_left_with_partials = self.amount_left for piece in partials: if self.places[piece] < 0: pos = partials[piece][0] self.places[piece] = pos self.rplaces[pos] = piece self._make_partial(piece, partials[piece][1]) for i in xrange(self.numpieces): if self.rplaces[i] != UNALLOCATED: self.storage.allocated(piece_size * i, self._piecelen(i)) if self.have[i]: self.storage.downloaded(piece_size * i, self._piecelen(i)) def _waspre(self, piece): return self.storage.was_preallocated(piece * self.piece_size, self._piecelen(piece)) def _piecelen(self, piece): if piece < self.numpieces - 1: return self.piece_size else: return self.total_length - piece * self.piece_size def _check_partial(self, pos, partials, data): index = None missing = False marklen = len(self.partial_mark)+4 for i in xrange(0, len(data) - marklen, self.config['download_slice_size']): if data[i:i+marklen-4] == self.partial_mark: ind = toint(data[i+marklen-4:i+marklen]) if index is None: index = ind parts = [] if ind >= self.numpieces or ind != index: return parts.append(i) else: missing = True if index is not None and missing: i += self.config['download_slice_size'] if i < len(data): parts.append(i) partials[index] = (pos, parts) def _make_partial(self, index, parts): length = self._piecelen(index) l = [] self.inactive_requests[index] = l x = 0 self.amount_left_with_partials -= self._piecelen(index) self.download_history[index] = {} request_size = self.config['download_slice_size'] for x in xrange(0, self._piecelen(index), request_size): partlen = min(request_size, length - x) if x in parts: l.append((x, partlen)) self.amount_left_with_partials += partlen else: self.amount_inactive -= partlen self.download_history[index][x] = None self.stat_dirty[index] = 1 def _initalloc(self, pos, piece): assert self.rplaces[pos] < 0 assert self.places[piece] == NO_PLACE p = self.piece_size * pos length = self._piecelen(pos) if self.rplaces[pos] == UNALLOCATED: self.storage.allocated(p, length) self.places[piece] = pos self.rplaces[pos] = piece # "if self.rplaces[pos] != ALLOCATED:" to skip extra mark writes mark = self.partial_mark + tobinary(piece) mark += chr(0xff) * (self.config['download_slice_size'] - len(mark)) mark *= (length - 1) // len(mark) + 1 self.storage.write(p, buffer(mark, 0, length)) def _move_piece(self, oldpos, newpos): assert self.rplaces[newpos] < 0 assert self.rplaces[oldpos] >= 0 data = self.storage.read(self.piece_size * oldpos, self._piecelen(newpos)) self.storage.write(self.piece_size * newpos, data) if self.rplaces[newpos] == UNALLOCATED: self.storage.allocated(self.piece_size * newpos, len(data)) piece = self.rplaces[oldpos] self.places[piece] = newpos self.rplaces[oldpos] = ALLOCATED self.rplaces[newpos] = piece if not self.have[piece]: return data = data[:self._piecelen(piece)] if sha(data).digest() != self.hashes[piece]: raise BTFailure(_("data corrupted on disk - " "maybe you have two copies running?")) def _get_free_place(self): while self.rplaces[self.holepos] >= 0: self.holepos += 1 return self.holepos def get_amount_left(self): return self.amount_left def do_I_have_anything(self): return self.amount_left < self.total_length def _make_inactive(self, index): length = self._piecelen(index) l = [] x = 0 request_size = self.config['download_slice_size'] while x + request_size < length: l.append((x, request_size)) x += request_size l.append((x, length - x)) self.inactive_requests[index] = l def _load_fastresume(self, resumefile, typecode): if resumefile is not None: try: r = array(typecode) r.fromfile(resumefile, self.numpieces) return r except Exception, e: self.errorfunc(WARNING, _("Couldn't read fastresume data: ") + str(e)) return None def write_fastresume(self, resumefile): for i in xrange(self.numpieces): if self.rplaces[i] >= 0 and not self.have[self.rplaces[i]]: self.rplaces[i] = FASTRESUME_PARTIAL self.rplaces.tofile(resumefile) def get_have_list(self): return self.have.tostring() def do_I_have(self, index): return self.have[index] def do_I_have_requests(self, index): return not not self.inactive_requests[index] def new_request(self, index): # returns (begin, length) if self.inactive_requests[index] == 1: self._make_inactive(index) self.numactive[index] += 1 self.stat_active[index] = 1 if index not in self.stat_dirty: self.stat_new[index] = 1 rs = self.inactive_requests[index] r = min(rs) rs.remove(r) self.amount_inactive -= r[1] if self.amount_inactive == 0: self.endgame = True return r def piece_came_in(self, index, begin, piece, source = None): if self.places[index] < 0: if self.rplaces[index] == ALLOCATED: self._initalloc(index, index) else: n = self._get_free_place() if self.places[n] >= 0: oldpos = self.places[n] self._move_piece(oldpos, n) n = oldpos if self.rplaces[index] < 0 or index == n: self._initalloc(n, index) else: self._move_piece(index, n) self._initalloc(index, index) if index in self.failed_pieces: old = self.storage.read(self.places[index] * self.piece_size + begin, len(piece)) if old != piece: self.failed_pieces[index][self.download_history[index][begin]]\ = None self.download_history.setdefault(index, {}) self.download_history[index][begin] = source self.storage.write(self.places[index] * self.piece_size + begin, piece) self.stat_dirty[index] = 1 self.numactive[index] -= 1 if self.numactive[index] == 0: del self.stat_active[index] if index in self.stat_new: del self.stat_new[index] if not self.inactive_requests[index] and not self.numactive[index]: del self.stat_dirty[index] if sha(self.storage.read(self.piece_size * self.places[index], self._piecelen(index))).digest() == self.hashes[index]: self.have[index] = True self.storage.downloaded(index * self.piece_size, self._piecelen(index)) self.inactive_requests[index] = None self.waschecked[index] = True self.amount_left -= self._piecelen(index) self.stat_numdownloaded += 1 for d in self.download_history[index].itervalues(): if d is not None: d.good(index) del self.download_history[index] if index in self.failed_pieces: for d in self.failed_pieces[index]: if d is not None: d.bad(index) del self.failed_pieces[index] if self.amount_left == 0: self.finished() else: self.data_flunked(self._piecelen(index), index) self.inactive_requests[index] = 1 self.amount_inactive += self._piecelen(index) self.stat_numflunked += 1 self.failed_pieces[index] = {} allsenders = {} for d in self.download_history[index].itervalues(): allsenders[d] = None if len(allsenders) == 1: culprit = allsenders.keys()[0] if culprit is not None: culprit.bad(index, bump = True) del self.failed_pieces[index] # found the culprit already return False return True def request_lost(self, index, begin, length): self.inactive_requests[index].append((begin, length)) self.amount_inactive += length self.numactive[index] -= 1 if not self.numactive[index] and index in self.stat_active: del self.stat_active[index] if index in self.stat_new: del self.stat_new[index] def get_piece(self, index, begin, length): if not self.have[index]: return None if not self.waschecked[index]: if sha(self.storage.read(self.piece_size * self.places[index], self._piecelen(index))).digest() != self.hashes[index]: raise BTFailure, _("told file complete on start-up, but piece failed hash check") self.waschecked[index] = True if begin + length > self._piecelen(index): return None return self.storage.read(self.piece_size * self.places[index] + begin, length)