227 lines
8.2 KiB
Python
227 lines
8.2 KiB
Python
import os
|
|
import logging
|
|
from twisted.internet import defer, threads
|
|
from twisted.web.client import FileBodyProducer
|
|
from twisted.python.failure import Failure
|
|
from lbrynet.cryptoutils import get_lbry_hash_obj
|
|
from lbrynet.p2p.Error import DownloadCanceledError, InvalidDataError, InvalidBlobHashError
|
|
from lbrynet.blob.writer import HashBlobWriter
|
|
from lbrynet.blob.reader import HashBlobReader
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
MAX_BLOB_SIZE = 2 * 2 ** 20
|
|
|
|
# digest_size is in bytes, and blob hashes are hex encoded
|
|
blobhash_length = get_lbry_hash_obj().digest_size * 2
|
|
|
|
|
|
def is_valid_hashcharacter(char):
|
|
return char in "0123456789abcdef"
|
|
|
|
|
|
def is_valid_blobhash(blobhash):
|
|
"""Checks whether the blobhash is the correct length and contains only
|
|
valid characters (0-9, a-f)
|
|
|
|
@param blobhash: string, the blobhash to check
|
|
|
|
@return: True/False
|
|
"""
|
|
return len(blobhash) == blobhash_length and all(is_valid_hashcharacter(l) for l in blobhash)
|
|
|
|
|
|
class BlobFile:
|
|
"""
|
|
A chunk of data available on the network which is specified by a hashsum
|
|
|
|
This class is used to create blobs on the local filesystem
|
|
when we already know the blob hash before hand (i.e., when downloading blobs)
|
|
Also can be used for reading from blobs on the local filesystem
|
|
"""
|
|
|
|
def __str__(self):
|
|
return self.blob_hash[:16]
|
|
|
|
def __repr__(self):
|
|
return '<{}({})>'.format(self.__class__.__name__, str(self))
|
|
|
|
def __init__(self, blob_dir, blob_hash, length=None):
|
|
if not is_valid_blobhash(blob_hash):
|
|
raise InvalidBlobHashError(blob_hash)
|
|
self.blob_hash = blob_hash
|
|
self.length = length
|
|
self.writers = {} # {Peer: writer, finished_deferred}
|
|
self._verified = False
|
|
self.readers = 0
|
|
self.blob_dir = blob_dir
|
|
self.file_path = os.path.join(blob_dir, self.blob_hash)
|
|
self.blob_write_lock = defer.DeferredLock()
|
|
self.saved_verified_blob = False
|
|
if os.path.isfile(self.file_path):
|
|
self.set_length(os.path.getsize(self.file_path))
|
|
# This assumes that the hash of the blob has already been
|
|
# checked as part of the blob creation process. It might
|
|
# be worth having a function that checks the actual hash;
|
|
# its probably too expensive to have that check be part of
|
|
# this call.
|
|
self._verified = True
|
|
|
|
def open_for_writing(self, peer):
|
|
"""
|
|
open a blob file to be written by peer, supports concurrent
|
|
writers, as long as they are from different peers.
|
|
|
|
returns tuple of (writer, finished_deferred)
|
|
|
|
writer - a file like object with a write() function, close() when finished
|
|
finished_deferred - deferred that is fired when write is finished and returns
|
|
a instance of itself as HashBlob
|
|
"""
|
|
if peer not in self.writers:
|
|
log.debug("Opening %s to be written by %s", str(self), str(peer))
|
|
finished_deferred = defer.Deferred()
|
|
writer = HashBlobWriter(self.get_length, self.writer_finished)
|
|
self.writers[peer] = (writer, finished_deferred)
|
|
return writer, finished_deferred
|
|
log.warning("Tried to download the same file twice simultaneously from the same peer")
|
|
return None, None
|
|
|
|
def open_for_reading(self):
|
|
"""
|
|
open blob for reading
|
|
|
|
returns a file like object that can be read() from, and closed() when
|
|
finished
|
|
"""
|
|
if self._verified is True:
|
|
f = open(self.file_path, 'rb')
|
|
reader = HashBlobReader(f, self.reader_finished)
|
|
self.readers += 1
|
|
return reader
|
|
return None
|
|
|
|
def delete(self):
|
|
"""
|
|
delete blob file from file system, prevent deletion
|
|
if a blob is being read from or written to
|
|
|
|
returns a deferred that firesback when delete is completed
|
|
"""
|
|
if not self.writers and not self.readers:
|
|
self._verified = False
|
|
self.saved_verified_blob = False
|
|
|
|
def delete_from_file_system():
|
|
if os.path.isfile(self.file_path):
|
|
os.remove(self.file_path)
|
|
|
|
d = threads.deferToThread(delete_from_file_system)
|
|
|
|
def log_error(err):
|
|
log.warning("An error occurred deleting %s: %s",
|
|
str(self.file_path), err.getErrorMessage())
|
|
return err
|
|
|
|
d.addErrback(log_error)
|
|
return d
|
|
else:
|
|
return defer.fail(Failure(
|
|
ValueError("File is currently being read or written and cannot be deleted")))
|
|
|
|
@property
|
|
def verified(self):
|
|
"""
|
|
Protect verified from being modified by other classes.
|
|
verified is True if a write to a blob has completed successfully,
|
|
or a blob has been read to have the same length as specified
|
|
in init
|
|
"""
|
|
return self._verified
|
|
|
|
def set_length(self, length):
|
|
if self.length is not None and length == self.length:
|
|
return True
|
|
if self.length is None and 0 <= length <= MAX_BLOB_SIZE:
|
|
self.length = length
|
|
return True
|
|
log.warning("Got an invalid length. Previous length: %s, Invalid length: %s",
|
|
self.length, length)
|
|
return False
|
|
|
|
def get_length(self):
|
|
return self.length
|
|
|
|
def get_is_verified(self):
|
|
return self.verified
|
|
|
|
def is_downloading(self):
|
|
if self.writers:
|
|
return True
|
|
return False
|
|
|
|
def reader_finished(self, reader):
|
|
self.readers -= 1
|
|
return defer.succeed(True)
|
|
|
|
def writer_finished(self, writer, err=None):
|
|
def fire_finished_deferred():
|
|
self._verified = True
|
|
for p, (w, finished_deferred) in list(self.writers.items()):
|
|
if w == writer:
|
|
del self.writers[p]
|
|
finished_deferred.callback(self)
|
|
return True
|
|
log.warning(
|
|
"Somehow, the writer that was accepted as being valid was already removed: %s",
|
|
writer)
|
|
return False
|
|
|
|
def errback_finished_deferred(err):
|
|
for p, (w, finished_deferred) in list(self.writers.items()):
|
|
if w == writer:
|
|
del self.writers[p]
|
|
finished_deferred.errback(err)
|
|
|
|
def cancel_other_downloads():
|
|
for p, (w, finished_deferred) in self.writers.items():
|
|
w.close()
|
|
|
|
if err is None:
|
|
if writer.len_so_far == self.length and writer.blob_hash == self.blob_hash:
|
|
if self._verified is False:
|
|
d = self.save_verified_blob(writer)
|
|
d.addCallbacks(lambda _: fire_finished_deferred(), errback_finished_deferred)
|
|
d.addCallback(lambda _: cancel_other_downloads())
|
|
else:
|
|
d = defer.succeed(None)
|
|
fire_finished_deferred()
|
|
else:
|
|
if writer.len_so_far != self.length:
|
|
err_string = "blob length is %i vs expected %i" % (writer.len_so_far, self.length)
|
|
else:
|
|
err_string = f"blob hash is {writer.blob_hash} vs expected {self.blob_hash}"
|
|
errback_finished_deferred(Failure(InvalidDataError(err_string)))
|
|
d = defer.succeed(None)
|
|
else:
|
|
errback_finished_deferred(err)
|
|
d = defer.succeed(None)
|
|
d.addBoth(lambda _: writer.close_handle())
|
|
return d
|
|
|
|
def save_verified_blob(self, writer):
|
|
# we cannot have multiple _save_verified_blob interrupting
|
|
# each other, can happen since startProducing is a deferred
|
|
return self.blob_write_lock.run(self._save_verified_blob, writer)
|
|
|
|
@defer.inlineCallbacks
|
|
def _save_verified_blob(self, writer):
|
|
if self.saved_verified_blob is False:
|
|
writer.write_handle.seek(0)
|
|
out_path = os.path.join(self.blob_dir, self.blob_hash)
|
|
producer = FileBodyProducer(writer.write_handle)
|
|
yield producer.startProducing(open(out_path, 'wb'))
|
|
self.saved_verified_blob = True
|
|
defer.returnValue(True)
|
|
else:
|
|
raise DownloadCanceledError()
|