summaryrefslogtreecommitdiffstats
path: root/BitTorrent/Storage.py
diff options
context:
space:
mode:
Diffstat (limited to 'BitTorrent/Storage.py')
-rw-r--r--BitTorrent/Storage.py276
1 files changed, 276 insertions, 0 deletions
diff --git a/BitTorrent/Storage.py b/BitTorrent/Storage.py
new file mode 100644
index 0000000..197fdef
--- /dev/null
+++ b/BitTorrent/Storage.py
@@ -0,0 +1,276 @@
+# 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
+
+import os
+from bisect import bisect_right
+from array import array
+
+from BitTorrent.obsoletepythonsupport import *
+
+from BitTorrent import BTFailure, app_name
+
+
+class FilePool(object):
+
+ def __init__(self, max_files_open):
+ self.allfiles = {}
+ self.handlebuffer = None
+ self.handles = {}
+ self.whandles = {}
+ self.set_max_files_open(max_files_open)
+
+ def close_all(self):
+ failures = {}
+ for filename, handle in self.handles.iteritems():
+ try:
+ handle.close()
+ except Exception, e:
+ failures[self.allfiles[filename]] = e
+ self.handles.clear()
+ self.whandles.clear()
+ if self.handlebuffer is not None:
+ del self.handlebuffer[:]
+ for torrent, e in failures.iteritems():
+ torrent.got_exception(e)
+
+ def set_max_files_open(self, max_files_open):
+ if max_files_open <= 0:
+ max_files_open = 1e100
+ self.max_files_open = max_files_open
+ self.close_all()
+ if len(self.allfiles) > self.max_files_open:
+ self.handlebuffer = []
+ else:
+ self.handlebuffer = None
+
+ def add_files(self, files, torrent):
+ for filename in files:
+ if filename in self.allfiles:
+ raise BTFailure(_("File %s belongs to another running torrent")
+ % filename)
+ for filename in files:
+ self.allfiles[filename] = torrent
+ if self.handlebuffer is None and \
+ len(self.allfiles) > self.max_files_open:
+ self.handlebuffer = self.handles.keys()
+
+ def remove_files(self, files):
+ for filename in files:
+ del self.allfiles[filename]
+ if self.handlebuffer is not None and \
+ len(self.allfiles) <= self.max_files_open:
+ self.handlebuffer = None
+
+
+# Make this a separate function because having this code in Storage.__init__()
+# would make python print a SyntaxWarning (uses builtin 'file' before 'global')
+
+def bad_libc_workaround():
+ global file
+ def file(name, mode = 'r', buffering = None):
+ return open(name, mode)
+
+class Storage(object):
+
+ def __init__(self, config, filepool, files, check_only=False):
+ self.filepool = filepool
+ self.config = config
+ self.ranges = []
+ self.myfiles = {}
+ self.tops = {}
+ self.undownloaded = {}
+ self.unallocated = {}
+ total = 0
+ for filename, length in files:
+ self.unallocated[filename] = length
+ self.undownloaded[filename] = length
+ if length > 0:
+ self.ranges.append((total, total + length, filename))
+ self.myfiles[filename] = None
+ total += length
+ if os.path.exists(filename):
+ if not os.path.isfile(filename):
+ raise BTFailure(_("File %s already exists, but is not a "
+ "regular file") % filename)
+ l = os.path.getsize(filename)
+ if l > length and not check_only:
+ h = file(filename, 'rb+')
+ h.truncate(length)
+ h.close()
+ l = length
+ self.tops[filename] = l
+ elif not check_only:
+ f = os.path.split(filename)[0]
+ if f != '' and not os.path.exists(f):
+ os.makedirs(f)
+ file(filename, 'wb').close()
+ self.begins = [i[0] for i in self.ranges]
+ self.total_length = total
+ if check_only:
+ return
+ self.handles = filepool.handles
+ self.whandles = filepool.whandles
+
+ # Rather implement this as an ugly hack here than change all the
+ # individual calls. Affects all torrent instances using this module.
+ if config['bad_libc_workaround']:
+ bad_libc_workaround()
+
+ def was_preallocated(self, pos, length):
+ for filename, begin, end in self._intervals(pos, length):
+ if self.tops.get(filename, 0) < end:
+ return False
+ return True
+
+ def get_total_length(self):
+ return self.total_length
+
+ def _intervals(self, pos, amount):
+ r = []
+ stop = pos + amount
+ p = bisect_right(self.begins, pos) - 1
+ while p < len(self.ranges) and self.ranges[p][0] < stop:
+ begin, end, filename = self.ranges[p]
+ r.append((filename, max(pos, begin) - begin, min(end, stop) - begin))
+ p += 1
+ return r
+
+ def _get_file_handle(self, filename, for_write):
+ handlebuffer = self.filepool.handlebuffer
+ if filename in self.handles:
+ if for_write and filename not in self.whandles:
+ self.handles[filename].close()
+ self.handles[filename] = file(filename, 'rb+', 0)
+ self.whandles[filename] = None
+ if handlebuffer is not None and handlebuffer[-1] != filename:
+ handlebuffer.remove(filename)
+ handlebuffer.append(filename)
+ else:
+ if for_write:
+ self.handles[filename] = file(filename, 'rb+', 0)
+ self.whandles[filename] = None
+ else:
+ self.handles[filename] = file(filename, 'rb', 0)
+ if handlebuffer is not None:
+ if len(handlebuffer) >= self.filepool.max_files_open:
+ oldfile = handlebuffer.pop(0)
+ if oldfile in self.whandles: # .pop() in python 2.3
+ del self.whandles[oldfile]
+ self.handles[oldfile].close()
+ del self.handles[oldfile]
+ handlebuffer.append(filename)
+ return self.handles[filename]
+
+ def read(self, pos, amount):
+ r = []
+ for filename, pos, end in self._intervals(pos, amount):
+ h = self._get_file_handle(filename, False)
+ h.seek(pos)
+ r.append(h.read(end - pos))
+ r = ''.join(r)
+ if len(r) != amount:
+ raise BTFailure(_("Short read - something truncated files?"))
+ return r
+
+ def write(self, pos, s):
+ # might raise an IOError
+ total = 0
+ for filename, begin, end in self._intervals(pos, len(s)):
+ h = self._get_file_handle(filename, True)
+ h.seek(begin)
+ h.write(s[total: total + end - begin])
+ total += end - begin
+
+ def close(self):
+ error = None
+ for filename in self.handles.keys():
+ if filename in self.myfiles:
+ try:
+ self.handles[filename].close()
+ except Exception, e:
+ error = e
+ del self.handles[filename]
+ if filename in self.whandles:
+ del self.whandles[filename]
+ handlebuffer = self.filepool.handlebuffer
+ if handlebuffer is not None:
+ handlebuffer = [f for f in handlebuffer if f not in self.myfiles]
+ self.filepool.handlebuffer = handlebuffer
+ if error is not None:
+ raise error
+
+ def write_fastresume(self, resumefile, amount_done):
+ resumefile.write('BitTorrent resume state file, version 1\n')
+ resumefile.write(str(amount_done) + '\n')
+ for x, x, filename in self.ranges:
+ resumefile.write(str(os.path.getsize(filename)) + ' ' +
+ str(os.path.getmtime(filename)) + '\n')
+
+ def check_fastresume(self, resumefile, return_filelist=False,
+ piece_size=None, numpieces=None, allfiles=None):
+ filenames = [name for x, x, name in self.ranges]
+ if resumefile is not None:
+ version = resumefile.readline()
+ if version != 'BitTorrent resume state file, version 1\n':
+ raise BTFailure(_("Unsupported fastresume file format, "
+ "maybe from another client version?"))
+ amount_done = int(resumefile.readline())
+ else:
+ amount_done = size = mtime = 0
+ for filename in filenames:
+ if resumefile is not None:
+ line = resumefile.readline()
+ size, mtime = line.split()[:2] # allow adding extra fields
+ size = int(size)
+ mtime = int(mtime)
+ if os.path.exists(filename):
+ fsize = os.path.getsize(filename)
+ else:
+ raise BTFailure(_("Another program appears to have moved, renamed, or deleted the file, "
+ "or %s may have crashed last time it was run.") % app_name)
+ if fsize > 0 and mtime != os.path.getmtime(filename):
+ raise BTFailure(_("Another program appears to have modified the file, "
+ "or %s may have crashed last time it was run.") % app_name)
+ if size != fsize:
+ raise BTFailure(_("Another program appears to have changed the file size, "
+ "or %s may have crashed last time it was run.") % app_name)
+ if not return_filelist:
+ return amount_done
+ if resumefile is None:
+ return None
+ if numpieces < 32768:
+ typecode = 'h'
+ else:
+ typecode = 'l'
+ try:
+ r = array(typecode)
+ r.fromfile(resumefile, numpieces)
+ except Exception, e:
+ raise BTFailure(_("Couldn't read fastresume data: ") + str(e) + '.')
+ for i in range(numpieces):
+ if r[i] >= 0:
+ # last piece goes "past the end", doesn't matter
+ self.downloaded(r[i] * piece_size, piece_size)
+ if r[i] != -2:
+ self.allocated(i * piece_size, piece_size)
+ undl = self.undownloaded
+ unal = self.unallocated
+ return amount_done, [undl[x] for x in allfiles], \
+ [not unal[x] for x in allfiles]
+
+ def allocated(self, pos, length):
+ for filename, begin, end in self._intervals(pos, length):
+ self.unallocated[filename] -= end - begin
+
+ def downloaded(self, pos, length):
+ for filename, begin, end in self._intervals(pos, length):
+ self.undownloaded[filename] -= end - begin